refacor all the things \o/

This commit is contained in:
uwap 2017-09-19 10:12:32 +02:00
parent d729d949bd
commit 538162d38c
15 changed files with 721 additions and 402 deletions

View file

@ -5,19 +5,15 @@ import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem';
import Slider from 'material-ui/Slider';
import Config from "./config";
import { keyOf } from "./util";
import R from "ramda";
const keyOf = (map: Map<any,any>, value: any) : ?any =>
((keys) => keys[R.findIndex(k => map[k] == value, keys)])
(R.keys(map));
const enabled = (props: ControlUI, state: State) => {
const key = keyOf(Config.topics[props.topic].values,
state.values[props.topic]);
if (props.enableCondition == null) return true;
if (key == null) return false;
else {
return props.enableCondition(key);
const val = state.values[props.topic];
return props.enableCondition(
val.internal == null ? val.actual : val.internal, val.actual);
}
};
@ -35,17 +31,12 @@ const onToggle = (topic: string, props: ControlUI, state: State) =>
export const toggle = (state: State, props: ControlUI) => {
const toggled = (() => {
const val = state.values[props.topic];
if (props.toggled != null) {
console.log(state.values[props.topic]);
console.log(Config.topics[props.topic].values);
const key = keyOf(Config.topics[props.topic].values,
state.values[props.topic]);
if (key == null) return true;
console.log(key);
if (props.toggled != null) return props.toggled(key); //only for flow </3
return props.toggled(val.internal == null ? val.actual : val.internal,
val.actual);
} else {
return state.values[props.topic] ===
getValue(props.topic, R.propOr("on", "on", props));
return val.internal === R.propOr("on", "on", props);
}
})();
return (<Toggle label={props.text}
@ -67,7 +58,7 @@ const dropDownItem = (topic: string) => (text: string, key: string) => (
);
export const dropDown = (state: State, props: ControlUI) => (
<SelectField value={state.values[props.topic]}
<SelectField value={state.values[props.topic].actual}
onChange={onDropDownChange(props.topic, props, state)}
disabled={!(enabled(props, state))}>
{R.values(R.mapObjIndexed(dropDownItem(props.topic), props.options))}
@ -84,7 +75,7 @@ const onSliderChange = (state: State, props: ControlUI) =>
export const slider = (state: State, props: ControlUI) => (
<div>
<span>{props.text}</span>
<Slider value={state.values[props.topic]}
<Slider value={state.values[props.topic].actual}
min={props.min == null ? 0 : props.min}
max={props.max == null ? 1 : props.max}
step={props.step == null ? 1 : props.step}

View file

@ -3,6 +3,7 @@ import React from "react";
import AppBar from "material-ui/AppBar";
import CircularProgress from "material-ui/CircularProgress";
import MapIcon from "material-ui/svg-icons/maps/map";
import PhonelinkOffIcon from "material-ui/svg-icons/hardware/phonelink-off";
import IconMenu from "material-ui/IconMenu";
import IconButton from "material-ui/IconButton";
import MenuItem from "material-ui/MenuItem";
@ -13,19 +14,24 @@ const TopBarIndicatorMenu = (props: Object) => (
iconButtonElement={
<IconButton style={{width:48, height:48, padding: 0}}
iconStyle={{width:48, height: 48}}
tooltip="Connected!">
<MapIcon color={grey50} />
tooltip={props.mqtt.connected ? "Connected!" : "Disconnected!"}>
{props.mqtt.connected ?
(<MapIcon color={grey50} />) :
(<PhonelinkOffIcon color={grey50} />)}
</IconButton>}
style={{width:48, height:48}}>
<MenuItem primaryText="Reconnect" />
<MenuItem primaryText="Reconnect (Not yet implemented)" />
</IconMenu>
);
const TopBarIndicator = (props: Object) => (
props.mqtt == null ? <CircularProgress size={48} color={grey50} />
: <TopBarIndicatorMenu {...props} />
);
const TopBarIndicator = (props: Object) => {
if (props.mqtt == null || props.mqtt.reconnecting) {
return (<CircularProgress size={48} color={grey50} />);
} else {
return (<TopBarIndicatorMenu {...props} />);
}
};
const TopBar = (props: Object) => (
<AppBar title={props.title}

View file

@ -4,31 +4,31 @@ const config : Config = {
led_stahltraeger: {
state: "/service/openhab/out/pca301_ledstrips/state",
command: "/service/openhab/in/pca301_ledstrips/command",
value: "OFF", // defaultValue
defaultValue: "OFF",
values: { on: "ON", off: "OFF" }
},
snackbar: {
state: "/service/openhab/out/pca301_snackbar/state",
command: "/service/openhab/in/pca301_snackbar/command",
value: "OFF",
defaultValue: "OFF",
values: { on: "ON", off: "OFF" }
},
twinkle: {
state: "/service/openhab/out/pca301_twinkle/state",
command: "/service/openhab/in/pca301_twinkle/command",
value: "OFF",
defaultValue: "OFF",
values: { on: "ON", off: "OFF" }
},
flyfry: {
state: "/service/openhab/out/wifi_flyfry/state",
command: "/service/openhab/in/wifi_flyfry/command",
value: "OFF",
defaultValue: "OFF",
values: { on: "ON", off: "OFF" }
},
artnet: {
state: "/artnet/state",
command: "/artnet/push",
value: "blackout",
defaultValue: "blackout",
values: { off: "blackout", yellow: "yellow", purple: "purple",
blue: "blue", green: "green", red: "red", random: "random",
cycle: "cycle-random" }
@ -36,21 +36,34 @@ const config : Config = {
onkyo_volume: {
state: "/service/onkyo/status/volume",
command: "/service/onkyo/set/volume",
value: 0,
defaultValue: 0,
values: {},
parseState: msg => JSON.parse(msg.toString()).val
},
onkyo_inputs: {
state: "/service/onkyo/status/input-selector",
command: "/service/onkyo/command",
defaultValue: "SLI00",
values: { tisch: "SLI11", chromecast: "SLI01", pult: "SLI10" },
parseState: msg => JSON.parse(msg.toString()).val
},
rundumleuchte: {
state: "/service/openhab/out/pca301_rundumleuchte/state",
command: "/service/openhab/in/pca301_rundumleuchte/command",
value: "OFF",
defaultValue: "OFF",
values: { on: "ON", off: "OFF" }
},
door_status: {
state: "/service/status",
command: "",
value: "\"closed\"",
defaultValue: "\"closed\"",
values: { on: "\"open\"", off: "\"closed\"" }
},
infoscreen: {
state: "/service/openhab/out/pca301_infoscreen/state",
command: "/service/openhab/in/pca301_infoscreen/command",
defaultValue: "OFF",
values: { on: "ON", off: "OFF" }
}
},
controls: {
@ -153,12 +166,22 @@ const config : Config = {
topic: "onkyo_volume",
min: 0,
max: 100
},
{
type: "dropDown",
text: "Inputs",
topic: "onkyo_inputs",
options: {
tisch: "Tisch",
chromecast: "Chromecast",
pult: "Pult"
}
}
]
},
rundumleuchte: {
name: "Rundumleuchte",
position: [240,210],
position: [225,220],
icon: "wb_sunny",
iconColor: state => state.rundumleuchte == "on" ? "#CCCC00" : "#000000",
ui: [
@ -175,6 +198,19 @@ const config : Config = {
icon: "swap_vert",
iconColor: state => state.door_status == "on" ? "#00FF00" : "#FF0000",
ui: []
},
infoscreen: {
name: "Infoscreen",
position: [255, 195],
icon: "developer_board",
iconColor: state => state.infoscreen == "on" ? "#4444FF" : "#000000",
ui: [
{
type: "toggle",
text: "Infoscreen",
topic: "infoscreen"
}
]
}
}
};

View file

@ -4,8 +4,7 @@ import ReactDOM from "react-dom";
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import Drawer from 'material-ui/Drawer';
import injectTapEventPlugin from 'react-tap-event-plugin';
import { createStore } from "redux";
import { MQTT_CONNECT } from "./stateActions";
import { store } from "./state";
import connectMqtt from "./mqtt";
import AppBar from "./appbar";
import Toggle from "material-ui/Toggle";
@ -17,47 +16,48 @@ import { Toolbar, ToolbarGroup, ToolbarTitle } from "material-ui/Toolbar";
injectTapEventPlugin();
const appState : State = {
mqtt: null,
ui: null,
values: R.map(R.prop("value"), Config.topics)
};
console.log(appState.values);
const handleEvent = (state = appState, action) => {
switch (action.type) {
case "CONNECT":
return R.merge(state, { mqtt: action.mqtt });
case "uiopen":
return R.merge(state, { ui: action.ui });
case "uiclose":
return R.merge(state, { ui: null });
case "mqtt_message":
console.log(action.topic + ": " + action.message.toString());
const val = (topic: string) =>
Config.topics[topic].parseState == null ?
action.message.toString() :
Config.topics[topic].parseState(action.message);
const keysToUpdate = R.keys(R.pickBy(val => val.state == action.topic,
Config.topics));
return R.mergeDeepRight(state, R.objOf("values", R.mergeAll(R.map(
k => R.objOf(k, val(k)), keysToUpdate))));
/*
return R.merge(state, R.objOf("topics", R.merge(state.topics,
R.map(R.merge(R.__, { value: val }),
R.pickBy(val => val.state == action.topic, Config.topics)))));
*/
}
return state;
};
const store = createStore(handleEvent);
// const appState : State = {
// mqtt: null,
// ui: null,
// values: R.map(R.prop("value"), Config.topics)
// };
//
// console.log(appState.values);
//
// const handleEvent = (state = appState, action) => {
// switch (action.type) {
// case "CONNECT":
// return R.merge(state, { mqtt: action.mqtt });
// case "uiopen":
// return R.merge(state, { ui: action.ui });
// case "uiclose":
// return R.merge(state, { ui: null });
// case "mqtt_message":
// console.log(action.topic + ": " + action.message.toString());
// const val = (topic: string) =>
// Config.topics[topic].parseState == null ?
// action.message.toString() :
// Config.topics[topic].parseState(action.message);
// const keysToUpdate = R.keys(R.pickBy(val => val.state == action.topic,
// Config.topics));
// return R.mergeDeepRight(state, R.objOf("values", R.mergeAll(R.map(
// k => R.objOf(k, val(k)), keysToUpdate))));
// /*
// return R.merge(state, R.objOf("topics", R.merge(state.topics,
// R.map(R.merge(R.__, { value: val }),
// R.pickBy(val => val.state == action.topic, Config.topics)))));
// */
// }
// return state;
// };
//
// const store = createStore(handleEvent);
const UiItem = (state) => (props : ControlUI) =>
UiItems[props.type](state, props);
const renderUi = (state, key) => key != null && Config.controls[key] != null ?
const renderUi = (state: State, key: ?string) =>
key != null && Config.controls[key] != null ?
R.map(UiItem(state), Config.controls[key].ui) : null;
const App = (state: State) => (
@ -65,16 +65,17 @@ const App = (state: State) => (
<MuiThemeProvider>
<div>
<AppBar title="RZL Map" {...state} />
<Drawer open={state.ui != null} openSecondary={true} disableSwipeToOpen={true}>
<Drawer open={state.uiOpened != null}
openSecondary={true} disableSwipeToOpen={true}>
<Toolbar>
<ToolbarGroup firstChild={true}>
<ToolbarTitle text={
state.ui == null ? "" : Config.controls[state.ui].name}
state.uiOpened == null ? "" : Config.controls[state.uiOpened].name}
style={{"marginLeft": 10}} />
</ToolbarGroup>
</Toolbar>
<div id="drawer_uiComponents">
{renderUi(state, state.ui)}
{renderUi(state, state.uiOpened)}
</div>
</Drawer>
</div>
@ -84,9 +85,9 @@ const App = (state: State) => (
</div>
);
store.dispatch({type: null});
store.subscribe(() => ReactDOM.render(<App {...store.getState()} />, document.getElementById("content")));
store.dispatch({type: null});
store.dispatch({type: "mqtt_message", topic: "/service/openhab/out/pca301_ledstrips/state", message: "ON"});
connectMqtt("ws://172.22.36.207:1884", store); // wss://mqtt.starletp9.de/mqtt", store);
// 192.168.178.6
connectMqtt("ws:192.168.178.6:1884", store); // "ws://172.22.36.207:1884", store); // wss://mqtt.starletp9.de/mqtt", store);

View file

@ -4,6 +4,8 @@ import { Map, ImageOverlay, Marker, Popup } from "react-leaflet";
import Leaflet from "leaflet";
import R from "ramda";
import Config from "./config";
import { Actions } from "./state";
import { keyOf } from "./util";
import ActionInfo from 'material-ui/svg-icons/action/info';
import ReactDOM from "react-dom";
@ -11,16 +13,12 @@ import ReactDOM from "react-dom";
// convert width/height coordinates to -height/width coordinates
const c = (p) => [-p[1], p[0]]
const keyOf = (map: Map<any,any>, value: any) : ?any =>
((keys) => keys[R.findIndex(k => map[k] == value, keys)])
(R.keys(map));
const color = (iconColor, state: State) => {
console.log(
R.mapObjIndexed((x,k) => keyOf(Config.topics[k].values, x), state.values)
); return iconColor == null ? "#000000" :
// TODO: give iconColor not only internal but also actual values
return iconColor == null ? "#000000" :
iconColor(
R.mapObjIndexed((x,k) => keyOf(Config.topics[k].values, x), state.values)
R.map(x => x.internal == null ?
x.actual : x.internal, state.values)
);
}
const iconHtml = (el, state: State) =>
@ -35,9 +33,10 @@ const Markers = (props) => R.values(R.mapObjIndexed((el,key) => (
html: iconHtml(el, props.state),
iconSize: Leaflet.point(32,32)
})}>
<Popup onOpen={() => props.store.dispatch({type: "uiopen", ui: key})}
onClose={() => props.store.dispatch({type: "uiclose"})}>
<span>{el.name}</span>
<Popup
onOpen={() => props.store.dispatch({type: Actions.CHANGE_UI, payload: key})}
onClose={() => props.store.dispatch({type: Actions.CHANGE_UI})}>
<span>{el.name}</span>
</Popup>
</Marker>
), R.propOr({}, "controls", Config)));

View file

@ -1,6 +1,6 @@
// @flow
import mqtt from "mqtt";
import { MQTT_MESSAGE, MQTT_CONNECT, MQTT_DISCONNECT } from "./stateActions";
import { Actions } from "./state";
import { Store } from "redux";
import Config from "./config";
import R from "ramda";
@ -9,14 +9,18 @@ export default function connectMqtt(url: string, store: Store<*,*>) {
const client = mqtt.connect(url);
client.on("connect", () => {
store.dispatch({
type: MQTT_CONNECT, mqtt: client
type: Actions.MQTT_CONNECT, payload: client
});
R.forEachObjIndexed(v =>
client.subscribe(v.state), Config.topics);
});
client.on("message", (topic, message) => {
store.dispatch({
type: "mqtt_message", message: message, topic: topic
type: Actions.MQTT_MESSAGE,
payload: {
message: message,
topic: topic
}
});
});
}

61
src/state.js Normal file
View file

@ -0,0 +1,61 @@
// @flow
import R from "ramda";
import { createStore } from "redux";
import Config from "./config";
import { keyOf } from "./util";
export const Actions = Object.freeze({
MQTT_CONNECT: "CONNECT",
MQTT_MESSAGE: "MESSAGE",
CHANGE_UI: "UI_POPUP"
});
const initState : State = {
mqtt: null,
uiOpened: null,
values: R.map(
topic => { return {
internal: keyOf(topic.values, topic.defaultValue),
actual: topic.defaultValue
}}, Config.topics)
};
const onMessage = (state: State, action: StateAction) => {
if (action.payload == undefined) return state;
// action.payload.topic is the mqtt topic
// topics is the list of all internal topic references
// that have their state topic set to action.payload.topic
const payload = action.payload == undefined ? { topic: "", message: {} }
: action.payload; // thx flow </3
const topics = R.keys(R.pickBy(
val => val.state == payload.topic, Config.topics));
const message = payload.message;
const parsedMessage = (topic: string) => {
let parseFunction = Config.topics[topic].parseState;
if (parseFunction == null) {
return message.toString();
} else {
return parseFunction(message);
}
}
const newValue = (topic: string) => {
return {
actual: parsedMessage(topic),
internal: keyOf(Config.topics[topic].values,parsedMessage(topic))
};
}
return R.mergeDeepRight(state, R.objOf("values", R.mergeAll(
R.map(topic => R.objOf(topic, newValue(topic)), topics)
)));
}
const match = (value: any, array: Map<any,any>) => array[value];
const handleEvent = (state: State = initState, action: StateAction) => {
return match(action.type, {
[Actions.MQTT_CONNECT ]: R.merge(state, { mqtt: action.payload }),
[Actions.MQTT_MESSAGE ]: onMessage(state, action),
[Actions.CHANGE_UI ]: R.merge(state, { uiOpened: action.payload })
});
}
export const store = createStore(handleEvent, initState);

View file

@ -1,5 +0,0 @@
// @flow
export const MQTT_DISCONNECT = "DISCONNECT";
export const MQTT_CONNECT = "CONNECT";
export const MQTT_MESSAGE = "MESSAGE";

6
src/util.js Normal file
View file

@ -0,0 +1,6 @@
// @flow
import R from "ramda";
export const keyOf = <a,b> (map: Map<b,a>, value: a) : ?b =>
((keys) => keys[R.findIndex(k => map[k] == value, keys)])
(R.keys(map));