refacor all the things \o/
This commit is contained in:
parent
d729d949bd
commit
538162d38c
15 changed files with 721 additions and 402 deletions
4
.babelrc
4
.babelrc
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"es2015", "react"
|
||||
]
|
||||
"env", "react"
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,3 +9,6 @@ types/types.js
|
|||
[options]
|
||||
esproposal.export_star_as=enable
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[lints]
|
||||
all=warn
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -1,4 +1,4 @@
|
|||
# RZL Map
|
||||
# Space Map
|
||||
|
||||
## How to set up
|
||||
|
||||
|
|
@ -22,19 +22,19 @@ The topics section defines the mqtt interfaces.
|
|||
|
||||
The Controls define the UI Controls.
|
||||
|
||||
| Name | Type | Optional? | Default |
|
||||
|-----------------|-------------------|------------|-----------------|
|
||||
| type | "toggle" | "dropDown" | "slider" | No | |
|
||||
| text | string | No | |
|
||||
| topic | string | No | |
|
||||
| enableCondition | string => boolean | Yes | () => true |
|
||||
| Name | Type | Optional? | Default | Description |
|
||||
|-----------------|-------------------|------------|-----------------|-------------|
|
||||
| type | "toggle" \| "dropDown" \| "slider" | No | | The type of the UI element. |
|
||||
| text | string | No | | The text displayed right next to the UI element. |
|
||||
| topic | string | No | | The topic the UI element is supposed to change and/or receive its status from. |
|
||||
| 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** |
|
||||
| on | string | Yes | "on" |
|
||||
| off | string | Yes | "off" |
|
||||
| toggled | string => boolean | Yes | x => x == "off" |
|
||||
| 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" | 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 | (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** |
|
||||
| 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** |
|
||||
| min | number | Yes | 0 |
|
||||
| max | number | Yes | 1 |
|
||||
| step | number | Yes | 1 |
|
||||
| min | number | Yes | 0 | The minimum value of that slider. |
|
||||
| max | number | Yes | 1 | The maximum value of that slider. |
|
||||
| step | number | Yes | 1 | The smallest step of the slider. |
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
"build": "webpack --bail"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"leaflet": "^1.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"material-ui": "^0.18.7",
|
||||
"mqtt": "^2.11.0",
|
||||
"ramda": "^0.24.1",
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
"babel-cli": "^6.24.1",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"flow": "^0.2.3",
|
||||
"flow-bin": "^0.50.0",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
19
src/map.js
19
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,8 +33,9 @@ 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"})}>
|
||||
<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>
|
||||
|
|
|
|||
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));
|
||||
|
|
@ -3,7 +3,7 @@ declare type Map<K,V> = { [K]: V };
|
|||
declare type Topic = {
|
||||
state: string,
|
||||
command: string,
|
||||
value: any,
|
||||
defaultValue: any,
|
||||
values: Map<string,any>,
|
||||
parseState?: (msg: Object) => any
|
||||
};
|
||||
|
|
@ -14,12 +14,12 @@ declare type ControlUI = {
|
|||
text: string,
|
||||
topic: string,
|
||||
|
||||
enableCondition?: (val: any) => boolean,
|
||||
enableCondition?: (internal: string, actual: any) => boolean,
|
||||
|
||||
// TOGGLE optional properties
|
||||
on?: string, // on override for toggle
|
||||
off?: string, // off override for toggle
|
||||
toggled?: (val: any) => boolean,
|
||||
toggled?: (internal: string, actual: any) => boolean,
|
||||
|
||||
// DROPDOWN optional properties
|
||||
options?: Map<string,any>, //options for dropDown
|
||||
|
|
@ -46,6 +46,17 @@ declare type Config = {
|
|||
|
||||
declare type State = {
|
||||
mqtt: ?any,
|
||||
ui: ?string,
|
||||
values: Map<string,any>
|
||||
uiOpened: ?string,
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue