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

@ -1,5 +1,5 @@
{ {
"presets": [ "presets": [
"es2015", "react" "env", "react"
] ],
} }

View file

@ -9,3 +9,6 @@ types/types.js
[options] [options]
esproposal.export_star_as=enable esproposal.export_star_as=enable
unsafe.enable_getters_and_setters=true unsafe.enable_getters_and_setters=true
[lints]
all=warn

View file

@ -1,4 +1,4 @@
# RZL Map # Space Map
## How to set up ## How to set up
@ -22,19 +22,19 @@ The topics section defines the mqtt interfaces.
The Controls define the UI Controls. The Controls define the UI Controls.
| Name | Type | Optional? | Default | | Name | Type | Optional? | Default | Description |
|-----------------|-------------------|------------|-----------------| |-----------------|-------------------|------------|-----------------|-------------|
| type | "toggle" | "dropDown" | "slider" | No | | | type | "toggle" \| "dropDown" \| "slider" | No | | The type of the UI element. |
| text | string | No | | | text | string | No | | The text displayed right next to the UI element. |
| topic | string | No | | | topic | string | No | | The topic the UI element is supposed to change and/or receive its status from. |
| enableCondition | string => boolean | Yes | () => true | | enableCondition | (key: string, value: *) => boolean | Yes | () => true | This option allows you to enable or disable UI elements, depending on the current state. The first parameter is the internal representation of the value. For example "off". The second parameter is the actual value that was received via MQTT. Return true to enable the element and false to disable it. |
| **Toggle Options** | | **Toggle Options** |
| on | string | Yes | "on" | | on | string | Yes | "on" | If the state is equal to the value of this option the toggle will be toggled on (if the toggled function is not overriden). This is also the value that will be sent when the button is toggled on. |
| off | string | Yes | "off" | | off | string | Yes | "off" | If the state is equal to the value of this option the toggle will be toggled off (if the toggled function is not overriden). This is also the value that will be sent when the button is toggled off. |
| toggled | string => boolean | Yes | x => x == "off" | | toggled | (key: string, value: *) => boolean | Yes | Use the on and off options | This is the function that decides whether the button should be in a toggled state or not. The parameters are equivalent to those of `enableCondition`. Return true to set the button to a toggled state. Return false to set it to the untoggled state. |
| **DropDown Options** | | **DropDown Options** |
| options | Map<string,*> | Yes | {} | | options | Map<string,string>| Yes | {} | This is an attribute set that will map all values defined in the topics section to a description text. For example `{ on: "Lights On", off: "Lights Off" }` will give the drop down element two options, one that is named `Lights On` and one that is named `Lights Off`. |
| **Slider Options** | | **Slider Options** |
| min | number | Yes | 0 | | min | number | Yes | 0 | The minimum value of that slider. |
| max | number | Yes | 1 | | max | number | Yes | 1 | The maximum value of that slider. |
| step | number | Yes | 1 | | step | number | Yes | 1 | The smallest step of the slider. |

View file

