Merge remote-tracking branch 'upstream/master' into patch-13

This commit is contained in:
Ranlvor 2018-06-28 21:48:31 +02:00
commit b868156deb
Signed by untrusted user who does not match committer: Ranlvor
GPG key ID: 5E12D04750EF6F8E
24 changed files with 1145 additions and 1940 deletions

View file

@ -8,9 +8,9 @@
1. run `yarn` to install all dependencies.
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.
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.
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

View file

@ -1,6 +1,8 @@
// @flow
import type { Config } from "config/flowtypes";
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";
const config : Config = {
@ -12,16 +14,18 @@ const config : Config = {
topics: [
{
hauptraum_table_light: {
command: "/public/sensoren/TPH/leinwand/control",
state: "test",
defaultValue: "A1 ON",
values: { on: "A1 ON", off: "A1 OFF" }
command: {
name: "/public/sensoren/TPH/leinwand/control",
type: types.option({ "A1 ON": "on", "A1 OFF": "off" })
},
defaultValue: "off"
},
hauptraum_table_light_on_hack: {
command: "/public/sensoren/TPH/leinwand/control",
state: "test",
defaultValue: "A1 OFF",
values: { on: "A1 ON", off: "A1 OFF" }
command: {
name: "/public/sensoren/TPH/leinwand/control",
type: types.option({ "A1 ON": "on", "A1 OFF": "off" })
},
defaultValue: "on"
}
}
],
@ -29,20 +33,20 @@ const config : Config = {
hauptraum_table_light: {
name: "Hauptraum Tisch",
position: [450, 450],
icon: "white-balance-iridescent",
icon: mdi("white-balance-iridescent"),
iconColor: () => hex("#000000"),
ui: [
{
type: "toggle",
text: "Licht",
topic: "hauptraum_table_light",
icon: "power"
icon: mdi("power")
},
{
type: "toggle",
text: "Licht",
topic: "hauptraum_table_light_on_hack",
icon: "power"
icon: mdi("power")
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,44 @@
// @flow
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) => ({
[ `esper_${name}_version` ]: {
state: `/service/esper/${chip_id}/info`,
command: "",
defaultValue: "UNKNOWN",
values: {},
type: msg => JSON.parse(msg.toString()).version.esper
state: {
name: `/service/esper/${chip_id}/info`,
type: types.json("version.esper")
},
defaultValue: "UNKNOWN"
},
[ `esper_${name}_ip` ]: {
state: `/service/esper/${chip_id}/info`,
command: "",
defaultValue: "UNKNOWN",
values: {},
type: msg => JSON.parse(msg.toString()).network.ip
state: {
name: `/service/esper/${chip_id}/info`,
type: types.json("network.ip")
},
defaultValue: "UNKNOWN"
},
[ `esper_${name}_rssi` ]: {
state: `/service/esper/${chip_id}/info`,
command: "",
defaultValue: "UNKNOWN",
values: {},
type: msg => JSON.parse(msg.toString()).wifi.rssi
state: {
name: `/service/esper/${chip_id}/info`,
type: types.json("wifi.rssi")
},
defaultValue: "UNKNOWN"
},
[ `esper_${name}_uptime` ]: {
state: `/service/esper/${chip_id}/info`,
command: "",
defaultValue: "UNKNOWN",
values: {},
state: {
name: `/service/esper/${chip_id}/info`,
type: msg => new Date(JSON.parse(msg.toString()).time.startup * 1000)
.toLocaleString()
},
[ `esper_${name}_device` ]: {
state: `/service/esper/${chip_id}/info`,
command: "",
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`,
topics: (light_id: string) => ({
[ `floalt_${light_id}_color` ]: {
state: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/state`,
command: `/service/openhab/in/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/command`,
defaultValue: "0",
values: {}
state: {
name: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/state`,
type: types.string
},
command: {
name: `/service/openhab/in/tradfri_0220_gwb8d7af2b448f_${light_id}_color_temperature/command`,
type: types.string
},
defaultValue: "0"
},
[ `floalt_${light_id}_brightness` ]: {
state: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_brightness/state`,
command: `/service/openhab/in/tradfri_0220_gwb8d7af2b448f_${light_id}_brightness/command`,
defaultValue: "0",
values: {}
state: {
name: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${light_id}_brightness/state`,
type: types.string
},
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,
prev_ui: Array<ControlUI> = []) => (
prev_ui: Array<ControlUI> = []): Array<ControlUI> => (
prev_ui.concat([
{
type: "section",
@ -69,31 +102,31 @@ export const esper_statistics = (name: string,
{
type: "text",
text: "Device Variant",
icon: "chart-donut",
icon: mdi("chart-donut"),
topic: `esper_${name}_device`
},
{
type: "text",
text: "Version",
icon: "source-branch",
icon: mdi("source-branch"),
topic: `esper_${name}_version`
},
{
type: "text",
text: "IP",
icon: "access-point-network",
icon: mdi("access-point-network"),
topic: `esper_${name}_ip`
},
{
type: "text",
text: "RSSI",
icon: "wifi",
icon: mdi("wifi"),
topic: `esper_${name}_rssi`
},
{
type: "text",
text: "Running since…",
icon: "av-timer",
icon: mdi("av-timer"),
topic: `esper_${name}_uptime`
}
])

View file

@ -5,6 +5,7 @@
.leaflet-container {
height: calc(100vh - 64px);
max-height: 100%;
background: rgba(239,239,203,.59);
}
body {
margin: 0;

View file

@ -4,20 +4,19 @@
"author": "uwap <me+mqttmap.package.json@uwap.name>",
"description": "control devices via mqtt on a beautiful map of your space",
"scripts": {
"build": "webpack --bail --config webpack.dev.js --env",
"production-build": "webpack --bail --config webpack.prod.js --env",
"watch": "webpack-dev-server --open --config webpack.dev.js --env",
"build": "webpack --bail --config webpack.config.js -p --env",
"dev": "webpack --bail --config webpack.config.js --mode development --env",
"watch": "webpack-dev-server --open --config webpack.config.js --mode development --env",
"travis": "./travis.sh",
"lint": "eslint -- --ext js --ext jsx src/",
"lint": "eslint --ext js --ext jsx src/",
"precommit": "yarn lint"
},
"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",
"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",
"react": "^16.0.0",
"react-dom": "^16.0.0",
@ -31,25 +30,23 @@
"babel-eslint": "^8.0.1",
"babel-loader": "^7.1.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"clean-webpack-plugin": "^0.1.18",
"css-loader": "^0.28.9",
"eslint": "^4.16.0",
"eslint": "^5.0.1",
"eslint-plugin-flowtype": "^2.42.0",
"eslint-plugin-react": "^7.6.1",
"extract-text-webpack-plugin": "next",
"file-loader": "^1.1.5",
"flow": "^0.2.3",
"flow-bin": "^0.69.0",
"flow-bin": "^0.75.0",
"flow-typed": "^2.3.0",
"html-webpack-plugin": "^3.1.0",
"husky": "^0.14.3",
"lodash-webpack-plugin": "^0.11.4",
"style-loader": "^0.21.0",
"webpack": "^4.3.0",
"webpack-cli": "^2.0.13",
"webpack-cli": "^3.0.0",
"webpack-dev-server": "^3.1.1",
"webpack-merge": "^4.1.1",
"webpack-shell-plugin": "^0.5.0"
},
"license": "MIT"

View file

@ -8,19 +8,16 @@ import merge from "lodash/merge";
import type { Config, Control, Topics } from "config/flowtypes";
import MuiThemeProvider from "material-ui/styles/MuiThemeProvider";
import createMuiTheme from "material-ui/styles/createMuiTheme";
import withStyles from "material-ui/styles/withStyles";
import * as Colors from "material-ui/colors";
import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider";
import createMuiTheme from "@material-ui/core/styles/createMuiTheme";
import withStyles from "@material-ui/core/styles/withStyles";
import * as Colors from "@material-ui/core/colors";
import SideBar from "components/SideBar";
import ControlMap from "components/ControlMap";
import TopBar from "components/TopBar";
import UiItemList from "components/UiItemList";
import keyOf from "utils/keyOf";
import { controlGetIcon } from "utils/parseIconName";
import connectMqtt from "../connectMqtt";
export type AppProps = {
@ -31,7 +28,7 @@ export type AppState = {
selectedControl: ?Control,
drawerOpened: boolean,
mqttState: State,
mqttSend: (topic: string, value: Actual) => void,
mqttSend: (topic: string, value: Buffer) => void,
mqttConnected: boolean,
};
@ -41,16 +38,15 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
this.state = {
selectedControl: null,
drawerOpened: false,
mqttState: mapValues(this.topics, (topic) => ({
actual: topic.defaultValue,
internal: keyOf(topic.values, topic.defaultValue)
})),
mqttState: mapValues(this.topics, (topic) => topic.defaultValue),
mqttSend: connectMqtt(props.config.space.mqtt, {
onMessage: this.receiveMessage.bind(this),
onConnect: () => this.setState({ mqttConnected: true }),
onReconnect: () => 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
};
@ -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(
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) {
return;
}
for (let i in topics) {
// TODO: Remove FlowFixMe
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);
this.setState({mqttState: Object.assign({}, merge(this.state.mqttState,
{ [topic]: {
actual: val,
internal: keyOf(this.topics[topic].values, val) || val
}}))});
{ [topic]: val}))});
}
}
@ -105,15 +101,15 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
this.setState({drawerOpened: false});
}
changeState(topic: string, value: Actual) {
const rawTopic = this.topics[topic].command;
if (rawTopic == null) {
changeState(topic: string, value: string) {
if (this.topics[topic].command == null) {
return;
}
this.state.mqttSend(
rawTopic,
String(this.topics[topic].values[value] || value)
);
const rawTopic = this.topics[topic].command.name;
const transformValue = this.topics[topic].command.type;
const val =
transformValue == null ? value : transformValue(Buffer.from(value));
this.state.mqttSend(rawTopic, Buffer.from(val));
}
render() {
@ -127,8 +123,7 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
control={this.state.selectedControl}
onCloseRequest={this.closeDrawer.bind(this)}
icon={this.state.selectedControl == null ? null :
controlGetIcon(this.state.selectedControl,
this.state.mqttState)}
this.state.selectedControl.icon(this.state.mqttState)}
>
{this.state.selectedControl == null
|| <UiItemList state={this.state.mqttState}

View file

@ -3,8 +3,6 @@ import React from "react";
import { Map, ImageOverlay, Marker, LayersControl } from "react-leaflet";
import { CRS, point, divIcon } from "leaflet";
import map from "lodash/map";
import mapValues from "lodash/mapValues";
import parseIconName, { controlGetIcon } from "utils/parseIconName";
import type { Controls, Control } from "config/flowtypes";
@ -50,8 +48,8 @@ export default class ControlMap extends React.PureComponent<ControlMapProps> {
}
createLeafletIcon(control: Control) {
const icon = controlGetIcon(control, this.props.state);
const iconClass = parseIconName(`${icon} 36px`);
const icon = control.icon(this.props.state);
const iconClass = `${icon} mdi-36px`;
return divIcon({
iconSize: point(36, 36),
iconAnchor: point(18, 18),
@ -61,10 +59,8 @@ export default class ControlMap extends React.PureComponent<ControlMapProps> {
}
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) {
return control.iconColor(ints, acts, this.props.state);
return control.iconColor(this.props.state);
}
return "#000";
}

View file

@ -1,31 +1,33 @@
// @flow
import * as React from "react";
import withStyles from "material-ui/styles/withStyles";
import Drawer from "material-ui/Drawer";
import Typography from "material-ui/Typography";
import IconButton from "material-ui/IconButton";
import AppBar from "material-ui/AppBar";
import Toolbar from "material-ui/Toolbar";
import List from "material-ui/List";
import { renderIcon } from "utils/parseIconName";
import withStyles from "@material-ui/core/styles/withStyles";
import Drawer from "@material-ui/core/Drawer";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import List from "@material-ui/core/List";
import { renderIcon } from "config/icon";
import type { RawIcon } from "config/icon";
import type { Control } from "config/flowtypes";
export type SideBarProps = {
control: ?Control,
open: boolean,
onCloseRequest: () => void,
icon?: ?string,
icon?: ?RawIcon,
children?: React.Node
};
export type SideBarState = {
};
class SideBar extends React.PureComponent<SideBarProps & Classes, SideBarState>
{
constructor(props: SideBarProps & Classes) {
type Props = SideBarProps & Classes;
class SideBar extends React.PureComponent<Props, SideBarState> {
constructor(props: Props) {
super(props);
}

View file

@ -1,10 +1,10 @@
// @flow
import React from "react";
import AppBar from "material-ui/AppBar";
import Toolbar from "material-ui/Toolbar";
import Typography from "material-ui/Typography";
import { CircularProgress } from "material-ui/Progress";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import CircularProgress from "@material-ui/core/CircularProgress";
export type TopBarProps = {
title: string,

View file

@ -2,31 +2,30 @@
import React from "react";
import keys from "lodash/keys";
import map from "lodash/map";
import {
ListItemSecondaryAction,
ListItemText,
ListSubheader
} from "material-ui/List";
import Switch from "material-ui/Switch";
import Input, { InputLabel } from "material-ui/Input";
import { FormControl } from "material-ui/Form";
import Select from "material-ui/Select";
import { MenuItem } from "material-ui/Menu";
import Button from "material-ui/Button";
import { LinearProgress } from "material-ui/Progress";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import ListSubheader from "@material-ui/core/ListSubheader";
import Switch from "@material-ui/core/Switch";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Button from "@material-ui/core/Button";
import LinearProgress from "@material-ui/core/LinearProgress";
import SliderComponent from "@material-ui/lab/Slider";
import type {
UIControl, UIToggle, UIDropDown, UILink,
UISection, UIText, UIProgress
UISection, UIText, UIProgress, UISlider
} from "config/flowtypes";
import keyOf from "utils/keyOf";
import { getInternals, getActuals } from "utils/state";
type UiItemProps<I> = {
item: I,
state: State,
onChangeState: (topic: string, nextState: Actual) => void
onChangeState: (topic: string, nextState: string) => void
};
// eslint-disable-next-line flowtype/no-weak-types
@ -54,9 +53,7 @@ export default class UiItem<I:Object>
typeof this.props.item.enableCondition == "function") {
const enableCondition = this.props.item.enableCondition;
const state = this.props.state;
const internals = getInternals(state);
const actuals = getActuals(state);
return enableCondition(internals, actuals, state);
return enableCondition(state);
} else {
return true;
}
@ -68,7 +65,7 @@ export class UiControl<I: UIControl> extends UiItem<I> {
super(props);
}
changeState(next: Actual) {
changeState(next: string) {
if (this.props.item.topic == null) {
throw new Error(
`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;
}
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> {
@ -113,9 +97,8 @@ export class Toggle extends UiControl<UIToggle> {
const value = this.getValue();
const control = this.props.item;
const isChecked = control.toggled ||
((i, _a, _s) => i === (control.on || "on"));
const checked = isChecked(
value.internal || value.actual, value.actual, this.props.state);
((i, _s) => i === (control.on || "on"));
const checked = isChecked(value, this.props.state);
return checked;
}
@ -145,7 +128,7 @@ export class Toggle extends UiControl<UIToggle> {
}
export class DropDown extends UiControl<UIDropDown> {
runPrimaryAction = (next?: Actual) => {
runPrimaryAction = (next?: string) => {
if (this.isEnabled()) {
const control = this.props.item;
const optionKeys = keys(control.options);
@ -172,7 +155,7 @@ export class DropDown extends UiControl<UIDropDown> {
return (
<FormControl>
<InputLabel htmlFor={id}>{control.text}</InputLabel>
<Select value={value.internal || value.actual}
<Select value={value}
onChange={(event) => this.runPrimaryAction(event.target.value)}
disabled={!this.isEnabled()}
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> {
runPrimaryAction = () => {
const control = this.props.item;
@ -223,7 +228,7 @@ export class Text extends UiControl<UIText> {
render() {
return [
<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() {
const min = this.props.item.min || 0;
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;
return [
<ListItemText key="label" secondary={this.props.item.text} />,
@ -242,3 +247,4 @@ export class Progress extends UiControl<UIProgress> {
];
}
}

View file

@ -1,25 +1,18 @@
// @flow
import React from "react";
import {
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText
} from "material-ui/List";
import { renderIcon } from "utils/parseIconName";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import { renderIcon } from "config/icon";
import type { ControlUI, UIControl, UISlider } from "config/flowtypes";
import type { ControlUI } from "config/flowtypes";
// TODO: Use something else
import Slider from "material-ui-old/Slider";
import MuiThemeProvider from "material-ui-old/styles/MuiThemeProvider";
import { Toggle, DropDown, Link, Section, Text, Progress } from "./UiItem";
import { Toggle, DropDown, Link,
Section, Text, Progress, Slider } from "./UiItem";
export type UiItemListProps = {
controls: Array<ControlUI>,
state: State,
onChangeState: (topic: string, nextState: Actual) => void
onChangeState: (topic: string, nextState: string) => void
};
export default class UiItemList extends React.PureComponent<UiItemListProps> {
@ -40,7 +33,9 @@ export default class UiItemList extends React.PureComponent<UiItemListProps> {
return (
<ListItem key={key}>
{control.icon == null ||
<ListItemIcon>{renderIcon(control.icon, "mdi-24px")}</ListItemIcon>}
<ListItemIcon>
{renderIcon(control.icon(this.props.state), "mdi-24px")}
</ListItemIcon>}
{this.renderControl(control)}
</ListItem>
);
@ -70,7 +65,9 @@ export default class UiItemList extends React.PureComponent<UiItemListProps> {
onChangeState={this.props.onChangeState} />;
}
case "slider": {
return this.renderSlider(control);
return <Slider item={control}
state={this.props.state}
onChangeState={this.props.onChangeState} />;
}
case "text": {
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>
];
}
}

View file

@ -1,24 +1,21 @@
// @flow
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 = {
state: string,
command: string,
defaultValue: Actual,
values: Map<Internal, Actual>,
type?: TopicType
state?: StateCommand,
command?: StateCommand,
defaultValue: string
};
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 {
+type: string,
+text: string,
@ -26,27 +23,27 @@ export interface UIControl {
}
export interface Enableable {
enableCondition?: TopicDependentOption<boolean>
enableCondition?: (s: State) => boolean
}
export type UIToggle = $ReadOnly<{|
type: "toggle",
text: string,
topic: string,
icon?: string,
enableCondition?: TopicDependentOption<boolean>,
on?: Actual,
off?: Actual,
toggled?: TopicDependentOption<boolean>
icon?: Icon,
enableCondition?: (s: State) => boolean,
on?: string,
off?: string,
toggled?: (v: string, s: State) => boolean
|}>;
export type UIDropDown = $ReadOnly<{|
type: "dropDown",
text: string,
topic: string,
icon?: string,
enableCondition?: TopicDependentOption<boolean>,
options: Map<string, any>,
icon?: Icon,
enableCondition?: (s: State) => boolean,
options: Map<string, string>,
renderValue?: (value: string) => string
|}>;
@ -54,8 +51,8 @@ export type UISlider = $ReadOnly<{|
type: "slider",
text: string,
topic: string,
icon?: string,
enableCondition?: TopicDependentOption<boolean>,
icon?: Icon,
enableCondition?: (s: State) => boolean,
min?: number,
max?: number,
step?: number,
@ -71,24 +68,24 @@ export type UILink = $ReadOnly<{|
type: "link",
text: string,
link: string,
enableCondition?: StateDependentOption<boolean>,
enableCondition?: (s: State) => boolean,
// TODO: check if both the following options are implemented
icon?: string
icon?: Icon
|}>;
export type UIText = $ReadOnly<{|
type: "text",
text: string,
topic: string,
icon?: string
icon?: Icon
|}>;
export type UIProgress = $ReadOnly<{|
type: "progress",
text: string,
topic: string,
icon?: string,
icon?: Icon,
min?: number,
max?: number
|}>;
@ -105,16 +102,8 @@ export type ControlUI =
export type Control = {
name: string,
position: [number, number],
icon: string | (
internals: Map<string, Internal>,
actuals: Map<string, Actual>,
state: State
) => string,
iconColor?: (
internals: Map<string, Internal>,
actuals: Map<string, Actual>,
state: State
) => Color,
icon: Icon,
iconColor?: (state: State) => Color,
ui: Array<ControlUI>
};
export type Controls = Map<string, Control>;

45
src/config/icon.js Normal file
View 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>;
};

View file

@ -1,8 +1,21 @@
// @flow
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 => {
const parseAgain = innerType == null ? x => x : innerType;
return msg => parseAgain(JSON.parse(msg.toString())[path]);
const parseAgain = innerType == null ? (x) => x.toString() : innerType;
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(", ");

View file

@ -5,14 +5,16 @@ import injectTapEventPlugin from "react-tap-event-plugin";
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";
const Config : Config = window.config;
import type { Config } from "config/flowtypes";
const config : Config = window.config;
injectTapEventPlugin();
document.title = `${Config.space.name} Map`;
document.title = `${config.space.name} Map`;
// $FlowFixMe
const contentElement: Element = document.getElementById("content");
ReactDOM.render(<App config={Config} />, contentElement);
ReactDOM.render(<App config={config} />, contentElement);

View file

@ -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);
};

View file

@ -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);

View file

@ -5,7 +5,7 @@ for conf in $(ls config/); do
if [ "$conf" = "utils.js" ]; then
continue
fi
yarn dev $conf
yarn build $conf
yarn production-build $conf
mv dist artifacts/$conf
done

View file

@ -7,26 +7,7 @@ declare type Classes = {
classes: Map<string, string>
};
declare type Internal = 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 State = Map<string,string>;
declare type Point = [number, number];

View file

@ -2,14 +2,23 @@ const path = require('path');
const webpack = require('webpack');
const WebpackShellPlugin = require('webpack-shell-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const CleanWebpackPlugin = require('clean-webpack-plugin');
const preBuildScripts = process.env.NO_FLOW == undefined ?
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: {
modules: [path.resolve(__dirname, "src"), "node_modules"],
extensions: ['.js', '.jsx'],
@ -23,7 +32,10 @@ module.exports = {
},
module: {
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: [
@ -33,6 +45,5 @@ module.exports = {
title: 'Space Map',
template: 'index.ejs'
}),
new ExtractTextPlugin("styles.css"),
]
};
});

View file

@ -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'
},
});

View file

@ -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()
]
});

1632
yarn.lock

File diff suppressed because it is too large Load diff