refacor all the things \o/
This commit is contained in:
parent
d729d949bd
commit
538162d38c
15 changed files with 721 additions and 402 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
21
src/map.js
21
src/map.js
|
|
@ -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)));
|
||||
|
|
|
|||
10
src/mqtt.js
10
src/mqtt.js
|
|
@ -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
61
src/state.js
Normal 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);
|
||||
|
|
@ -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
6
src/util.js
Normal 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));
|
||||
Loading…
Add table
Add a link
Reference in a new issue