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

5
README Normal file
View file

@ -0,0 +1,5 @@
# RZL Map
Die Config ist in src/config.js erreichbar.
Doku folgt. Bald™

27
index.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.2.0/dist/leaflet.css" integrity="sha512-M2wvCLH6DSRazYeZRIm1JnYyh22purTM+FDB5CsyxtQJYeKq83arPe5wgbNmcFXGqiSH2XR8dT/fJISVA1r/zQ==" crossorigin=""/>
<meta charset="utf-8" />
<style>
#content {
height: 100%;
height: 100vh;
}
.leaflet-container {
height: calc(100vh - 64px);
max-height: 100%;
}
body {
margin: 0;
}
#drawer_uiComponents {
margin: 10px;
}
</style>
</head>
<body>
<div id="content">
</div>
<script src="public/dist/main.js"></script>
</body>

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "reacttest",
"version": "1.0.0",
"author": "uwap <me@uwap.name>",
"description": "react",
"scripts": {
"build": "webpack --bail"
},
"dependencies": {
"leaflet": "^1.2.0",
"lodash": "^4.17.4",
"material-ui": "^0.18.7",
"mqtt": "^2.11.0",
"ramda": "^0.24.1",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-leaflet": "^1.5.0",
"react-tap-event-plugin": "^2.0.1",
"redux": "^3.7.2"
},
"devDependencies": {
"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",
"webpack": "^3.1.0"
},
"license": "MIT"
}

2
rzl.svg Normal file
View file

@ -0,0 +1,2 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="571px" height="341px" version="1.1" content="&lt;mxfile userAgent=&quot;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36&quot; version=&quot;7.1.6&quot; editor=&quot;www.draw.io&quot;&gt;&lt;diagram id=&quot;99136bbf-b1d7-fc77-4446-920ab02a96c1&quot; name=&quot;Page-1&quot;&gt;3ZdNj5swEIZ/jY8rYRsIHBeatlK1p1Tq2YsdsGJw5DgL6a+vCSZ8RuLASnRzieed8dczYzAAx3n1Q5Fz9iYpEwA5tAL4G0AIutA1f7Vya5RdbdVCqji1QZ1w4H+ZFR2rXjlll0GgllJofh6KiSwKluiBRpSS5TDsKMVw1jNJ2UQ4JERM1T+c6qxRA7Tr9J+Mp1k7M/TDxvNOklOq5LWw8wGEj/df485JO5bd6CUjVJY9Ce8BjpWUumnlVcxEzbbF1vT7/sT7WLdihV7Uweblg4gra5d8X5i+tTDu22F1BwfgqMy4ZoczSWpvadJvtEznwljQNO1wTGlWPV0TfOzUVBCTOdPqZkKqYRG0tWPNskuE12pZLwnYtSKxyU8fI3cATMMyeMID/488kDPDAwZr8PC2x2MM5HGYekQgnKuQNYD42wcyUyEw/Cweu+3zgM4UiDt3YtAaQILtAcH+CAheCASuASTcHhA3HAIJF/JY4xXTrnZTPIY48NyBCWaA+GvwcCc8fgPkCzNFRPmHaaZ1k7SaGa8nz0SqxZF0cSRfHHl5FjlKsUmWHubxopU8sVgKqYxSyMJERkcuxEgigqeFMROTXGb0qE49N5fXV+vIOaX1NLOF8wmnB3reouOzxuMVTS8kv0CMQRQnGftKkFEwhrzsGbUK5OklZ/+yT05fCvACvt46fI3ZfdHdfb3PZrz/Bw==&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(255, 255, 255);"><defs/><g transform="translate(0.5,0.5)"><rect x="0" y="0" width="570" height="340" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="0" y="0" width="200" height="180" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="200" y="150" width="110" height="30" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="200" y="0" width="190" height="30" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="200" y="30" width="40" height="120" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="350" y="60" width="40" height="110" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="480" y="20" width="40" height="140" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="30" y="230" width="480" height="60" fill="#ffffff" stroke="#000000" pointer-events="none"/><g transform="translate(495.5,53.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="8" height="82" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 8px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;">T<div>a</div><div>r</div><div>d</div><div>i</div><div>s</div></div></div></foreignObject><text x="4" y="47" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">[Not supported by viewer]</text></switch></g><g transform="translate(272.5,83.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="34" height="12" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 36px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;">Küche</div></div></foreignObject><text x="17" y="12" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">Küche</text></switch></g><g transform="translate(75.5,83.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="38" height="12" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 40px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;">E-Ecke</div></div></foreignObject><text x="19" y="12" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">E-Ecke</text></switch></g></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

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";

35
types/types.js Normal file
View file

@ -0,0 +1,35 @@
declare type Map<K,V> = { [K]: V };
declare type Topic = {
state: string,
command: string,
value: any,
values: Map<string,any>,
parseState?: (msg: Object) => any
};
declare type Topics = Map<string,Topic>;
declare type ControlUI = {
type: "toggle",
text: string,
topic: string
};
declare type Control = {
name: string,
position: Array<number>,
icon: string,
ui: Array<ControlUI>
};
declare type Controls = Map<string,Control>;
declare type Config = {
topics: Topics,
controls: Controls
};
declare type State = {
mqtt: ?any,
ui: ?string,
values: Map<string,any>
};

25
webpack.config.js Normal file
View file

@ -0,0 +1,25 @@
// webpack.config.js:
var path = require('path');
var webpack = require('webpack');
module.exports = {
resolve: {
extensions: ['.js', '.jsx']
},
entry: [
path.resolve(__dirname, 'src/index.jsx')
],
output: {
path: path.resolve(__dirname, 'public/dist'),
filename: 'main.js',
publicPath: 'dist/'
},
module: {
loaders: [
{ test: /\.css$/, loader: "style!css" },
{ test: /\.js(x)?$/, exclude: /node_modules/, loader: "babel-loader" }
]
},
devtool: 'source-map'
}

3179
yarn.lock Normal file

File diff suppressed because it is too large Load diff