@ -7,8 +7,8 @@
"build": "webpack --bail" "build": "webpack --bail"
}, },
"dependencies": { "dependencies": {
"babel-preset-env": "^1.6.0",
"leaflet": "^1.2.0", "leaflet": "^1.2.0",
"lodash": "^4.17.4",
"material-ui": "^0.18.7", "material-ui": "^0.18.7",
"mqtt": "^2.11.0", "mqtt": "^2.11.0",
"ramda": "^0.24.1", "ramda": "^0.24.1",
@ -22,7 +22,6 @@
"babel-cli": "^6.24.1", "babel-cli": "^6.24.1",
"babel-core": "^6.25.0", "babel-core": "^6.25.0",
"babel-loader": "^7.1.1", "babel-loader": "^7.1.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"flow": "^0.2.3", "flow": "^0.2.3",
"flow-bin": "^0.50.0", "flow-bin": "^0.50.0",

View file

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

View file

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

View file

@ -4,31 +4,31 @@ const config : Config = {
led_stahltraeger: { led_stahltraeger: {
state: "/service/openhab/out/pca301_ledstrips/state", state: "/service/openhab/out/pca301_ledstrips/state",
command: "/service/openhab/in/pca301_ledstrips/command", command: "/service/openhab/in/pca301_ledstrips/command",
value: "OFF", // defaultValue defaultValue: "OFF",
values: { on: "ON", off: "OFF" } values: { on: "ON", off: "OFF" }
}, },
snackbar: { snackbar: {
state: "/service/openhab/out/pca301_snackbar/state", state: "/service/openhab/out/pca301_snackbar/state",
command: "/service/openhab/in/pca301_snackbar/command", command: "/service/openhab/in/pca301_snackbar/command",
value: "OFF", defaultValue: "OFF",
values: { on: "ON", off: "OFF" } values: { on: "ON", off: "OFF" }
}, },
twinkle: { twinkle: {
state: "/service/openhab/out/pca301_twinkle/state", state: "/service/openhab/out/pca301_twinkle/state",
command: "/service/openhab/in/pca301_twinkle/command", command: "/service/openhab/in/pca301_twinkle/command",
value: "OFF", defaultValue: "OFF",
values: { on: "ON", off: "OFF" } values: { on: "ON", off: "OFF" }
}, },
flyfry: { flyfry: {
state: "/service/openhab/out/wifi_flyfry/state", state: "/service/openhab/out/wifi_flyfry/state",
command: "/service/openhab/in/wifi_flyfry/command", command: "/service/openhab/in/wifi_flyfry/command",
value: "OFF", defaultValue: "OFF",
values: { on: "ON", off: "OFF" } values: { on: "ON", off: "OFF" }
}, },
artnet: { artnet: {
state: "/artnet/state", state: "/artnet/state",
command: "/artnet/push", command: "/artnet/push",
value: "blackout", defaultValue: "blackout",
values: { off: "blackout", yellow: "yellow", purple: "purple", values: { off: "blackout", yellow: "yellow", purple: "purple",
blue: "blue", green: "green", red: "red", random: "random", blue: "blue", green: "green", red: "red", random: "random",
cycle: "cycle-random" } cycle: "cycle-random" }
@ -36,21 +36,34 @@ const config : Config = {
onkyo_volume: { onkyo_volume: {
state: "/service/onkyo/status/volume", state: "/service/onkyo/status/volume",
command: "/service/onkyo/set/volume", command: "/service/onkyo/set/volume",
value: 0, defaultValue: 0,
values: {}, values: {},
parseState: msg => JSON.parse(msg.toString()).val 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: { rundumleuchte: {
state: "/service/openhab/out/pca301_rundumleuchte/state", state: "/service/openhab/out/pca301_rundumleuchte/state",
command: "/service/openhab/in/pca301_rundumleuchte/command", command: "/service/openhab/in/pca301_rundumleuchte/command",
value: "OFF", defaultValue: "OFF",
values: { on: "ON", off: "OFF" } values: { on: "ON", off: "OFF" }
}, },
door_status: { door_status: {
state: "/service/status", state: "/service/status",
command: "", command: "",
value: "\"closed\"", defaultValue: "\"closed\"",
values: { on: "\"open\"", off: "\"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: { controls: {
@ -153,12 +166,22 @@ const config : Config = {
topic: "onkyo_volume", topic: "onkyo_volume",
min: 0, min: 0,
max: 100 max: 100
},
{
type: "dropDown",
text: "Inputs",
topic: "onkyo_inputs",
options: {
tisch: "Tisch",
chromecast: "Chromecast",
pult: "Pult"
}
} }
] ]
}, },
rundumleuchte: { rundumleuchte: {
name: "Rundumleuchte", name: "Rundumleuchte",
position: [240,210], position: [225,220],
icon: "wb_sunny", icon: "wb_sunny",
iconColor: state => state.rundumleuchte == "on" ? "#CCCC00" : "#000000", iconColor: state => state.rundumleuchte == "on" ? "#CCCC00" : "#000000",
ui: [ ui: [
@ -175,6 +198,19 @@ const config : Config = {
icon: "swap_vert", icon: "swap_vert",
iconColor: state => state.door_status == "on" ? "#00FF00" : "#FF0000", iconColor: state => state.door_status == "on" ? "#00FF00" : "#FF0000",
ui: [] 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 MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import Drawer from 'material-ui/Drawer'; import Drawer from 'material-ui/Drawer';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
import { createStore } from "redux"; import { store } from "./state";
import { MQTT_CONNECT } from "./stateActions";
import connectMqtt from "./mqtt"; import connectMqtt from "./mqtt";
import AppBar from "./appbar"; import AppBar from "./appbar";
import Toggle from "material-ui/Toggle"; import Toggle from "material-ui/Toggle";
@ -17,47 +16,48 @@ import { Toolbar, ToolbarGroup, ToolbarTitle } from "material-ui/Toolbar";
injectTapEventPlugin(); injectTapEventPlugin();
const appState : State = { // const appState : State = {
mqtt: null, // mqtt: null,
ui: null, // ui: null,
values: R.map(R.prop("value"), Config.topics) // values: R.map(R.prop("value"), Config.topics)
}; // };
//
console.log(appState.values); // console.log(appState.values);
//
const handleEvent = (state = appState, action) => { // const handleEvent = (state = appState, action) => {
switch (action.type) { // switch (action.type) {
case "CONNECT": // case "CONNECT":
return R.merge(state, { mqtt: action.mqtt }); // return R.merge(state, { mqtt: action.mqtt });
case "uiopen": // case "uiopen":
return R.merge(state, { ui: action.ui }); // return R.merge(state, { ui: action.ui });
case "uiclose": // case "uiclose":
return R.merge(state, { ui: null }); // return R.merge(state, { ui: null });
case "mqtt_message": // case "mqtt_message":
console.log(action.topic + ": " + action.message.toString()); // console.log(action.topic + ": " + action.message.toString());
const val = (topic: string) => // const val = (topic: string) =>
Config.topics[topic].parseState == null ? // Config.topics[topic].parseState == null ?
action.message.toString() : // action.message.toString() :
Config.topics[topic].parseState(action.message); // Config.topics[topic].parseState(action.message);
const keysToUpdate = R.keys(R.pickBy(val => val.state == action.topic, // const keysToUpdate = R.keys(R.pickBy(val => val.state == action.topic,
Config.topics)); // Config.topics));
return R.mergeDeepRight(state, R.objOf("values", R.mergeAll(R.map( // return R.mergeDeepRight(state, R.objOf("values", R.mergeAll(R.map(
k => R.objOf(k, val(k)), keysToUpdate)))); // k => R.objOf(k, val(k)), keysToUpdate))));
/* // /*
return R.merge(state, R.objOf("topics", R.merge(state.topics, // return R.merge(state, R.objOf("topics", R.merge(state.topics,
R.map(R.merge(R.__, { value: val }), // R.map(R.merge(R.__, { value: val }),
R.pickBy(val => val.state == action.topic, Config.topics))))); // R.pickBy(val => val.state == action.topic, Config.topics)))));
*/ // */
} // }
return state; // return state;
}; // };
//
const store = createStore(handleEvent); // const store = createStore(handleEvent);
const UiItem = (state) => (props : ControlUI) => const UiItem = (state) => (props : ControlUI) =>
UiItems[props.type](state, props); 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; R.map(UiItem(state), Config.controls[key].ui) : null;
const App = (state: State) => ( const App = (state: State) => (
@ -65,16 +65,17 @@ const App = (state: State) => (
<MuiThemeProvider> <MuiThemeProvider>
<div> <div>
<AppBar title="RZL Map" {...state} /> <AppBar title="RZL Map" {...state} />
<Drawer open={state.ui != null} openSecondary={true} disableSwipeToOpen={true}> <Drawer open={state.uiOpened != null}
openSecondary={true} disableSwipeToOpen={true}>
<Toolbar> <Toolbar>
<ToolbarGroup firstChild={true}> <ToolbarGroup firstChild={true}>
<ToolbarTitle text={ <ToolbarTitle text={
state.ui == null ? "" : Config.controls[state.ui].name} state.uiOpened == null ? "" : Config.controls[state.uiOpened].name}
style={{"marginLeft": 10}} /> style={{"marginLeft": 10}} />
</ToolbarGroup> </ToolbarGroup>
</Toolbar> </Toolbar>
<div id="drawer_uiComponents"> <div id="drawer_uiComponents">
{renderUi(state, state.ui)} {renderUi(state, state.uiOpened)}
</div> </div>
</Drawer> </Drawer>
</div> </div>
@ -84,9 +85,9 @@ const App = (state: State) => (
</div> </div>
); );
store.dispatch({type: null});
store.subscribe(() => ReactDOM.render(<App {...store.getState()} />, document.getElementById("content"))); store.subscribe(() => ReactDOM.render(<App {...store.getState()} />, document.getElementById("content")));
store.dispatch({type: null}); // 192.168.178.6
store.dispatch({type: "mqtt_message", topic: "/service/openhab/out/pca301_ledstrips/state", message: "ON"}); connectMqtt("ws:192.168.178.6:1884", store); // "ws://172.22.36.207:1884", store); // wss://mqtt.starletp9.de/mqtt", store);
connectMqtt("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 Leaflet from "leaflet";
import R from "ramda"; import R from "ramda";
import Config from "./config"; import Config from "./config";
import { Actions } from "./state";
import { keyOf } from "./util";
import ActionInfo from 'material-ui/svg-icons/action/info'; import ActionInfo from 'material-ui/svg-icons/action/info';
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
@ -11,16 +13,12 @@ import ReactDOM from "react-dom";
// convert width/height coordinates to -height/width coordinates // convert width/height coordinates to -height/width coordinates
const c = (p) => [-p[1], p[0]] 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) => { const color = (iconColor, state: State) => {
console.log( // TODO: give iconColor not only internal but also actual values
R.mapObjIndexed((x,k) => keyOf(Config.topics[k].values, x), state.values) return iconColor == null ? "#000000" :
); return iconColor == null ? "#000000" :
iconColor( 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) => const iconHtml = (el, state: State) =>
@ -35,9 +33,10 @@ const Markers = (props) => R.values(R.mapObjIndexed((el,key) => (
html: iconHtml(el, props.state), html: iconHtml(el, props.state),
iconSize: Leaflet.point(32,32) iconSize: Leaflet.point(32,32)
})}> })}>
<Popup onOpen={() => props.store.dispatch({type: "uiopen", ui: key})} <Popup
onClose={() => props.store.dispatch({type: "uiclose"})}> onOpen={() => props.store.dispatch({type: Actions.CHANGE_UI, payload: key})}
<span>{el.name}</span> onClose={() => props.store.dispatch({type: Actions.CHANGE_UI})}>
<span>{el.name}</span>
</Popup> </Popup>
</Marker> </Marker>
), R.propOr({}, "controls", Config))); ), R.propOr({}, "controls", Config)));

View file

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

View file

@ -3,7 +3,7 @@ declare type Map<K,V> = { [K]: V };
declare type Topic = { declare type Topic = {
state: string, state: string,
command: string, command: string,
value: any, defaultValue: any,
values: Map<string,any>, values: Map<string,any>,
parseState?: (msg: Object) => any parseState?: (msg: Object) => any
}; };
@ -14,12 +14,12 @@ declare type ControlUI = {
text: string, text: string,
topic: string, topic: string,
enableCondition?: (val: any) => boolean, enableCondition?: (internal: string, actual: any) => boolean,
// TOGGLE optional properties // TOGGLE optional properties
on?: string, // on override for toggle on?: string, // on override for toggle
off?: string, // off override for toggle off?: string, // off override for toggle
toggled?: (val: any) => boolean, toggled?: (internal: string, actual: any) => boolean,
// DROPDOWN optional properties // DROPDOWN optional properties
options?: Map<string,any>, //options for dropDown options?: Map<string,any>, //options for dropDown
@ -46,6 +46,17 @@ declare type Config = {
declare type State = { declare type State = {
mqtt: ?any, mqtt: ?any,
ui: ?string, uiOpened: ?string,
values: Map<string,any> // A map of the actual state values for each topic.
// internal is the internal term for the value,
// that is equal to the key in the values section of that
// topic, for example given by:
// values: { off: "OFF", on: "ON" }
// and actual is the value of that or whatever is given by mqtt.
values: Map<string, { internal: ?string, actual: any }>
}; };
declare type StateAction = {
type: "DISCONNECT" | "CONNECT" | "MESSAGE" | "UI_POPUP",
payload?: any
}

765
yarn.lock

File diff suppressed because it is too large Load diff