Merge remote-tracking branch 'upstream/master' into patch-13
This commit is contained in:
commit
b868156deb
24 changed files with 1145 additions and 1940 deletions
|
|
@ -8,9 +8,9 @@
|
||||||
1. run `yarn` to install all dependencies.
|
1. run `yarn` to install all dependencies.
|
||||||
2. run `yarn watch CONFIG` to run a local build server that automatically builds
|
2. run `yarn watch CONFIG` to run a local build server that automatically builds
|
||||||
your the mqtt control map for the given CONFIG everytime something changes.
|
your the mqtt control map for the given CONFIG everytime something changes.
|
||||||
3. run `yarn build CONFIG` to create just a single build of the mqtt control map
|
3. run `yarn dev CONFIG` to create just a single build of the mqtt control map
|
||||||
for the given config.
|
for the given config.
|
||||||
4. run `yarn production-build CONFIG` to generate all files for production use.
|
4. run `yarn build CONFIG` to generate all files for production use.
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Config } from "config/flowtypes";
|
import type { Config } from "config/flowtypes";
|
||||||
import { hex, rgb, rgba, rainbow } from "config/colors";
|
import { hex, rgb, rgba, rainbow } from "config/colors";
|
||||||
|
import * as types from "config/types";
|
||||||
|
import { mdi } from "config/icon";
|
||||||
import { esper_topics, esper_statistics } from "./utils";
|
import { esper_topics, esper_statistics } from "./utils";
|
||||||
|
|
||||||
const config : Config = {
|
const config : Config = {
|
||||||
|
|
@ -12,16 +14,18 @@ const config : Config = {
|
||||||
topics: [
|
topics: [
|
||||||
{
|
{
|
||||||
hauptraum_table_light: {
|
hauptraum_table_light: {
|
||||||
command: "/public/sensoren/TPH/leinwand/control",
|
command: {
|
||||||
state: "test",
|
name: "/public/sensoren/TPH/leinwand/control",
|
||||||
defaultValue: "A1 ON",
|
type: types.option({ "A1 ON": "on", "A1 OFF": "off" })
|
||||||
values: { on: "A1 ON", off: "A1 OFF" }
|
},
|
||||||
|
defaultValue: "off"
|
||||||
},
|
},
|
||||||
hauptraum_table_light_on_hack: {
|
hauptraum_table_light_on_hack: {
|
||||||
command: "/public/sensoren/TPH/leinwand/control",
|
command: {
|
||||||
state: "test",
|
name: "/public/sensoren/TPH/leinwand/control",
|
||||||
defaultValue: "A1 OFF",
|
type: types.option({ "A1 ON": "on", "A1 OFF": "off" })
|
||||||
values: { on: "A1 ON", off: "A1 OFF" }
|
},
|
||||||
|
defaultValue: "on"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -29,20 +33,20 @@ const config : Config = {
|
||||||
hauptraum_table_light: {
|
hauptraum_table_light: {
|
||||||
name: "Hauptraum Tisch",
|
name: "Hauptraum Tisch",
|
||||||
position: [450, 450],
|
position: [450, 450],
|
||||||
icon: "white-balance-iridescent",
|
icon: mdi("white-balance-iridescent"),
|
||||||
iconColor: () => hex("#000000"),
|
iconColor: () => hex("#000000"),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Licht",
|
text: "Licht",
|
||||||
topic: "hauptraum_table_light",
|
topic: "hauptraum_table_light",
|
||||||
icon: "power"
|
icon: mdi("power")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Licht",
|
text: "Licht",
|
||||||
topic: "hauptraum_table_light_on_hack",
|
topic: "hauptraum_table_light_on_hack",
|
||||||
icon: "power"
|
icon: mdi("power")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
725
config/rzl.js
725
config/rzl.js
File diff suppressed because it is too large
Load diff
109
config/utils.js
109
config/utils.js
|
|
@ -1,42 +1,44 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { ControlUI } from "config/flowtypes";
|
import type { ControlUI } from "config/flowtypes";
|
||||||
|
import { mdi } from "config/icon";
|
||||||
|
import * as types from "config/types";
|
||||||
|
|
||||||
export const esper_topics = (chip_id: string, name: string) => ({
|
export const esper_topics = (chip_id: string, name: string) => ({
|
||||||
[ `esper_${name}_version` ]: {
|
[ `esper_${name}_version` ]: {
|
||||||
state: `/service/esper/${chip_id}/info`,
|
state: {
|
||||||
command: "",
|
name: `/service/esper/${chip_id}/info`,
|
||||||
defaultValue: "UNKNOWN",
|
type: types.json("version.esper")
|
||||||
values: {},
|
},
|
||||||
type: msg => JSON.parse(msg.toString()).version.esper
|
defaultValue: "UNKNOWN"
|
||||||
},
|
},
|
||||||
[ `esper_${name}_ip` ]: {
|
[ `esper_${name}_ip` ]: {
|
||||||
state: `/service/esper/${chip_id}/info`,
|
state: {
|
||||||
command: "",
|
name: `/service/esper/${chip_id}/info`,
|
||||||
defaultValue: "UNKNOWN",
|
type: types.json("network.ip")
|
||||||
values: {},
|
},
|
||||||
type: msg => JSON.parse(msg.toString()).network.ip
|
defaultValue: "UNKNOWN"
|
||||||
},
|
},
|
||||||
[ `esper_${name}_rssi` ]: {
|
[ `esper_${name}_rssi` ]: {
|
||||||
state: `/service/esper/${chip_id}/info`,
|
state: {
|
||||||
command: "",
|
name: `/service/esper/${chip_id}/info`,
|
||||||
defaultValue: "UNKNOWN",
|
type: types.json("wifi.rssi")
|
||||||
values: {},
|
},
|
||||||
type: msg => JSON.parse(msg.toString()).wifi.rssi
|
defaultValue: "UNKNOWN"
|
||||||
},
|
},
|
||||||
[ `esper_${name}_uptime` ]: {
|
[ `esper_${name}_uptime` ]: {
|
||||||
state: `/service/esper/${chip_id}/info`,
|
state: {
|
||||||
command: "",
|
name: `/service/esper/${chip_id}/info`,
|
||||||
defaultValue: "UNKNOWN",
|
|
||||||
values: {},
|
|
||||||
type: msg => new Date(JSON.parse(msg.toString()).time.startup * 1000)
|
type: msg => new Date(JSON.parse(msg.toString()).time.startup * 1000)
|
||||||
.toLocaleString()
|
.toLocaleString()
|
||||||
},
|
},
|
||||||
[ `esper_${name}_device` ]: {
|
|
||||||
state: `/service/esper/${chip_id}/info`,
|
|
||||||
command: "",
|
|
||||||
defaultValue: "UNKNOWN",
|
defaultValue: "UNKNOWN",
|
||||||
values: {},
|
},
|
||||||
type: msg => JSON.parse(msg.toString()).device
|
[ `esper_${name}_device` ]: {
|
||||||
|
state: {
|
||||||
|
name: `/service/esper/${chip_id}/info`,
|
||||||
|
type: types.json("device")
|
||||||
|
},
|
||||||
|
defaultValue: "UNKNOWN"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -45,22 +47,53 @@ export const floalt = {
|
||||||
brightness: (light_id: string) => `floalt_${light_id}_brightness`,
|
brightness: (light_id: string) => `floalt_${light_id}_brightness`,
|
||||||
topics: (light_id: string) => ({
|
topics: (light_id: string) => ({
|
||||||
[ `floalt_${light_id}_color` ]: {
|
[ `floalt_${light_id}_color` ]: {
|
||||||
state: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/state`,
|
state: {
|
||||||
command: `/service/openhab/in/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/command`,
|
name: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/state`,
|
||||||
defaultValue: "0",
|
type: types.string
|
||||||
values: {}
|
},
|
||||||
|
command: {
|
||||||
|
name: `/service/openhab/in/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/command`,
|
||||||
|
type: types.string
|
||||||
|
},
|
||||||
|
defaultValue: "0"
|
||||||
},
|
},
|
||||||
[ `floalt_${light_id}_brightness` ]: {
|
[ `floalt_${light_id}_brightness` ]: {
|
||||||
state: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_brightness/state`,
|
state: {
|
||||||
command: `/service/openhab/in/tradfri_0220_gwb8d7af2b448f_${light_id}_brightness/command`,
|
name: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_brightness/state`,
|
||||||
defaultValue: "0",
|
type: types.string
|
||||||
values: {}
|
},
|
||||||
|
command: {
|
||||||
|
name: `/service/openhab/in/tradfri_0220_gwb8d7af2b448f_${light_id}_brightness/command`,
|
||||||
|
type: types.string
|
||||||
|
},
|
||||||
|
defaultValue: "0"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tradfri_remote = {
|
||||||
|
level: (remote_id: string) => `tradfri_remote_${remote_id}_level`,
|
||||||
|
low: (remote_id: string) => `tradfri_remote_${remote_id}_low`,
|
||||||
|
topics: (remote_id: string) => ({
|
||||||
|
[ `tradfri_remote_${remote_id}_level` ]: {
|
||||||
|
state: {
|
||||||
|
name: `/service/openhab/out/tradfri_0830_gwb8d7af2b448f_${remote_id}_battery_level/state`,
|
||||||
|
type: types.string
|
||||||
|
},
|
||||||
|
defaultValue: "0"
|
||||||
|
},
|
||||||
|
[ `tradfri_remote_${remote_id}_low` ]: {
|
||||||
|
state: {
|
||||||
|
name: `/service/openhab/out/tradfri_0830_gwb8d7af2b448f_${remote_id}_battery_low/state`,
|
||||||
|
type: types.option({ ON: "true", OFF: "false" })
|
||||||
|
},
|
||||||
|
defaultValue: "false",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const esper_statistics = (name: string,
|
export const esper_statistics = (name: string,
|
||||||
prev_ui: Array<ControlUI> = []) => (
|
prev_ui: Array<ControlUI> = []): Array<ControlUI> => (
|
||||||
prev_ui.concat([
|
prev_ui.concat([
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
|
|
@ -69,31 +102,31 @@ export const esper_statistics = (name: string,
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Device Variant",
|
text: "Device Variant",
|
||||||
icon: "chart-donut",
|
icon: mdi("chart-donut"),
|
||||||
topic: `esper_${name}_device`
|
topic: `esper_${name}_device`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Version",
|
text: "Version",
|
||||||
icon: "source-branch",
|
icon: mdi("source-branch"),
|
||||||
topic: `esper_${name}_version`
|
topic: `esper_${name}_version`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "IP",
|
text: "IP",
|
||||||
icon: "access-point-network",
|
icon: mdi("access-point-network"),
|
||||||
topic: `esper_${name}_ip`
|
topic: `esper_${name}_ip`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "RSSI",
|
text: "RSSI",
|
||||||
icon: "wifi",
|
icon: mdi("wifi"),
|
||||||
topic: `esper_${name}_rssi`
|
topic: `esper_${name}_rssi`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Running since…",
|
text: "Running since…",
|
||||||
icon: "av-timer",
|
icon: mdi("av-timer"),
|
||||||
topic: `esper_${name}_uptime`
|
topic: `esper_${name}_uptime`
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
background: rgba(239,239,203,.59);
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
25
package.json
25
package.json
|
|
@ -4,20 +4,19 @@
|
||||||
"author": "uwap <me+mqttmap.package.json@uwap.name>",
|
"author": "uwap <me+mqttmap.package.json@uwap.name>",
|
||||||
"description": "control devices via mqtt on a beautiful map of your space",
|
"description": "control devices via mqtt on a beautiful map of your space",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --bail --config webpack.dev.js --env",
|
"build": "webpack --bail --config webpack.config.js -p --env",
|
||||||
"production-build": "webpack --bail --config webpack.prod.js --env",
|
"dev": "webpack --bail --config webpack.config.js --mode development --env",
|
||||||
"watch": "webpack-dev-server --open --config webpack.dev.js --env",
|
"watch": "webpack-dev-server --open --config webpack.config.js --mode development --env",
|
||||||
"travis": "./travis.sh",
|
"travis": "./travis.sh",
|
||||||
"lint": "eslint -- --ext js --ext jsx src/",
|
"lint": "eslint --ext js --ext jsx src/",
|
||||||
"precommit": "yarn lint"
|
"precommit": "yarn lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-preset-env": "^1.6.0",
|
"@material-ui/core": "^1.2.1",
|
||||||
|
"@material-ui/lab": "^1.0.0-alpha.5",
|
||||||
|
"@mdi/font": "^2.0.46",
|
||||||
"leaflet": "^1.3.1",
|
"leaflet": "^1.3.1",
|
||||||
"lodash-es": "^4.17.4",
|
"lodash-es": "^4.17.4",
|
||||||
"material-ui": "npm:material-ui@next",
|
|
||||||
"material-ui-old": "npm:material-ui@latest",
|
|
||||||
"mdi": "^2.0.46",
|
|
||||||
"mqtt": "^2.14.0",
|
"mqtt": "^2.14.0",
|
||||||
"react": "^16.0.0",
|
"react": "^16.0.0",
|
||||||
"react-dom": "^16.0.0",
|
"react-dom": "^16.0.0",
|
||||||
|
|
@ -31,25 +30,23 @@
|
||||||
"babel-eslint": "^8.0.1",
|
"babel-eslint": "^8.0.1",
|
||||||
"babel-loader": "^7.1.1",
|
"babel-loader": "^7.1.1",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
|
"babel-preset-env": "^1.6.0",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
"clean-webpack-plugin": "^0.1.18",
|
"clean-webpack-plugin": "^0.1.18",
|
||||||
"css-loader": "^0.28.9",
|
"css-loader": "^0.28.9",
|
||||||
"eslint": "^4.16.0",
|
"eslint": "^5.0.1",
|
||||||
"eslint-plugin-flowtype": "^2.42.0",
|
"eslint-plugin-flowtype": "^2.42.0",
|
||||||
"eslint-plugin-react": "^7.6.1",
|
"eslint-plugin-react": "^7.6.1",
|
||||||
"extract-text-webpack-plugin": "next",
|
|
||||||
"file-loader": "^1.1.5",
|
"file-loader": "^1.1.5",
|
||||||
"flow": "^0.2.3",
|
"flow": "^0.2.3",
|
||||||
"flow-bin": "^0.69.0",
|
"flow-bin": "^0.75.0",
|
||||||
"flow-typed": "^2.3.0",
|
"flow-typed": "^2.3.0",
|
||||||
"html-webpack-plugin": "^3.1.0",
|
"html-webpack-plugin": "^3.1.0",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
"lodash-webpack-plugin": "^0.11.4",
|
|
||||||
"style-loader": "^0.21.0",
|
"style-loader": "^0.21.0",
|
||||||
"webpack": "^4.3.0",
|
"webpack": "^4.3.0",
|
||||||
"webpack-cli": "^2.0.13",
|
"webpack-cli": "^3.0.0",
|
||||||
"webpack-dev-server": "^3.1.1",
|
"webpack-dev-server": "^3.1.1",
|
||||||
"webpack-merge": "^4.1.1",
|
|
||||||
"webpack-shell-plugin": "^0.5.0"
|
"webpack-shell-plugin": "^0.5.0"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,16 @@ import merge from "lodash/merge";
|
||||||
|
|
||||||
import type { Config, Control, Topics } from "config/flowtypes";
|
import type { Config, Control, Topics } from "config/flowtypes";
|
||||||
|
|
||||||
import MuiThemeProvider from "material-ui/styles/MuiThemeProvider";
|
import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider";
|
||||||
import createMuiTheme from "material-ui/styles/createMuiTheme";
|
import createMuiTheme from "@material-ui/core/styles/createMuiTheme";
|
||||||
import withStyles from "material-ui/styles/withStyles";
|
import withStyles from "@material-ui/core/styles/withStyles";
|
||||||
import * as Colors from "material-ui/colors";
|
import * as Colors from "@material-ui/core/colors";
|
||||||
|
|
||||||
import SideBar from "components/SideBar";
|
import SideBar from "components/SideBar";
|
||||||
import ControlMap from "components/ControlMap";
|
import ControlMap from "components/ControlMap";
|
||||||
import TopBar from "components/TopBar";
|
import TopBar from "components/TopBar";
|
||||||
import UiItemList from "components/UiItemList";
|
import UiItemList from "components/UiItemList";
|
||||||
|
|
||||||
import keyOf from "utils/keyOf";
|
|
||||||
import { controlGetIcon } from "utils/parseIconName";
|
|
||||||
|
|
||||||
import connectMqtt from "../connectMqtt";
|
import connectMqtt from "../connectMqtt";
|
||||||
|
|
||||||
export type AppProps = {
|
export type AppProps = {
|
||||||
|
|
@ -31,7 +28,7 @@ export type AppState = {
|
||||||
selectedControl: ?Control,
|
selectedControl: ?Control,
|
||||||
drawerOpened: boolean,
|
drawerOpened: boolean,
|
||||||
mqttState: State,
|
mqttState: State,
|
||||||
mqttSend: (topic: string, value: Actual) => void,
|
mqttSend: (topic: string, value: Buffer) => void,
|
||||||
mqttConnected: boolean,
|
mqttConnected: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -41,16 +38,15 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedControl: null,
|
selectedControl: null,
|
||||||
drawerOpened: false,
|
drawerOpened: false,
|
||||||
mqttState: mapValues(this.topics, (topic) => ({
|
mqttState: mapValues(this.topics, (topic) => topic.defaultValue),
|
||||||
actual: topic.defaultValue,
|
|
||||||
internal: keyOf(topic.values, topic.defaultValue)
|
|
||||||
})),
|
|
||||||
mqttSend: connectMqtt(props.config.space.mqtt, {
|
mqttSend: connectMqtt(props.config.space.mqtt, {
|
||||||
onMessage: this.receiveMessage.bind(this),
|
onMessage: this.receiveMessage.bind(this),
|
||||||
onConnect: () => this.setState({ mqttConnected: true }),
|
onConnect: () => this.setState({ mqttConnected: true }),
|
||||||
onReconnect: () => this.setState({ mqttConnected: false }),
|
onReconnect: () => this.setState({ mqttConnected: false }),
|
||||||
onDisconnect: () => this.setState({ mqttConnected: false }),
|
onDisconnect: () => this.setState({ mqttConnected: false }),
|
||||||
subscribe: map(this.topics, (x) => x.state)
|
subscribe: map(
|
||||||
|
filter(keys(this.topics), (x) => this.topics[x].state != null),
|
||||||
|
(x) => this.topics[x].state.name)
|
||||||
}),
|
}),
|
||||||
mqttConnected: false
|
mqttConnected: false
|
||||||
};
|
};
|
||||||
|
|
@ -77,23 +73,23 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveMessage(rawTopic: string, message: Object) {
|
receiveMessage(rawTopic: string, message: Buffer) {
|
||||||
const topics = filter(
|
const topics = filter(
|
||||||
keys(this.topics),
|
keys(this.topics),
|
||||||
(k) => this.topics[k].state === rawTopic
|
(k) => this.topics[k].state != null &&
|
||||||
|
this.topics[k].state.name === rawTopic
|
||||||
);
|
);
|
||||||
if (topics.length === 0) {
|
if (topics.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let i in topics) {
|
for (let i in topics) {
|
||||||
|
// TODO: Remove FlowFixMe
|
||||||
const topic = topics[i];
|
const topic = topics[i];
|
||||||
const parseValue = this.topics[topic].type;
|
// $FlowFixMe
|
||||||
|
const parseValue = this.topics[topic].state.type;
|
||||||
const val = parseValue == null ? message.toString() : parseValue(message);
|
const val = parseValue == null ? message.toString() : parseValue(message);
|
||||||
this.setState({mqttState: Object.assign({}, merge(this.state.mqttState,
|
this.setState({mqttState: Object.assign({}, merge(this.state.mqttState,
|
||||||
{ [topic]: {
|
{ [topic]: val}))});
|
||||||
actual: val,
|
|
||||||
internal: keyOf(this.topics[topic].values, val) || val
|
|
||||||
}}))});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,15 +101,15 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
this.setState({drawerOpened: false});
|
this.setState({drawerOpened: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeState(topic: string, value: Actual) {
|
changeState(topic: string, value: string) {
|
||||||
const rawTopic = this.topics[topic].command;
|
if (this.topics[topic].command == null) {
|
||||||
if (rawTopic == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.state.mqttSend(
|
const rawTopic = this.topics[topic].command.name;
|
||||||
rawTopic,
|
const transformValue = this.topics[topic].command.type;
|
||||||
String(this.topics[topic].values[value] || value)
|
const val =
|
||||||
);
|
transformValue == null ? value : transformValue(Buffer.from(value));
|
||||||
|
this.state.mqttSend(rawTopic, Buffer.from(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -127,8 +123,7 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
control={this.state.selectedControl}
|
control={this.state.selectedControl}
|
||||||
onCloseRequest={this.closeDrawer.bind(this)}
|
onCloseRequest={this.closeDrawer.bind(this)}
|
||||||
icon={this.state.selectedControl == null ? null :
|
icon={this.state.selectedControl == null ? null :
|
||||||
controlGetIcon(this.state.selectedControl,
|
this.state.selectedControl.icon(this.state.mqttState)}
|
||||||
this.state.mqttState)}
|
|
||||||
>
|
>
|
||||||
{this.state.selectedControl == null
|
{this.state.selectedControl == null
|
||||||
|| <UiItemList state={this.state.mqttState}
|
|| <UiItemList state={this.state.mqttState}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ import React from "react";
|
||||||
import { Map, ImageOverlay, Marker, LayersControl } from "react-leaflet";
|
import { Map, ImageOverlay, Marker, LayersControl } from "react-leaflet";
|
||||||
import { CRS, point, divIcon } from "leaflet";
|
import { CRS, point, divIcon } from "leaflet";
|
||||||
import map from "lodash/map";
|
import map from "lodash/map";
|
||||||
import mapValues from "lodash/mapValues";
|
|
||||||
import parseIconName, { controlGetIcon } from "utils/parseIconName";
|
|
||||||
|
|
||||||
import type { Controls, Control } from "config/flowtypes";
|
import type { Controls, Control } from "config/flowtypes";
|
||||||
|
|
||||||
|
|
@ -50,8 +48,8 @@ export default class ControlMap extends React.PureComponent<ControlMapProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
createLeafletIcon(control: Control) {
|
createLeafletIcon(control: Control) {
|
||||||
const icon = controlGetIcon(control, this.props.state);
|
const icon = control.icon(this.props.state);
|
||||||
const iconClass = parseIconName(`${icon} 36px`);
|
const iconClass = `${icon} mdi-36px`;
|
||||||
return divIcon({
|
return divIcon({
|
||||||
iconSize: point(36, 36),
|
iconSize: point(36, 36),
|
||||||
iconAnchor: point(18, 18),
|
iconAnchor: point(18, 18),
|
||||||
|
|
@ -61,10 +59,8 @@ export default class ControlMap extends React.PureComponent<ControlMapProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
iconColor(control: Control): string {
|
iconColor(control: Control): string {
|
||||||
const ints = mapValues(this.props.state, (x) => x.internal || x.actual);
|
|
||||||
const acts = mapValues(this.props.state, (x) => x.actual);
|
|
||||||
if (control.iconColor != null) {
|
if (control.iconColor != null) {
|
||||||
return control.iconColor(ints, acts, this.props.state);
|
return control.iconColor(this.props.state);
|
||||||
}
|
}
|
||||||
return "#000";
|
return "#000";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,33 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import withStyles from "material-ui/styles/withStyles";
|
import withStyles from "@material-ui/core/styles/withStyles";
|
||||||
import Drawer from "material-ui/Drawer";
|
import Drawer from "@material-ui/core/Drawer";
|
||||||
import Typography from "material-ui/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import IconButton from "material-ui/IconButton";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import AppBar from "material-ui/AppBar";
|
import AppBar from "@material-ui/core/AppBar";
|
||||||
import Toolbar from "material-ui/Toolbar";
|
import Toolbar from "@material-ui/core/Toolbar";
|
||||||
import List from "material-ui/List";
|
import List from "@material-ui/core/List";
|
||||||
import { renderIcon } from "utils/parseIconName";
|
import { renderIcon } from "config/icon";
|
||||||
|
|
||||||
|
import type { RawIcon } from "config/icon";
|
||||||
import type { Control } from "config/flowtypes";
|
import type { Control } from "config/flowtypes";
|
||||||
|
|
||||||
export type SideBarProps = {
|
export type SideBarProps = {
|
||||||
control: ?Control,
|
control: ?Control,
|
||||||
open: boolean,
|
open: boolean,
|
||||||
onCloseRequest: () => void,
|
onCloseRequest: () => void,
|
||||||
icon?: ?string,
|
icon?: ?RawIcon,
|
||||||
children?: React.Node
|
children?: React.Node
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SideBarState = {
|
export type SideBarState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
class SideBar extends React.PureComponent<SideBarProps & Classes, SideBarState>
|
type Props = SideBarProps & Classes;
|
||||||
{
|
|
||||||
constructor(props: SideBarProps & Classes) {
|
class SideBar extends React.PureComponent<Props, SideBarState> {
|
||||||
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import AppBar from "material-ui/AppBar";
|
import AppBar from "@material-ui/core/AppBar";
|
||||||
import Toolbar from "material-ui/Toolbar";
|
import Toolbar from "@material-ui/core/Toolbar";
|
||||||
import Typography from "material-ui/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import { CircularProgress } from "material-ui/Progress";
|
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||||
|
|
||||||
export type TopBarProps = {
|
export type TopBarProps = {
|
||||||
title: string,
|
title: string,
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,30 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import keys from "lodash/keys";
|
import keys from "lodash/keys";
|
||||||
import map from "lodash/map";
|
import map from "lodash/map";
|
||||||
import {
|
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
|
||||||
ListItemSecondaryAction,
|
import ListItemText from "@material-ui/core/ListItemText";
|
||||||
ListItemText,
|
import ListSubheader from "@material-ui/core/ListSubheader";
|
||||||
ListSubheader
|
import Switch from "@material-ui/core/Switch";
|
||||||
} from "material-ui/List";
|
import Input from "@material-ui/core/Input";
|
||||||
import Switch from "material-ui/Switch";
|
import InputLabel from "@material-ui/core/InputLabel";
|
||||||
import Input, { InputLabel } from "material-ui/Input";
|
import FormControl from "@material-ui/core/FormControl";
|
||||||
import { FormControl } from "material-ui/Form";
|
import Select from "@material-ui/core/Select";
|
||||||
import Select from "material-ui/Select";
|
import MenuItem from "@material-ui/core/MenuItem";
|
||||||
import { MenuItem } from "material-ui/Menu";
|
import Button from "@material-ui/core/Button";
|
||||||
import Button from "material-ui/Button";
|
import LinearProgress from "@material-ui/core/LinearProgress";
|
||||||
import { LinearProgress } from "material-ui/Progress";
|
import SliderComponent from "@material-ui/lab/Slider";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
UIControl, UIToggle, UIDropDown, UILink,
|
UIControl, UIToggle, UIDropDown, UILink,
|
||||||
UISection, UIText, UIProgress
|
UISection, UIText, UIProgress, UISlider
|
||||||
} from "config/flowtypes";
|
} from "config/flowtypes";
|
||||||
|
|
||||||
import keyOf from "utils/keyOf";
|
import keyOf from "utils/keyOf";
|
||||||
import { getInternals, getActuals } from "utils/state";
|
|
||||||
|
|
||||||
type UiItemProps<I> = {
|
type UiItemProps<I> = {
|
||||||
item: I,
|
item: I,
|
||||||
state: State,
|
state: State,
|
||||||
onChangeState: (topic: string, nextState: Actual) => void
|
onChangeState: (topic: string, nextState: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line flowtype/no-weak-types
|
// eslint-disable-next-line flowtype/no-weak-types
|
||||||
|
|
@ -54,9 +53,7 @@ export default class UiItem<I:Object>
|
||||||
typeof this.props.item.enableCondition == "function") {
|
typeof this.props.item.enableCondition == "function") {
|
||||||
const enableCondition = this.props.item.enableCondition;
|
const enableCondition = this.props.item.enableCondition;
|
||||||
const state = this.props.state;
|
const state = this.props.state;
|
||||||
const internals = getInternals(state);
|
return enableCondition(state);
|
||||||
const actuals = getActuals(state);
|
|
||||||
return enableCondition(internals, actuals, state);
|
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +65,7 @@ export class UiControl<I: UIControl> extends UiItem<I> {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeState(next: Actual) {
|
changeState(next: string) {
|
||||||
if (this.props.item.topic == null) {
|
if (this.props.item.topic == null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing topic in ${this.props.item.type} "${this.props.item.text}"`
|
`Missing topic in ${this.props.item.type} "${this.props.item.text}"`
|
||||||
|
|
@ -93,19 +90,6 @@ export class UiControl<I: UIControl> extends UiItem<I> {
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
if (Object.keys(this.props.item).includes("enableCondition") &&
|
|
||||||
// $FlowFixMe
|
|
||||||
typeof this.props.item.enableCondition == "function") {
|
|
||||||
const enableCondition = this.props.item.enableCondition;
|
|
||||||
const value = this.getValue();
|
|
||||||
return enableCondition(
|
|
||||||
value.internal || value.actual, value.actual, this.props.state);
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Toggle extends UiControl<UIToggle> {
|
export class Toggle extends UiControl<UIToggle> {
|
||||||
|
|
@ -113,9 +97,8 @@ export class Toggle extends UiControl<UIToggle> {
|
||||||
const value = this.getValue();
|
const value = this.getValue();
|
||||||
const control = this.props.item;
|
const control = this.props.item;
|
||||||
const isChecked = control.toggled ||
|
const isChecked = control.toggled ||
|
||||||
((i, _a, _s) => i === (control.on || "on"));
|
((i, _s) => i === (control.on || "on"));
|
||||||
const checked = isChecked(
|
const checked = isChecked(value, this.props.state);
|
||||||
value.internal || value.actual, value.actual, this.props.state);
|
|
||||||
return checked;
|
return checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +128,7 @@ export class Toggle extends UiControl<UIToggle> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DropDown extends UiControl<UIDropDown> {
|
export class DropDown extends UiControl<UIDropDown> {
|
||||||
runPrimaryAction = (next?: Actual) => {
|
runPrimaryAction = (next?: string) => {
|
||||||
if (this.isEnabled()) {
|
if (this.isEnabled()) {
|
||||||
const control = this.props.item;
|
const control = this.props.item;
|
||||||
const optionKeys = keys(control.options);
|
const optionKeys = keys(control.options);
|
||||||
|
|
@ -172,7 +155,7 @@ export class DropDown extends UiControl<UIDropDown> {
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<InputLabel htmlFor={id}>{control.text}</InputLabel>
|
<InputLabel htmlFor={id}>{control.text}</InputLabel>
|
||||||
<Select value={value.internal || value.actual}
|
<Select value={value}
|
||||||
onChange={(event) => this.runPrimaryAction(event.target.value)}
|
onChange={(event) => this.runPrimaryAction(event.target.value)}
|
||||||
disabled={!this.isEnabled()}
|
disabled={!this.isEnabled()}
|
||||||
input={<Input id={id} />}
|
input={<Input id={id} />}
|
||||||
|
|
@ -184,6 +167,28 @@ export class DropDown extends UiControl<UIDropDown> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Slider extends UiControl<UISlider> {
|
||||||
|
runPrimaryAction = (e: ?Event, v: ?number) => {
|
||||||
|
if (v != null) {
|
||||||
|
this.changeState(v.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<ListItemText key="label" primary={this.props.item.text} />,
|
||||||
|
<SliderComponent key="slidercomponent"
|
||||||
|
value={parseFloat(this.getValue())}
|
||||||
|
min={this.props.item.min || 0} max={this.props.item.max || 0}
|
||||||
|
step={this.props.item.step || 1}
|
||||||
|
onChange={(e, v) =>
|
||||||
|
this.props.item.delayedApply || this.runPrimaryAction(e, v)}
|
||||||
|
onDragEnd={this.runPrimaryAction}
|
||||||
|
disabled={!this.isEnabled()} />
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Link extends UiItem<UILink> {
|
export class Link extends UiItem<UILink> {
|
||||||
runPrimaryAction = () => {
|
runPrimaryAction = () => {
|
||||||
const control = this.props.item;
|
const control = this.props.item;
|
||||||
|
|
@ -223,7 +228,7 @@ export class Text extends UiControl<UIText> {
|
||||||
render() {
|
render() {
|
||||||
return [
|
return [
|
||||||
<ListItemText key="label" secondary={this.props.item.text} />,
|
<ListItemText key="label" secondary={this.props.item.text} />,
|
||||||
<ListItemText key="vr" primary={this.getValue().internal} align="right" />
|
<ListItemText key="vr" primary={this.getValue()} align="right" />
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +237,7 @@ export class Progress extends UiControl<UIProgress> {
|
||||||
render() {
|
render() {
|
||||||
const min = this.props.item.min || 0;
|
const min = this.props.item.min || 0;
|
||||||
const max = this.props.item.max || 100;
|
const max = this.props.item.max || 100;
|
||||||
const val = parseFloat(this.getValue().internal || this.getValue().actual);
|
const val = parseFloat(this.getValue());
|
||||||
const value = val * 100 / max - min;
|
const value = val * 100 / max - min;
|
||||||
return [
|
return [
|
||||||
<ListItemText key="label" secondary={this.props.item.text} />,
|
<ListItemText key="label" secondary={this.props.item.text} />,
|
||||||
|
|
@ -242,3 +247,4 @@ export class Progress extends UiControl<UIProgress> {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,18 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import ListItem from "@material-ui/core/ListItem";
|
||||||
ListItem,
|
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||||
ListItemIcon,
|
import { renderIcon } from "config/icon";
|
||||||
ListItemSecondaryAction,
|
|
||||||
ListItemText
|
|
||||||
} from "material-ui/List";
|
|
||||||
import { renderIcon } from "utils/parseIconName";
|
|
||||||
|
|
||||||
import type { ControlUI, UIControl, UISlider } from "config/flowtypes";
|
import type { ControlUI } from "config/flowtypes";
|
||||||
|
|
||||||
// TODO: Use something else
|
import { Toggle, DropDown, Link,
|
||||||
import Slider from "material-ui-old/Slider";
|
Section, Text, Progress, Slider } from "./UiItem";
|
||||||
import MuiThemeProvider from "material-ui-old/styles/MuiThemeProvider";
|
|
||||||
|
|
||||||
import { Toggle, DropDown, Link, Section, Text, Progress } from "./UiItem";
|
|
||||||
|
|
||||||
export type UiItemListProps = {
|
export type UiItemListProps = {
|
||||||
controls: Array<ControlUI>,
|
controls: Array<ControlUI>,
|
||||||
state: State,
|
state: State,
|
||||||
onChangeState: (topic: string, nextState: Actual) => void
|
onChangeState: (topic: string, nextState: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class UiItemList extends React.PureComponent<UiItemListProps> {
|
export default class UiItemList extends React.PureComponent<UiItemListProps> {
|
||||||
|
|
@ -40,7 +33,9 @@ export default class UiItemList extends React.PureComponent<UiItemListProps> {
|
||||||
return (
|
return (
|
||||||
<ListItem key={key}>
|
<ListItem key={key}>
|
||||||
{control.icon == null ||
|
{control.icon == null ||
|
||||||
<ListItemIcon>{renderIcon(control.icon, "mdi-24px")}</ListItemIcon>}
|
<ListItemIcon>
|
||||||
|
{renderIcon(control.icon(this.props.state), "mdi-24px")}
|
||||||
|
</ListItemIcon>}
|
||||||
{this.renderControl(control)}
|
{this.renderControl(control)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|
@ -70,7 +65,9 @@ export default class UiItemList extends React.PureComponent<UiItemListProps> {
|
||||||
onChangeState={this.props.onChangeState} />;
|
onChangeState={this.props.onChangeState} />;
|
||||||
}
|
}
|
||||||
case "slider": {
|
case "slider": {
|
||||||
return this.renderSlider(control);
|
return <Slider item={control}
|
||||||
|
state={this.props.state}
|
||||||
|
onChangeState={this.props.onChangeState} />;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
return <Text item={control}
|
return <Text item={control}
|
||||||
|
|
@ -89,43 +86,4 @@ export default class UiItemList extends React.PureComponent<UiItemListProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getValue(control: UIControl) {
|
|
||||||
const value = this.props.state[control.topic];
|
|
||||||
if (value == null) {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown topic "${control.topic}" in ${control.type} "${control.text}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSlider(control: UISlider) {
|
|
||||||
const value = this.getValue(control);
|
|
||||||
const on = (dontApply: ?boolean) => () => {
|
|
||||||
if (dontApply == null || dontApply === false) {
|
|
||||||
this.props.onChangeState(control.topic,
|
|
||||||
// $FlowFixMe
|
|
||||||
this.val);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return [
|
|
||||||
<ListItemText primary={control.text} key="text" />,
|
|
||||||
<ListItemSecondaryAction key="action">
|
|
||||||
<MuiThemeProvider>
|
|
||||||
<Slider value={value.internal || value.actual}
|
|
||||||
min={control.min || 0}
|
|
||||||
max={control.max || 100}
|
|
||||||
step={control.step || 1}
|
|
||||||
onChange={(_event, next) => {
|
|
||||||
// $FlowFixMe
|
|
||||||
this.val = next;
|
|
||||||
on(control.delayedApply)();
|
|
||||||
}}
|
|
||||||
onDragStop={on(false)}
|
|
||||||
style={{width: 100}}
|
|
||||||
/></MuiThemeProvider>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Color } from "config/colors";
|
import type { Color } from "config/colors";
|
||||||
|
import type { Icon } from "config/icon";
|
||||||
|
|
||||||
export type TopicType = (msg: Buffer) => any;
|
export type TopicType = (msg: Buffer) => string;
|
||||||
|
|
||||||
|
export type StateCommand = {
|
||||||
|
name: string,
|
||||||
|
type: TopicType
|
||||||
|
}
|
||||||
|
|
||||||
export type Topic = {
|
export type Topic = {
|
||||||
state: string,
|
state?: StateCommand,
|
||||||
command: string,
|
command?: StateCommand,
|
||||||
defaultValue: Actual,
|
defaultValue: string
|
||||||
values: Map<Internal, Actual>,
|
|
||||||
type?: TopicType
|
|
||||||
};
|
};
|
||||||
export type Topics = Map<string, Topic>;
|
export type Topics = Map<string, Topic>;
|
||||||
|
|
||||||
export type TopicDependentOption<T> = (
|
|
||||||
internal: Internal, actual: Actual, state: State
|
|
||||||
) => T;
|
|
||||||
export type StateDependentOption<T> = (
|
|
||||||
internals: Map<string, Internal>, actuals: Map<string, Actual>, state: State
|
|
||||||
) => T;
|
|
||||||
|
|
||||||
export interface UIControl {
|
export interface UIControl {
|
||||||
+type: string,
|
+type: string,
|
||||||
+text: string,
|
+text: string,
|
||||||
|
|
@ -26,27 +23,27 @@ export interface UIControl {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Enableable {
|
export interface Enableable {
|
||||||
enableCondition?: TopicDependentOption<boolean>
|
enableCondition?: (s: State) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UIToggle = $ReadOnly<{|
|
export type UIToggle = $ReadOnly<{|
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: string,
|
text: string,
|
||||||
topic: string,
|
topic: string,
|
||||||
icon?: string,
|
icon?: Icon,
|
||||||
enableCondition?: TopicDependentOption<boolean>,
|
enableCondition?: (s: State) => boolean,
|
||||||
on?: Actual,
|
on?: string,
|
||||||
off?: Actual,
|
off?: string,
|
||||||
toggled?: TopicDependentOption<boolean>
|
toggled?: (v: string, s: State) => boolean
|
||||||
|}>;
|
|}>;
|
||||||
|
|
||||||
export type UIDropDown = $ReadOnly<{|
|
export type UIDropDown = $ReadOnly<{|
|
||||||
type: "dropDown",
|
type: "dropDown",
|
||||||
text: string,
|
text: string,
|
||||||
topic: string,
|
topic: string,
|
||||||
icon?: string,
|
icon?: Icon,
|
||||||
enableCondition?: TopicDependentOption<boolean>,
|
enableCondition?: (s: State) => boolean,
|
||||||
options: Map<string, any>,
|
options: Map<string, string>,
|
||||||
renderValue?: (value: string) => string
|
renderValue?: (value: string) => string
|
||||||
|}>;
|
|}>;
|
||||||
|
|
||||||
|
|
@ -54,8 +51,8 @@ export type UISlider = $ReadOnly<{|
|
||||||
type: "slider",
|
type: "slider",
|
||||||
text: string,
|
text: string,
|
||||||
topic: string,
|
topic: string,
|
||||||
icon?: string,
|
icon?: Icon,
|
||||||
enableCondition?: TopicDependentOption<boolean>,
|
enableCondition?: (s: State) => boolean,
|
||||||
min?: number,
|
min?: number,
|
||||||
max?: number,
|
max?: number,
|
||||||
step?: number,
|
step?: number,
|
||||||
|
|
@ -71,24 +68,24 @@ export type UILink = $ReadOnly<{|
|
||||||
type: "link",
|
type: "link",
|
||||||
text: string,
|
text: string,
|
||||||
link: string,
|
link: string,
|
||||||
enableCondition?: StateDependentOption<boolean>,
|
enableCondition?: (s: State) => boolean,
|
||||||
|
|
||||||
// TODO: check if both the following options are implemented
|
// TODO: check if both the following options are implemented
|
||||||
icon?: string
|
icon?: Icon
|
||||||
|}>;
|
|}>;
|
||||||
|
|
||||||
export type UIText = $ReadOnly<{|
|
export type UIText = $ReadOnly<{|
|
||||||
type: "text",
|
type: "text",
|
||||||
text: string,
|
text: string,
|
||||||
topic: string,
|
topic: string,
|
||||||
icon?: string
|
icon?: Icon
|
||||||
|}>;
|
|}>;
|
||||||
|
|
||||||
export type UIProgress = $ReadOnly<{|
|
export type UIProgress = $ReadOnly<{|
|
||||||
type: "progress",
|
type: "progress",
|
||||||
text: string,
|
text: string,
|
||||||
topic: string,
|
topic: string,
|
||||||
icon?: string,
|
icon?: Icon,
|
||||||
min?: number,
|
min?: number,
|
||||||
max?: number
|
max?: number
|
||||||
|}>;
|
|}>;
|
||||||
|
|
@ -105,16 +102,8 @@ export type ControlUI =
|
||||||
export type Control = {
|
export type Control = {
|
||||||
name: string,
|
name: string,
|
||||||
position: [number, number],
|
position: [number, number],
|
||||||
icon: string | (
|
icon: Icon,
|
||||||
internals: Map<string, Internal>,
|
iconColor?: (state: State) => Color,
|
||||||
actuals: Map<string, Actual>,
|
|
||||||
state: State
|
|
||||||
) => string,
|
|
||||||
iconColor?: (
|
|
||||||
internals: Map<string, Internal>,
|
|
||||||
actuals: Map<string, Actual>,
|
|
||||||
state: State
|
|
||||||
) => Color,
|
|
||||||
ui: Array<ControlUI>
|
ui: Array<ControlUI>
|
||||||
};
|
};
|
||||||
export type Controls = Map<string, Control>;
|
export type Controls = Map<string, Control>;
|
||||||
|
|
|
||||||
45
src/config/icon.js
Normal file
45
src/config/icon.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export opaque type RawIcon: string = string;
|
||||||
|
|
||||||
|
export type Icon = (State) => RawIcon;
|
||||||
|
|
||||||
|
export const raw_mdi = (name: string): RawIcon => {
|
||||||
|
return `mdi ${name.split(" ").map((icon) => "mdi-".concat(icon)).join(" ")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mdi = (icon: string) => () => raw_mdi(icon);
|
||||||
|
|
||||||
|
export const mdi_battery = (topic: string) => (state: State) => {
|
||||||
|
const rawval = state[topic];
|
||||||
|
const val = parseInt(rawval);
|
||||||
|
if (isNaN(val)) {
|
||||||
|
return raw_mdi("battery-unknown");
|
||||||
|
} else if (val > 95) {
|
||||||
|
return raw_mdi("battery");
|
||||||
|
} else if (val > 85) {
|
||||||
|
return raw_mdi("battery-90");
|
||||||
|
} else if (val > 75) {
|
||||||
|
return raw_mdi("battery-80");
|
||||||
|
} else if (val > 65) {
|
||||||
|
return raw_mdi("battery-70");
|
||||||
|
} else if (val > 55) {
|
||||||
|
return raw_mdi("battery-60");
|
||||||
|
} else if (val > 45) {
|
||||||
|
return raw_mdi("battery-50");
|
||||||
|
} else if (val > 35) {
|
||||||
|
return raw_mdi("battery-40");
|
||||||
|
} else if (val > 25) {
|
||||||
|
return raw_mdi("battery-30");
|
||||||
|
} else if (val > 15) {
|
||||||
|
return raw_mdi("battery-20");
|
||||||
|
} else {
|
||||||
|
return raw_mdi("battery-10");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderIcon =
|
||||||
|
(icon: RawIcon, extraClass?: string): React.Node => {
|
||||||
|
return <i className={`${extraClass || ""} ${icon}`}></i>;
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { TopicType } from "config/flowtypes";
|
import type { TopicType } from "config/flowtypes";
|
||||||
|
import at from "lodash/at";
|
||||||
|
|
||||||
|
export const string: TopicType = (msg: Buffer) => msg.toString();
|
||||||
|
|
||||||
export const string: TopicType = msg => msg.toString();
|
|
||||||
export const json = (path: string, innerType?: TopicType): TopicType => {
|
export const json = (path: string, innerType?: TopicType): TopicType => {
|
||||||
const parseAgain = innerType == null ? x => x : innerType;
|
const parseAgain = innerType == null ? (x) => x.toString() : innerType;
|
||||||
return msg => parseAgain(JSON.parse(msg.toString())[path]);
|
return (msg) => parseAgain(Buffer.from(
|
||||||
|
at(JSON.parse(msg.toString()), path)[0].toString()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TypeOptionParam = { otherwise?: string, [string]: string };
|
||||||
|
export const option = (values: TypeOptionParam): TopicType => {
|
||||||
|
// TODO: error
|
||||||
|
const defaultValue = values.otherwise != null ? values.otherwise : "";
|
||||||
|
const mapVal = (x) => (values[x] != null ? values[x] : defaultValue);
|
||||||
|
return (x) => mapVal(x.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const jsonArray = (msg: Buffer) => JSON.parse(msg.toString()).join(", ");
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,16 @@ import injectTapEventPlugin from "react-tap-event-plugin";
|
||||||
|
|
||||||
import App from "components/App";
|
import App from "components/App";
|
||||||
|
|
||||||
import "../node_modules/mdi/css/materialdesignicons.min.css";
|
import "../node_modules/@mdi/font/css/materialdesignicons.min.css";
|
||||||
import "../css/styles.css";
|
import "../css/styles.css";
|
||||||
|
|
||||||
const Config : Config = window.config;
|
import type { Config } from "config/flowtypes";
|
||||||
|
|
||||||
|
const config : Config = window.config;
|
||||||
injectTapEventPlugin();
|
injectTapEventPlugin();
|
||||||
|
|
||||||
document.title = `${Config.space.name} Map`;
|
document.title = `${config.space.name} Map`;
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const contentElement: Element = document.getElementById("content");
|
const contentElement: Element = document.getElementById("content");
|
||||||
ReactDOM.render(<App config={Config} />, contentElement);
|
ReactDOM.render(<App config={config} />, contentElement);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from "react";
|
|
||||||
import { getInternals, getActuals } from "utils/state";
|
|
||||||
|
|
||||||
import type { Control } from "config/flowtypes";
|
|
||||||
|
|
||||||
export default function parseIconName(name: string): string {
|
|
||||||
return `mdi ${name.split(" ").map((icon) => "mdi-".concat(icon)).join(" ")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderIcon = (name: string, extraClass?: string): React.Node => {
|
|
||||||
return <i className={`${extraClass || ""} ${parseIconName(name)}`}></i>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const controlGetIcon = (control: Control, state: State): string => {
|
|
||||||
const internals: Map<string, Internal> = getInternals(state);
|
|
||||||
const actuals: Map<string, Actual> = getActuals(state);
|
|
||||||
return typeof control.icon !== "function" ? control.icon
|
|
||||||
: control.icon(internals, actuals, state);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renderControlIcon = (control: Control,
|
|
||||||
state: State, extraClass?: string): React.Node => {
|
|
||||||
return renderIcon(controlGetIcon(control, state), extraClass);
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
// @flow
|
|
||||||
import mapValues from "lodash/mapValues";
|
|
||||||
|
|
||||||
export const getInternals = (state: State): Map<string, Internal> =>
|
|
||||||
mapValues(state, (x) => x.internal || x.actual);
|
|
||||||
|
|
||||||
export const getActuals = (state: State): Map<string, Actual> =>
|
|
||||||
mapValues(state, (x) => x.actual);
|
|
||||||
|
|
@ -5,7 +5,7 @@ for conf in $(ls config/); do
|
||||||
if [ "$conf" = "utils.js" ]; then
|
if [ "$conf" = "utils.js" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
yarn dev $conf
|
||||||
yarn build $conf
|
yarn build $conf
|
||||||
yarn production-build $conf
|
|
||||||
mv dist artifacts/$conf
|
mv dist artifacts/$conf
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,7 @@ declare type Classes = {
|
||||||
classes: Map<string, string>
|
classes: Map<string, string>
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type Internal = string;
|
declare type State = Map<string,string>;
|
||||||
declare type Actual = any;
|
|
||||||
declare type StateValue = {
|
|
||||||
internal: string,
|
|
||||||
actual: any
|
|
||||||
};
|
|
||||||
declare type State = Map<string,StateValue>;
|
|
||||||
|
|
||||||
//declare type State = {
|
|
||||||
// mqtt: ?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 }>,
|
|
||||||
// visibleLayers: Array<string>
|
|
||||||
//};
|
|
||||||
|
|
||||||
declare type Point = [number, number];
|
declare type Point = [number, number];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,23 @@ const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const WebpackShellPlugin = require('webpack-shell-plugin');
|
const WebpackShellPlugin = require('webpack-shell-plugin');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
|
||||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||||
|
|
||||||
const preBuildScripts = process.env.NO_FLOW == undefined ?
|
const preBuildScripts = process.env.NO_FLOW == undefined ?
|
||||||
process.env.FLOW_PATH != undefined ? [process.env.FLOW_PATH] : ['flow']
|
process.env.FLOW_PATH != undefined ? [process.env.FLOW_PATH] : ['flow']
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
module.exports = {
|
const configPath = env => {
|
||||||
|
if (env === true) {
|
||||||
|
throw "No config file was provided.";
|
||||||
|
}
|
||||||
|
return path.resolve(__dirname, `config/${env}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = env => ({
|
||||||
|
entry: {
|
||||||
|
main: [configPath(env),
|
||||||
|
path.resolve(__dirname, 'src/index.jsx')]
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
modules: [path.resolve(__dirname, "src"), "node_modules"],
|
modules: [path.resolve(__dirname, "src"), "node_modules"],
|
||||||
extensions: ['.js', '.jsx'],
|
extensions: ['.js', '.jsx'],
|
||||||
|
|
@ -23,7 +32,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{ test: /\.(woff2?|eot|ttf|svg)$/, loader: "file-loader" }
|
// TODO: CSS follow imports and minify + sourcemap on production
|
||||||
|
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
|
||||||
|
{ test: /\.(woff2?|eot|ttf|svg)$/, loader: "file-loader" },
|
||||||
|
{ test: /\.js(x)?$/, exclude: /node_modules/, loader: "babel-loader?cacheDirectory=true" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
@ -33,6 +45,5 @@ module.exports = {
|
||||||
title: 'Space Map',
|
title: 'Space Map',
|
||||||
template: 'index.ejs'
|
template: 'index.ejs'
|
||||||
}),
|
}),
|
||||||
new ExtractTextPlugin("styles.css"),
|
|
||||||
]
|
]
|
||||||
};
|
});
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
const merge = require('webpack-merge');
|
|
||||||
const path = require('path');
|
|
||||||
const common = require('./webpack.common.js');
|
|
||||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
|
||||||
|
|
||||||
const extractCSS = ExtractTextPlugin.extract({
|
|
||||||
fallback: "style-loader",
|
|
||||||
use: {
|
|
||||||
loader: "css-loader"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const configPath = env => {
|
|
||||||
if (env === true) {
|
|
||||||
throw "No config file was provided.";
|
|
||||||
}
|
|
||||||
return path.resolve(__dirname, `config/${env}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = env => merge(common, {
|
|
||||||
entry: {
|
|
||||||
main: [configPath(env),
|
|
||||||
path.resolve(__dirname, 'src/index.jsx')]
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{ test: /\.css$/, use: extractCSS },
|
|
||||||
{ test: /\.js(x)?$/, exclude: /node_modules/, loader: "babel-loader?cacheDirectory=true" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
devtool: "eval-cheap-module-source-map",
|
|
||||||
devServer: {
|
|
||||||
contentBase: './dist'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
const webpack = require('webpack');
|
|
||||||
const path = require('path');
|
|
||||||
const merge = require('webpack-merge');
|
|
||||||
const common = require('./webpack.common.js');
|
|
||||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
|
||||||
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
|
|
||||||
|
|
||||||
const extractCSS = ExtractTextPlugin.extract({
|
|
||||||
fallback: "style-loader",
|
|
||||||
use: {
|
|
||||||
loader: "css-loader",
|
|
||||||
options: {
|
|
||||||
sourceMap: true,
|
|
||||||
minimize: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const configPath = env => {
|
|
||||||
if (env === true) {
|
|
||||||
throw "No config file was provided.";
|
|
||||||
}
|
|
||||||
return path.resolve(__dirname, `config/${env}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = env => merge(common, {
|
|
||||||
entry: {
|
|
||||||
main: [configPath(env),
|
|
||||||
path.resolve(__dirname, 'src/index.jsx')],
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{ test: /\.css$/, use: extractCSS },
|
|
||||||
{ test: /\.js(x)?$/, exclude: /node_modules/, loader: "babel-loader" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
devtool: "source-map",
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
'NODE_ENV': JSON.stringify('production')
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// new LodashModuleReplacementPlugin(),
|
|
||||||
new webpack.optimize.OccurrenceOrderPlugin(),
|
|
||||||
new webpack.HashedModuleIdsPlugin(),
|
|
||||||
new webpack.optimize.AggressiveMergingPlugin()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue