First commit

This commit is contained in:
uwap 2017-09-12 19:17:33 +02:00
commit 9d69521b26
14 changed files with 3636 additions and 0 deletions

20
src/UiItems.js Normal file
View file

@ -0,0 +1,20 @@
// @flow
import React from "react";
import Toggle from "material-ui/Toggle";
import Config from "./config";
import R from "ramda";
const onToggle = (topic, props, state) => (x, toggled) =>
state.mqtt.publish(Config.topics[topic].command,
toggled ? Config.topics[topic].values[R.propOr("on", "on", props)]
: Config.topics[topic].values[R.propOr("off", "off", props)]);
export const toggle = (props: Object) => (
<Toggle label={props.text}
toggled={
props.state.values[props.topic] ===
Config.topics[props.topic].values[R.propOr("on", "on", props)]
}
onToggle={onToggle(props.topic, props, props.state)}
/>
);

37
src/appbar.js Normal file
View file

@ -0,0 +1,37 @@
// @flow
import React from "react";
import AppBar from "material-ui/AppBar";
import CircularProgress from "material-ui/CircularProgress";
import DoneIcon from "material-ui/svg-icons/action/done";
import IconMenu from "material-ui/IconMenu";
import IconButton from "material-ui/IconButton";
import MenuItem from "material-ui/MenuItem";
import { orange400, grey50 } from "material-ui/styles/colors";
const TopBarIndicatorMenu = (props: Object) => (
<IconMenu
iconButtonElement={
<IconButton style={{width:48, height:48, padding: 0}}
iconStyle={{width:48, height: 48}}
tooltip="Connected!">
<DoneIcon color={grey50} />
</IconButton>}
style={{width:48, height:48}}>
<MenuItem primaryText="Reconnect" />
</IconMenu>
);
const TopBarIndicator = (props: Object) => (
props.mqtt == null ? <CircularProgress size={48} color={grey50} />
: <TopBarIndicatorMenu {...props} />
);
const TopBar = (props: Object) => (
<AppBar title={props.title}
style={{background:orange400}}
iconElementLeft={<TopBarIndicator {...props} />}
className="nav"
/>);
export default TopBar;

126
src/config.js Normal file
View file

@ -0,0 +1,126 @@
// @flow
const config : Config = {
topics: {
led_stahltraeger: {
state: "/service/openhab/out/pca301_ledstrips/state",
command: "/service/openhab/in/pca301_ledstrips/command",
value: "OFF", # defaultValue
values: { on: "ON", off: "OFF" }
},
snackbar: {
state: "/service/openhab/out/pca301_snackbar/state",
command: "/service/openhab/in/pca301_snackbar/command",
value: "OFF",
values: { on: "ON", off: "OFF" }
},
twinkle: {
state: "/service/openhab/out/pca301_twinkle/state",
command: "/service/openhab/in/pca301_twinkle/command",
value: "OFF",
values: { on: "ON", off: "OFF" }
},
flyfry: {
state: "/service/openhab/out/wifi_flyfry/state",
command: "/service/openhab/in/wifi_flyfry/command",
value: "OFF",
values: { on: "ON", off: "OFF" }
},
artnet: {
state: "/artnet/state",
command: "/artnet/push",
value: "blackout",
values: { off: "blackout", yellow: "yellow", purple: "purple",
blue: "blue", green: "green", red: "red", random: "random",
cycle: "cycle-random" }
}
},
controls: {
led_stahltrager: {
name: "LED Stahlträger",
position: [360, 80],
icon: "",
ui: [
{
type: "toggle",
text: "Stahlträger LED",
topic: "led_stahltraeger"
},
]
},
snackbar: {
name: "Snackbar",
position: [550, 200],
icon: "",
ui: [
{
type: "toggle",
text: "Snackbar",
topic: "snackbar"
}
]
},
twinkle: {
name: "Twinkle",
position: [500, 280],
icon: "",
ui: [
{
type: "toggle",
text: "Twinkle",
topic: "twinkle"
}
]
},
flyfry: {
name: "Fliegenbratgerät",
position: [450, 320],
icon: "",
ui: [
{
type: "toggle",
text: "Fliegenbratgerät",
topic: "flyfry"
}
]
},
artnet: {
name: "Artnet",
position: [550,150],
icon: "",
ui: [
{
type: "toggle",
text: "Gelb",
topic: "artnet",
on: "yellow"
},
{
type: "toggle",
text: "Rot",
topic: "artnet",
on: "red"
},
{
type: "toggle",
text: "Pink",
topic: "artnet",
on: "purple"
},
{
type: "toggle",
text: "Grün",
topic: "artnet",
on: "green"
},
{
type: "toggle",
text: "Cycle Random",
topic: "artnet",
on: "cycle"
}
]
}
}
};
export default config;

92
src/index.jsx Normal file
View file

@ -0,0 +1,92 @@
// @flow
import React from "react";
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 connectMqtt from "./mqtt";
import AppBar from "./appbar";
import Toggle from "material-ui/Toggle";
import * as UiItems from "./UiItems.js";
import SpaceMap from "./map.js";
import R from "ramda";
import Config from "./config";
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 UiItem = (state) => (props) =>
UiItems[props.type](R.merge(props, {state:state}));
const renderUi = (state, key) => key != null && Config.controls[key] != null ?
R.map(UiItem(state), Config.controls[key].ui) : null;
const App = (state: State) => (
<div>
<MuiThemeProvider>
<div>
<AppBar title="RZL Map" {...state} />
<Drawer open={state.ui != null} openSecondary={true} disableSwipeToOpen={true}>
<Toolbar>
<ToolbarGroup firstChild={true}>
<ToolbarTitle text={
state.ui == null ? "" : Config.controls[state.ui].name}
style={{"marginLeft": 10}} />
</ToolbarGroup>
</Toolbar>
<div id="drawer_uiComponents">
{renderUi(state, state.ui)}
</div>
</Drawer>
</div>
</MuiThemeProvider>
<SpaceMap width={640} height={400} image="rzl.svg" zoom={0.1}
store={store} state={state} />
</div>
);
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);

29
src/map.js Normal file
View file

@ -0,0 +1,29 @@
// @flow
import React from "react";
import { Map, ImageOverlay, Marker, Popup } from "react-leaflet";
import Leaflet from "leaflet";
import R from "ramda";
import Config from "./config";
// convert width/height coordinates to -height/width coordinates
const c = (p) => [-p[1], p[0]]
const Markers = (props) => R.values(R.mapObjIndexed((el,key) => (
<Marker position={c(el.position)} key={el.name}>
<Popup onOpen={() => props.store.dispatch({type: "uiopen", ui: key})}
onClose={() => props.store.dispatch({type: "uiclose"})}>
<span>{el.name}</span>
</Popup>
</Marker>
), R.propOr({}, "controls", Config)));
const SpaceMap = (props: Object) => (
<Map center={c([props.width / 2, props.height / 2])} zoom={props.zoom}
crs={Leaflet.CRS.Simple}>
<ImageOverlay url={props.image}
bounds={[c([0,0]), c([props.width, props.height])]} />
{Markers(props)}
</Map>
);
export default SpaceMap;

22
src/mqtt.js Normal file
View file

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

5
src/stateActions.js Normal file
View file

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