Compare commits
121 commits
greenkeepe
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b1c59d866a | |||
| 3528e8179d | |||
| a67aaade0a | |||
|
|
bb8f5be644 | ||
| dd198d8aaa | |||
| 60178de93b | |||
| e89fd10c23 | |||
| 6f3fb4dc0a | |||
|
|
8e002e3b1a | ||
|
|
58c9ae1125 | ||
|
|
c2ed2fd37a | ||
|
|
eb7d1f7c5d | ||
|
|
b6c7ba89cb | ||
|
|
14d72d626c | ||
|
|
d73ee95178 | ||
|
|
fa8d403076 | ||
|
|
38de936bdd | ||
|
|
060dfa61d8 | ||
|
|
ca2bef4248 | ||
|
|
f2f5e3441b | ||
|
|
eab431f13b | ||
|
|
53911db686 | ||
|
|
51d410fbd3 | ||
|
|
4d39149e77 | ||
|
|
5b62bc9325 | ||
|
|
9e8765711a | ||
|
|
cbc3084eff | ||
|
|
9b0c554e54 | ||
|
|
4806973c0a | ||
|
|
167d58d670 | ||
|
|
b4b85fdb17 | ||
|
|
8c91dee9b8 | ||
|
|
db62d59f67 | ||
|
|
3208de65d4 | ||
|
|
bb334ca7bc | ||
|
|
fa07cdc784 | ||
|
|
9d44540ebc | ||
|
|
e6b26dbac1 | ||
|
|
27a9544092 | ||
|
|
6b78cc0a2f | ||
|
|
6e63404724 | ||
|
|
6028302a64 | ||
|
|
a6aef2ee9a | ||
|
|
01ed36e5be | ||
|
|
a25d7cc01d | ||
|
|
57655b0f66 | ||
|
|
b7b0bd7cac | ||
|
|
6c80eb2bd4 | ||
|
|
9a070dcc2b | ||
|
|
424aef1991 | ||
|
|
e3eb924f1f | ||
|
|
9e388f4ae9 | ||
|
|
dc2728b59a | ||
|
|
94aafdfbdd | ||
|
|
ee4800d087 | ||
|
|
b898455f56 | ||
|
|
4e316f9e47 | ||
|
|
fdb49f79a5 | ||
|
|
39c364c742 | ||
|
|
c298a58a47 | ||
| 0a0639e994 | |||
| d9096b13e4 | |||
| a44eea520a | |||
| 0f08e9f1ee | |||
| ccd9bcd3b5 | |||
|
|
550e0c7479 | ||
|
|
3fd432b2d2 | ||
|
|
263507871b | ||
|
|
65452ac0cb | ||
|
|
d50ea6fd49 | ||
|
|
53d547bc65 | ||
| 2997ff8862 | |||
| e43842fbed | |||
| 1c1de6356c | |||
| 9a5557db03 | |||
| 8376f404e4 | |||
| 856aab41ad | |||
| 43a33c3ab3 | |||
|
|
5bae8025bd | ||
|
|
d338093eee | ||
| 8917402888 | |||
| af89238999 | |||
|
|
42e7697bf8 | ||
|
|
66ff91da2d | ||
| d39e547623 | |||
|
|
2e1d9d83c8 | ||
|
|
9af88a02a4 | ||
|
|
195728631a | ||
|
|
890960fe23 | ||
|
|
15cf79f547 | ||
|
|
c29775389b | ||
|
|
23a193625c | ||
| db36cdf37c | |||
| b4979ca910 | |||
|
|
c87e739c60 | ||
|
|
e3fb07eb5e | ||
| 0b08d8e0ad | |||
| b173050dce | |||
| edb3ae4ff9 | |||
|
|
226bfa3602 | ||
|
|
7a57cf8e7f | ||
|
|
392c9f6507 | ||
|
|
c7aa754d6a | ||
|
|
6d292c3433 | ||
|
|
6c641f4eba | ||
|
|
45bb9034fe | ||
|
|
4588d450fb | ||
|
|
8ac32ced41 | ||
|
|
a149fed592 | ||
|
|
43dcf12281 | ||
|
|
50083999da | ||
|
|
45e2e7c6fc | ||
|
|
2d528a72c6 | ||
| 9d0f47299a | |||
| 6ab1c3bfae | |||
| 3aaf7a0430 | |||
|
|
56c39461c4 | ||
| 4d40321975 | |||
| 0a2c46c37b | |||
| ee2dbe0f7b | |||
| 0a027fd7c2 |
5
.babelrc
|
|
@ -1,7 +1,10 @@
|
||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
["@babel/preset-env", {
|
["@babel/preset-env", {
|
||||||
modules: false
|
modules: false,
|
||||||
|
corejs: "3.6",
|
||||||
|
useBuiltIns: "entry",
|
||||||
|
targets: "last 3 years"
|
||||||
}],
|
}],
|
||||||
"@babel/preset-react",
|
"@babel/preset-react",
|
||||||
"@babel/preset-flow"
|
"@babel/preset-flow"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ module.exports = {
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:flowtype/recommended",
|
"plugin:flowtype/recommended",
|
||||||
"plugin:react/recommended"
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
[ignore]
|
[ignore]
|
||||||
|
|
||||||
|
[untyped]
|
||||||
|
.*/node_modules/react-leaflet
|
||||||
|
|
||||||
[include]
|
[include]
|
||||||
|
|
||||||
[libs]
|
[libs]
|
||||||
|
|
@ -7,9 +10,8 @@ types/types.js
|
||||||
types/mqtt.js
|
types/mqtt.js
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
esproposal.export_star_as=enable
|
|
||||||
module.system.node.resolve_dirname=node_modules
|
module.system.node.resolve_dirname=node_modules
|
||||||
module.system.node.resolve_dirname=./src
|
module.system.node.resolve_dirname=src
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
all=warn
|
all=warn
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,6 @@ node_js:
|
||||||
script:
|
script:
|
||||||
- yarn travis
|
- yarn travis
|
||||||
|
|
||||||
before_install: yarn global add greenkeeper-lockfile@1
|
|
||||||
before_script: greenkeeper-lockfile-update
|
|
||||||
after_script: greenkeeper-lockfile-upload
|
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- ./travis-upload-artifacts.sh
|
- ./travis-upload-artifacts.sh
|
||||||
|
|
||||||
|
|
|
||||||
32
README.md
|
|
@ -1,7 +1,6 @@
|
||||||
# MQTT Control Map
|
# MQTT Control Map
|
||||||
|
|
||||||
[](https://travis-ci.org/uwap/mqtt-control-map)
|
[](https://travis-ci.org/uwap/mqtt-control-map)
|
||||||
[](https://greenkeeper.io/)
|
|
||||||
|
|
||||||
## Development / Configuration
|
## Development / Configuration
|
||||||
|
|
||||||
|
|
@ -12,33 +11,6 @@ your the mqtt control map for the given CONFIG everytime something changes.
|
||||||
for the given config.
|
for the given config.
|
||||||
4. run `yarn build CONFIG` to generate all files for production use.
|
4. run `yarn build CONFIG` to generate all files for production use.
|
||||||
|
|
||||||
## Config
|
## Documentation
|
||||||
|
|
||||||
See `config/`.
|
The documentation can be found in our [mqtt-control-map wiki](https://github.com/uwap/mqtt-control-map/wiki).
|
||||||
|
|
||||||
The Config format consists out of two sections. Topics and Controls.
|
|
||||||
|
|
||||||
### Topics
|
|
||||||
|
|
||||||
The topics section defines the mqtt interfaces.
|
|
||||||
|
|
||||||
### Controls
|
|
||||||
|
|
||||||
The Controls define the UI Controls.
|
|
||||||
|
|
||||||
| Name | Type | Optional? | Default | Description |
|
|
||||||
|-----------------|-------------------|------------|-----------------|-------------|
|
|
||||||
| type | "toggle" \| "dropDown" \| "slider" | No | | The type of the UI element. |
|
|
||||||
| text | string | No | | The text displayed right next to the UI element. |
|
|
||||||
| topic | string | No | | The topic the UI element is supposed to change and/or receive its status from. |
|
|
||||||
| enableCondition | (key: string, value: *) => boolean | Yes | () => true | This option allows you to enable or disable UI elements, depending on the current state. The first parameter is the internal representation of the value. For example "off". The second parameter is the actual value that was received via MQTT. Return true to enable the element and false to disable it. |
|
|
||||||
| **Toggle Options** |
|
|
||||||
| on | string | Yes | "on" | If the state is equal to the value of this option the toggle will be toggled on (if the toggled function is not overriden). This is also the value that will be sent when the button is toggled on. |
|
|
||||||
| off | string | Yes | "off" | If the state is equal to the value of this option the toggle will be toggled off (if the toggled function is not overriden). This is also the value that will be sent when the button is toggled off. |
|
|
||||||
| toggled | (key: string, value: *) => boolean | Yes | Use the on and off options | This is the function that decides whether the button should be in a toggled state or not. The parameters are equivalent to those of `enableCondition`. Return true to set the button to a toggled state. Return false to set it to the untoggled state. |
|
|
||||||
| **DropDown Options** |
|
|
||||||
| options | Map<string,string>| Yes | {} | This is an attribute set that will map all values defined in the topics section to a description text. For example `{ on: "Lights On", off: "Lights Off" }` will give the drop down element two options, one that is named `Lights On` and one that is named `Lights Off`. |
|
|
||||||
| **Slider Options** |
|
|
||||||
| min | number | Yes | 0 | The minimum value of that slider. |
|
|
||||||
| max | number | Yes | 1 | The maximum value of that slider. |
|
|
||||||
| step | number | Yes | 1 | The smallest step of the slider. |
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Config } from "config/flowtypes";
|
import type { Config } from "config/flowtypes";
|
||||||
import { hex } from "config/colors";
|
|
||||||
import * as types from "config/types";
|
import * as types from "config/types";
|
||||||
import { mdi } from "config/icon";
|
import * as icons from "@mdi/js";
|
||||||
|
import { svg } from "config/icon";
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
space: {
|
space: {
|
||||||
|
|
@ -32,20 +32,19 @@ const config: Config = {
|
||||||
hauptraumTableLight: {
|
hauptraumTableLight: {
|
||||||
name: "Hauptraum Tisch",
|
name: "Hauptraum Tisch",
|
||||||
position: [450, 450],
|
position: [450, 450],
|
||||||
icon: mdi("white-balance-iridescent"),
|
icon: svg(icons.mdiWhiteBalanceIridescent),
|
||||||
iconColor: () => hex("#000000"),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Licht",
|
text: "Licht",
|
||||||
topic: "hauptraumTableLight",
|
topic: "hauptraumTableLight",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Licht",
|
text: "Licht",
|
||||||
topic: "hauptraumTableLightOnHack",
|
topic: "hauptraumTableLightOnHack",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -1 +1 @@
|
||||||
<mxfile userAgent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36" version="7.6.7" editor="www.draw.io" type="device"><diagram id="99136bbf-b1d7-fc77-4446-920ab02a96c1" name="Page-1">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</diagram></mxfile>
|
<mxfile userAgent="Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0" version="9.4.2" editor="www.draw.io" type="device"><diagram id="99136bbf-b1d7-fc77-4446-920ab02a96c1" name="Page-1">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</diagram></mxfile>
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
import type { Config } from "config/flowtypes";
|
import type { Config } from "config/flowtypes";
|
||||||
import * as types from "config/types";
|
import * as types from "config/types";
|
||||||
import { hex, rainbow } from "config/colors";
|
import { hex, rainbow } from "config/colors";
|
||||||
import { mdi, rawMdi } from "config/icon";
|
import { svg, withState } from "config/icon";
|
||||||
import { esper, tasmota } from "./utils";
|
import { esper, tasmota } from "./utils";
|
||||||
|
import * as icons from "@mdi/js";
|
||||||
|
|
||||||
import * as onkyo from "./onkyo";
|
import * as onkyo from "./onkyo";
|
||||||
import * as olymp from "./olymp";
|
import * as olymp from "./olymp";
|
||||||
|
|
@ -137,6 +138,14 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
defaultValue: ""
|
defaultValue: ""
|
||||||
},
|
},
|
||||||
|
powerConsumption: {
|
||||||
|
state: {
|
||||||
|
name: "/service/power/hauptraum/power",
|
||||||
|
type: (msg) =>
|
||||||
|
(Number.parseFloat(msg.toString()) / 1000).toFixed(2) + " kW"
|
||||||
|
},
|
||||||
|
defaultValue: ""
|
||||||
|
},
|
||||||
projector: {
|
projector: {
|
||||||
state: {
|
state: {
|
||||||
name: "/service/beamer/state",
|
name: "/service/beamer/state",
|
||||||
|
|
@ -169,8 +178,8 @@ const config: Config = {
|
||||||
type: types.option({
|
type: types.option({
|
||||||
unreachable: "unavailable",
|
unreachable: "unavailable",
|
||||||
booting: "unavailable",
|
booting: "unavailable",
|
||||||
prePrint: "printing",
|
"pre_print": "printing",
|
||||||
postPrint: "printing",
|
"post_print": "printing",
|
||||||
printing: "printing",
|
printing: "printing",
|
||||||
idle: "idle",
|
idle: "idle",
|
||||||
error: "error",
|
error: "error",
|
||||||
|
|
@ -217,11 +226,42 @@ const config: Config = {
|
||||||
type: types.option({ on: "ON", off: "OFF" })
|
type: types.option({ on: "ON", off: "OFF" })
|
||||||
},
|
},
|
||||||
defaultValue: "off"
|
defaultValue: "off"
|
||||||
|
},
|
||||||
|
whirlpoolTemperatureSetpoint: {
|
||||||
|
state: {
|
||||||
|
name: "/service/whirlpool/state",
|
||||||
|
type: types.json("temperatureSetpointC")
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
name: "/service/whirlpool/set/temperature",
|
||||||
|
type: types.string
|
||||||
|
},
|
||||||
|
defaultValue: "0"
|
||||||
|
},
|
||||||
|
whirlpoolBubbles: {
|
||||||
|
state: {
|
||||||
|
name: "/service/whirlpool/state",
|
||||||
|
type: types.json("bubbles")
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
name: "/service/whirlpool/set/bubbles",
|
||||||
|
type: types.string
|
||||||
|
},
|
||||||
|
defaultValue: "0"
|
||||||
|
},
|
||||||
|
whirlpoolTemperature: {
|
||||||
|
state: {
|
||||||
|
name: "/service/whirlpool/state",
|
||||||
|
type: types.json("waterTemperatureC")
|
||||||
|
},
|
||||||
|
defaultValue: "0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//Tasmota-Dosen
|
//Tasmota-Dosen
|
||||||
|
tasmota.topics("2", "printerAnnette"),
|
||||||
tasmota.topics("6", "snackbar"),
|
tasmota.topics("6", "snackbar"),
|
||||||
tasmota.topics("7", "infoscreen"),
|
tasmota.topics("7", "infoscreen"),
|
||||||
|
tasmota.topics("9", "pilze"),
|
||||||
|
|
||||||
esper.topics("afba40", "flyfry"),
|
esper.topics("afba40", "flyfry"),
|
||||||
|
|
||||||
|
|
@ -236,29 +276,28 @@ const config: Config = {
|
||||||
ledStahltrager: {
|
ledStahltrager: {
|
||||||
name: "LED Stahlträger",
|
name: "LED Stahlträger",
|
||||||
position: [340, 590],
|
position: [340, 590],
|
||||||
icon: mdi("white-balance-iridescent"),
|
icon: svg(icons.mdiWhiteBalanceIridescent).color(({ledStahltraeger}) =>
|
||||||
iconColor: ({ledStahltraeger}) =>
|
(ledStahltraeger === "on" ? rainbow : hex("#000000"))),
|
||||||
(ledStahltraeger === "on" ? rainbow : hex("#000000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Stahlträger LED",
|
text: "Stahlträger LED",
|
||||||
topic: "ledStahltraeger",
|
topic: "ledStahltraeger",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
snackbar: {
|
snackbar: {
|
||||||
name: "Snackbar",
|
name: "Snackbar",
|
||||||
position: [510, 500],
|
position: [510, 500],
|
||||||
icon: mdi("fridge"),
|
icon: svg(icons.mdiFridge).color(
|
||||||
iconColor: tasmota.iconColor("snackbar", hex("#E20074")),
|
tasmota.iconColor("snackbar", hex("#E20074"))),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Snackbar",
|
text: "Snackbar",
|
||||||
topic: "snackbar",
|
topic: "snackbar",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
|
|
@ -268,7 +307,7 @@ const config: Config = {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "LED-Streifen",
|
text: "LED-Streifen",
|
||||||
topic: "snackbarLedOnline",
|
topic: "snackbarLedOnline",
|
||||||
icon: mdi("white-balance-iridescent")
|
icon: svg(icons.mdiWhiteBalanceIridescent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "dropDown",
|
type: "dropDown",
|
||||||
|
|
@ -287,7 +326,7 @@ const config: Config = {
|
||||||
"11": "Rainbow Pattern",
|
"11": "Rainbow Pattern",
|
||||||
"12": "Fire Pattern"
|
"12": "Fire Pattern"
|
||||||
},
|
},
|
||||||
icon: mdi("settings"),
|
icon: svg(icons.mdiCog),
|
||||||
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
|
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -296,7 +335,7 @@ const config: Config = {
|
||||||
topic: "snackbarDimmmer",
|
topic: "snackbarDimmmer",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
|
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -305,7 +344,7 @@ const config: Config = {
|
||||||
topic: "snackbarSpeed",
|
topic: "snackbarSpeed",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 20,
|
max: 20,
|
||||||
icon: mdi("speedometer"),
|
icon: svg(icons.mdiSpeedometer),
|
||||||
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
|
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -313,142 +352,151 @@ const config: Config = {
|
||||||
twinkle: {
|
twinkle: {
|
||||||
name: "Twinkle",
|
name: "Twinkle",
|
||||||
position: [530, 560],
|
position: [530, 560],
|
||||||
icon: ({twinkle}) =>
|
icon: withState(({twinkle}) =>
|
||||||
(twinkle === "on" ? rawMdi("led-on flip-v") : rawMdi("led-off flip-v")),
|
(twinkle === "on" ? svg(icons.mdiLedOn).flipV().color(rainbow)
|
||||||
iconColor: ({twinkle}) => (twinkle === "on" ? rainbow : hex("#000000")),
|
: svg(icons.mdiLedOff).flipV())
|
||||||
|
),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Twinkle",
|
text: "Twinkle",
|
||||||
topic: "twinkle",
|
topic: "twinkle",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
fan: {
|
fan: {
|
||||||
name: "Ventilator",
|
name: "Ventilator",
|
||||||
position: [530, 440],
|
position: [530, 440],
|
||||||
icon: mdi("fan"),
|
icon: svg(icons.mdiFan).color(({fan}) =>
|
||||||
iconColor: ({fan}) => (fan === "on" ? hex("#00FF00") : hex("#000000")),
|
(fan === "on" ? hex("#00FF00") : hex("#000000"))),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Ventilator",
|
text: "Ventilator",
|
||||||
topic: "fan",
|
topic: "fan",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
cashdesk: {
|
cashdesk: {
|
||||||
name: "Cashdesk",
|
name: "Cashdesk",
|
||||||
position: [510, 467],
|
position: [510, 467],
|
||||||
icon: mdi("coin"),
|
icon: svg(icons.mdiCurrencyUsdCircle),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
link: "http://cashdesk.rzl:8081/",
|
link: "http://cashdesk.rzl:8000/",
|
||||||
text: "Open Cashdesk",
|
text: "Open Cashdesk",
|
||||||
icon: mdi("open-in-new")
|
icon: svg(icons.mdiOpenInNew)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
flyfry: {
|
flyfry: {
|
||||||
name: "Fliegenbratgerät",
|
name: "Fliegenbratgerät",
|
||||||
position: [450, 570],
|
position: [450, 570],
|
||||||
icon: mdi("fire"),
|
icon: svg(icons.mdiFire).color(({flyfry}) =>
|
||||||
iconColor: ({flyfry}) =>
|
(flyfry === "on" ? hex("#6666FF") : hex("#000000"))),
|
||||||
(flyfry === "on" ? hex("#6666FF") : hex("#000000")),
|
|
||||||
ui: esper.statistics("flyfry", [
|
ui: esper.statistics("flyfry", [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Fliegenbratgerät",
|
text: "Fliegenbratgerät",
|
||||||
topic: "flyfry",
|
topic: "flyfry",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
projector: {
|
projector: {
|
||||||
name: "Beamer",
|
name: "Beamer",
|
||||||
position: [380, 590],
|
position: [380, 590],
|
||||||
icon: mdi("projector flip-v"),
|
icon: svg(icons.mdiProjector).flipV().color(({projector}) =>
|
||||||
iconColor: ({projector}) =>
|
|
||||||
({
|
({
|
||||||
transientOn: hex("#b3b300"),
|
transientOn: hex("#b3b300"),
|
||||||
transientOff: hex("#b3b300"),
|
transientOff: hex("#b3b300"),
|
||||||
on: hex("#00ff00"),
|
on: hex("#00ff00"),
|
||||||
off: hex("#000000"),
|
off: hex("#000000"),
|
||||||
unknown: hex("#888888")
|
unknown: hex("#888888")
|
||||||
})[projector],
|
})[projector]),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Beamer",
|
text: "Beamer",
|
||||||
topic: "projector",
|
topic: "projector",
|
||||||
toggled: (val) => val === "transientOn" || val === "on",
|
toggled: (val) => val === "transientOn" || val === "on",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
loetarbeitsplatz4: {
|
loetarbeitsplatz4: {
|
||||||
name: "Lötarbeitsplatz",
|
name: "Lötarbeitsplatz",
|
||||||
position: [205, 455],
|
position: [205, 455],
|
||||||
icon: mdi("eyedropper-variant"),
|
icon: svg(icons.mdiEyedropperVariant).color(({loetarbeitsplatz4}) =>
|
||||||
iconColor: ({loetarbeitsplatz4}) =>
|
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000"))),
|
||||||
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Status",
|
text: "Status",
|
||||||
topic: "loetarbeitsplatz4",
|
topic: "loetarbeitsplatz4",
|
||||||
icon: mdi("eyedropper-variant")
|
icon: svg(icons.mdiEyedropperVariant)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
loetarbeitsplatz5: {
|
loetarbeitsplatz5: {
|
||||||
name: "Lötarbeitsplatz",
|
name: "Lötarbeitsplatz",
|
||||||
position: [205, 405],
|
position: [205, 405],
|
||||||
icon: mdi("eyedropper-variant"),
|
icon: svg(icons.mdiEyedropperVariant).color(({loetarbeitsplatz4}) =>
|
||||||
iconColor: ({loetarbeitsplatz5}) =>
|
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000"))),
|
||||||
(loetarbeitsplatz5 === "on" ? hex("#FF0000") : hex("#000000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Status",
|
text: "Status",
|
||||||
topic: "loetarbeitsplatz5",
|
topic: "loetarbeitsplatz5",
|
||||||
icon: mdi("eyedropper-variant")
|
icon: svg(icons.mdiEyedropperVariant)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
door: {
|
door: {
|
||||||
name: "Tür",
|
name: "Tür",
|
||||||
position: [455, 350],
|
position: [455, 350],
|
||||||
icon: mdi("swap-vertical"),
|
icon: svg(icons.mdiSwapVertical).color(({doorStatus}) =>
|
||||||
iconColor: ({doorStatus}) =>
|
(doorStatus === "on" ? hex("#00FF00") : hex("#FF0000"))),
|
||||||
(doorStatus === "on" ? hex("#00FF00") : hex("#FF0000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
link: "http://s.rzl.so",
|
link: "http://s.rzl.so",
|
||||||
text: "Open Status Page",
|
text: "Open Status Page",
|
||||||
icon: mdi("open-in-new")
|
icon: svg(icons.mdiOpenInNew)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "link",
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
link: "http://kunterbunt.vm.rzl/dashboard/db/allgemeines-copy-ranlvor?orgId=1",
|
||||||
|
text: "RZL-Dashboard",
|
||||||
|
icon: svg(icons.mdiOpenInNew)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Anwesend",
|
text: "Anwesend",
|
||||||
topic: "presenceStatus",
|
topic: "presenceStatus",
|
||||||
icon: mdi("account")
|
icon: svg(icons.mdiAccount)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Devices",
|
text: "Devices",
|
||||||
topic: "devicesStatus",
|
topic: "devicesStatus",
|
||||||
icon: mdi("wifi")
|
icon: svg(icons.mdiWifi)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Deko",
|
text: "Deko",
|
||||||
topic: "deko",
|
topic: "deko",
|
||||||
icon: mdi("invert-colors")
|
icon: svg(icons.mdiInvertColors)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Power Hauptraum",
|
||||||
|
topic: "powerConsumption",
|
||||||
|
icon: svg(icons.mdiSpeedometer)
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
@ -456,41 +504,56 @@ const config: Config = {
|
||||||
infoscreen: {
|
infoscreen: {
|
||||||
name: "Infoscreen",
|
name: "Infoscreen",
|
||||||
position: [255, 495],
|
position: [255, 495],
|
||||||
icon: mdi("television-guide flip-v"),
|
icon: svg(icons.mdiTelevisionGuide).flipV().color(
|
||||||
iconColor: tasmota.iconColor("infoscreen", hex("#4444FF")),
|
tasmota.iconColor("infoscreen", hex("#4444FF"))
|
||||||
|
),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Infoscreen",
|
text: "Infoscreen",
|
||||||
topic: "infoscreen",
|
topic: "infoscreen",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
link: "http://cashdesk.rzl:3030/rzl",
|
link: "http://cashdesk.rzl:3030/rzl",
|
||||||
text: "Open Infoscreen",
|
text: "Open Infoscreen",
|
||||||
icon: mdi("open-in-new")
|
icon: svg(icons.mdiOpenInNew)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
pilze: {
|
||||||
|
name: "Pilze",
|
||||||
|
position: [48, 499],
|
||||||
|
icon: withState(({pilze}) =>
|
||||||
|
(pilze === "on" ? svg(icons.mdiLedOn) : svg(icons.mdiLedOff))).color(
|
||||||
|
tasmota.iconColor("pilze", rainbow)),
|
||||||
|
ui: [
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
text: "Pilze",
|
||||||
|
topic: "pilze",
|
||||||
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
printer3D: {
|
printer3D: {
|
||||||
name: "Ultimaker 3",
|
name: "Ultimaker 3",
|
||||||
position: [754, 560],
|
position: [754, 560],
|
||||||
icon: mdi("printer-3d"),
|
icon: svg(icons.mdiPrinter3d).color(({printer3DStatus}) =>
|
||||||
iconColor: ({printer3DStatus}) =>
|
|
||||||
({
|
({
|
||||||
awaitingInteraction: hex("#b3b300"),
|
awaitingInteraction: hex("#b3b300"),
|
||||||
printing: hex("#00ff00"),
|
printing: hex("#00ff00"),
|
||||||
idle: hex("#000000"),
|
idle: hex("#000000"),
|
||||||
unavailable: hex("#888888"),
|
unavailable: hex("#888888"),
|
||||||
error: hex("#ff0000")
|
error: hex("#ff0000")
|
||||||
})[printer3DStatus],
|
})[printer3DStatus]),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
link: "http://ultimaker.rzl/",
|
link: "http://ultimaker.rzl/",
|
||||||
text: "Open Webinterface",
|
text: "Open Webinterface",
|
||||||
icon: mdi("open-in-new")
|
icon: svg(icons.mdiOpenInNew)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
|
|
@ -498,7 +561,7 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "progress",
|
type: "progress",
|
||||||
icon: mdi("rotate-right"),
|
icon: svg(icons.mdiRotateRight),
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
text: "Printing Progress",
|
text: "Printing Progress",
|
||||||
|
|
@ -507,7 +570,7 @@ const config: Config = {
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Time Left",
|
text: "Time Left",
|
||||||
icon: mdi("clock"),
|
icon: svg(icons.mdiClock),
|
||||||
topic: "printer3Dremaining"
|
topic: "printer3Dremaining"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -515,31 +578,87 @@ const config: Config = {
|
||||||
partkeepr: {
|
partkeepr: {
|
||||||
name: "Partkeepr",
|
name: "Partkeepr",
|
||||||
position: [48, 450],
|
position: [48, 450],
|
||||||
icon: mdi("chip"),
|
icon: svg(icons.mdiChip),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
link: "http://partkeepr.rzl/",
|
link: "http://partkeepr.rzl/",
|
||||||
text: "Open Partkeepr",
|
text: "Open Partkeepr",
|
||||||
icon: mdi("open-in-new")
|
icon: svg(icons.mdiOpenInNew)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
printerAnnette: {
|
||||||
|
name: "Drucker",
|
||||||
|
position: [800, 350],
|
||||||
|
icon: svg(icons.mdiPrinter).color(tasmota.iconColor("printerAnnette")),
|
||||||
|
ui: [
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
text: "Drucker",
|
||||||
|
topic: "printerAnnette",
|
||||||
|
icon: svg(icons.mdiPower)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "link",
|
||||||
|
link: "http://annette.rzl/",
|
||||||
|
text: "Open Annette",
|
||||||
|
icon: svg(icons.mdiOpenInNew)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
nebenraumPowerStatus: {
|
nebenraumPowerStatus: {
|
||||||
name: "Strom Fablab",
|
name: "Strom Fablab",
|
||||||
position: [613, 537],
|
position: [613, 537],
|
||||||
icon: ({nebenraumPowerStatus}) =>
|
icon: withState(({nebenraumPowerStatus}) =>
|
||||||
(nebenraumPowerStatus === "on" ? rawMdi("flash") : rawMdi("flash-off")),
|
(nebenraumPowerStatus === "on" ?
|
||||||
iconColor: ({nebenraumPowerStatus}) =>
|
svg(icons.mdiFlash).color(hex("#00FF00")) : svg(icons.mdiFlashOff))),
|
||||||
(nebenraumPowerStatus === "on" ? hex("#00ff00") : hex("#000000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
icon: mdi("power"),
|
icon: svg(icons.mdiPower),
|
||||||
text: "Strom Fablab",
|
text: "Strom Fablab",
|
||||||
topic: "nebenraumPowerStatus"
|
topic: "nebenraumPowerStatus"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
whirlpool: {
|
||||||
|
name: "Vorstandswhirlpool",
|
||||||
|
position: [1413, 500],
|
||||||
|
icon: svg(icons.mdiPool).color(
|
||||||
|
({whirlpoolBubbles}) =>
|
||||||
|
(parseInt(whirlpoolBubbles, 10) > 0 ? hex("#00ff00")
|
||||||
|
: hex("#000000"))),
|
||||||
|
ui: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
icon: svg(icons.mdiOilTemperature),
|
||||||
|
text: "Temperatur",
|
||||||
|
topic: "whirlpoolTemperature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
icon: svg(icons.mdiOilTemperature),
|
||||||
|
text: "Temperatur Sollwert",
|
||||||
|
topic: "whirlpoolTemperatureSetpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "slider",
|
||||||
|
min: 4,
|
||||||
|
max: 100,
|
||||||
|
text: "Temperatur Sollwert",
|
||||||
|
icon: svg(icons.mdiOilTemperature),
|
||||||
|
topic: "whirlpoolTemperatureSetpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "slider",
|
||||||
|
min: 0,
|
||||||
|
max: 9,
|
||||||
|
text: "Bubbles",
|
||||||
|
icon: svg(icons.mdiChartBubble),
|
||||||
|
topic: "whirlpoolBubbles"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
|
|
@ -551,7 +670,7 @@ const config: Config = {
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
bounds: {
|
bounds: {
|
||||||
topLeft: [0, 0],
|
topLeft: [0, 0],
|
||||||
bottomRight: [1000, 700]
|
bottomRight: [1320, 720]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -561,7 +680,7 @@ const config: Config = {
|
||||||
opacity: 0.4,
|
opacity: 0.4,
|
||||||
bounds: {
|
bounds: {
|
||||||
topLeft: [0, 0],
|
topLeft: [0, 0],
|
||||||
bottomRight: [1000, 700]
|
bottomRight: [1320, 720]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -571,7 +690,7 @@ const config: Config = {
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
bounds: {
|
bounds: {
|
||||||
topLeft: [0, 0],
|
topLeft: [0, 0],
|
||||||
bottomRight: [1000, 700]
|
bottomRight: [1320, 720]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Topics, Controls } from "config/flowtypes";
|
import type { Topics, Controls } from "config/flowtypes";
|
||||||
import { mdi, mdiBattery } from "config/icon";
|
import { svg, mdiBattery } from "config/icon";
|
||||||
import { hex } from "config/colors";
|
import { hex } from "config/colors";
|
||||||
import * as types from "config/types";
|
import * as types from "config/types";
|
||||||
import { floalt, tradfri } from "./utils";
|
import { floalt, tradfri, tasmota } from "./utils";
|
||||||
|
import * as icons from "@mdi/js";
|
||||||
|
|
||||||
export const topics: Topics = {
|
export const topics: Topics = {
|
||||||
//Kuechen-Floalts
|
//Kuechen-Floalts
|
||||||
|
|
@ -20,14 +21,16 @@ export const topics: Topics = {
|
||||||
...tradfri.remote.topics("65542"),
|
...tradfri.remote.topics("65542"),
|
||||||
...tradfri.remote.topics("65546"),
|
...tradfri.remote.topics("65546"),
|
||||||
|
|
||||||
|
...tasmota.topics("10", "lichtDunstabzug"),
|
||||||
|
|
||||||
kitchenLightColor: {
|
kitchenLightColor: {
|
||||||
state: {
|
state: {
|
||||||
name: "/service/openhab/out/kitchenLight_allColor_temperature"
|
name: "/service/openhab/out/kitchen_light_all_color_temperature"
|
||||||
+ "/state",
|
+ "/state",
|
||||||
type: types.string
|
type: types.string
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
name: "/service/openhab/in/kitchenLight_allColor_temperature"
|
name: "/service/openhab/in/kitchen_light_all_color_temperature"
|
||||||
+ "/command",
|
+ "/command",
|
||||||
type: types.string
|
type: types.string
|
||||||
},
|
},
|
||||||
|
|
@ -35,11 +38,11 @@ export const topics: Topics = {
|
||||||
},
|
},
|
||||||
kitchenLightBrightness: {
|
kitchenLightBrightness: {
|
||||||
state: {
|
state: {
|
||||||
name: "/service/openhab/out/kitchenLight_allBrightness/state",
|
name: "/service/openhab/out/kitchen_light_all_brightness/state",
|
||||||
type: types.string
|
type: types.string
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
name: "/service/openhab/in/kitchenLight_allBrightness/command",
|
name: "/service/openhab/in/kitchen_light_all_brightness/command",
|
||||||
type: types.string
|
type: types.string
|
||||||
},
|
},
|
||||||
defaultValue: "0"
|
defaultValue: "0"
|
||||||
|
|
@ -47,12 +50,12 @@ export const topics: Topics = {
|
||||||
kitchenSinkLightBrightness: {
|
kitchenSinkLightBrightness: {
|
||||||
state: {
|
state: {
|
||||||
name: "/service/openhab/out/tradfri_0100_"
|
name: "/service/openhab/out/tradfri_0100_"
|
||||||
+ "gwb8d7af2b448f_65545Brightness/state",
|
+ "gwb8d7af2b448f_65545_brightness/state",
|
||||||
type: types.string
|
type: types.string
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
name: "/service/openhab/in/tradfri_0100_"
|
name: "/service/openhab/in/tradfri_0100_"
|
||||||
+ "gwb8d7af2b448f_65545Brightness/command",
|
+ "gwb8d7af2b448f_65545_brightness/command",
|
||||||
type: types.string
|
type: types.string
|
||||||
},
|
},
|
||||||
defaultValue: "0"
|
defaultValue: "0"
|
||||||
|
|
@ -63,7 +66,7 @@ export const controls: Controls = {
|
||||||
kitchenLight: {
|
kitchenLight: {
|
||||||
name: "Deckenlicht Küche",
|
name: "Deckenlicht Küche",
|
||||||
position: [325, 407],
|
position: [325, 407],
|
||||||
icon: mdi("ceiling-light"),
|
icon: svg(icons.mdiCeilingLight),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
|
|
@ -72,14 +75,14 @@ export const controls: Controls = {
|
||||||
toggled: (n) => parseInt(n, 10) > 0,
|
toggled: (n) => parseInt(n, 10) > 0,
|
||||||
topic: "kitchenLightBrightness",
|
topic: "kitchenLightBrightness",
|
||||||
text: "Ein/Ausschalten",
|
text: "Ein/Ausschalten",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "slider",
|
type: "slider",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: "kitchenLightBrightness"
|
topic: "kitchenLightBrightness"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -87,7 +90,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Farbtemperatur",
|
text: "Farbtemperatur",
|
||||||
icon: mdi("weather-sunset-down"),
|
icon: svg(icons.mdiWeatherSunsetDown),
|
||||||
topic: "kitchenLightColor"
|
topic: "kitchenLightColor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -99,7 +102,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: floalt.brightness("65537")
|
topic: floalt.brightness("65537")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -107,7 +110,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Farbtemperatur",
|
text: "Farbtemperatur",
|
||||||
icon: mdi("weather-sunset-down"),
|
icon: svg(icons.mdiWeatherSunsetDown),
|
||||||
topic: floalt.color("65537")
|
topic: floalt.color("65537")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -119,7 +122,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: floalt.brightness("65538")
|
topic: floalt.brightness("65538")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -127,7 +130,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Farbtemperatur",
|
text: "Farbtemperatur",
|
||||||
icon: mdi("weather-sunset-down"),
|
icon: svg(icons.mdiWeatherSunsetDown),
|
||||||
topic: floalt.color("65538")
|
topic: floalt.color("65538")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -139,7 +142,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: floalt.brightness("65539")
|
topic: floalt.brightness("65539")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -147,7 +150,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Farbtemperatur",
|
text: "Farbtemperatur",
|
||||||
icon: mdi("weather-sunset-down"),
|
icon: svg(icons.mdiWeatherSunsetDown),
|
||||||
topic: floalt.color("65539")
|
topic: floalt.color("65539")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -159,7 +162,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: floalt.brightness("65540")
|
topic: floalt.brightness("65540")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -167,7 +170,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Farbtemperatur",
|
text: "Farbtemperatur",
|
||||||
icon: mdi("weather-sunset-down"),
|
icon: svg(icons.mdiWeatherSunsetDown),
|
||||||
topic: floalt.color("65540")
|
topic: floalt.color("65540")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -175,7 +178,7 @@ export const controls: Controls = {
|
||||||
kitchenSinkLight: {
|
kitchenSinkLight: {
|
||||||
name: "Licht Spüle",
|
name: "Licht Spüle",
|
||||||
position: [300, 345],
|
position: [300, 345],
|
||||||
icon: mdi("wall-sconce-flat"),
|
icon: svg(icons.mdiWallSconceFlat),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
|
|
@ -184,14 +187,14 @@ export const controls: Controls = {
|
||||||
toggled: (n) => parseInt(n, 10) > 0,
|
toggled: (n) => parseInt(n, 10) > 0,
|
||||||
topic: "kitchenSinkLightBrightness",
|
topic: "kitchenSinkLightBrightness",
|
||||||
text: "Ein/Ausschalten",
|
text: "Ein/Ausschalten",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "slider",
|
type: "slider",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: "kitchenSinkLightBrightness"
|
topic: "kitchenSinkLightBrightness"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -199,7 +202,7 @@ export const controls: Controls = {
|
||||||
kitchenCounterLight: {
|
kitchenCounterLight: {
|
||||||
name: "Deckenlicht Theke",
|
name: "Deckenlicht Theke",
|
||||||
position: [400, 440],
|
position: [400, 440],
|
||||||
icon: mdi("ceiling-light"),
|
icon: svg(icons.mdiCeilingLight),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
|
|
@ -210,7 +213,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: floalt.brightness("65544")
|
topic: floalt.brightness("65544")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -218,7 +221,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Farbtemperatur",
|
text: "Farbtemperatur",
|
||||||
icon: mdi("weather-sunset-down"),
|
icon: svg(icons.mdiWeatherSunsetDown),
|
||||||
topic: floalt.color("65544")
|
topic: floalt.color("65544")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -230,7 +233,7 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Helligkeit",
|
text: "Helligkeit",
|
||||||
icon: mdi("brightness-7"),
|
icon: svg(icons.mdiBrightness7),
|
||||||
topic: floalt.brightness("65543")
|
topic: floalt.brightness("65543")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -238,15 +241,29 @@ export const controls: Controls = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
text: "Farbtemperatur",
|
text: "Farbtemperatur",
|
||||||
icon: mdi("weather-sunset-down"),
|
icon: svg(icons.mdiWeatherSunsetDown),
|
||||||
topic: floalt.color("65543")
|
topic: floalt.color("65543")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
lichtDunstabzug: {
|
||||||
|
name: "Licht Dunstabzugshaube",
|
||||||
|
position: [252, 405],
|
||||||
|
icon: svg(icons.mdiCeilingLight),
|
||||||
|
iconColor: tasmota.iconColor("lichtDunstabzug"),
|
||||||
|
ui: [
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
text: "Licht Dunstabzugshaube",
|
||||||
|
topic: "lichtDunstabzug",
|
||||||
|
icon: svg(icons.mdiPower)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
remotes: {
|
remotes: {
|
||||||
name: "Fernbedinungen",
|
name: "Fernbedinungen",
|
||||||
position: [400, 344],
|
position: [400, 344],
|
||||||
icon: mdi("light-switch"),
|
icon: svg(icons.mdiLightSwitch),
|
||||||
iconColor: (state) => //if any remote is low make icon red
|
iconColor: (state) => //if any remote is low make icon red
|
||||||
(["65536", "65542", "65546", "65547"]
|
(["65536", "65542", "65546", "65547"]
|
||||||
.some((x) => state[tradfri.remote.low(x)] === "true")
|
.some((x) => state[tradfri.remote.low(x)] === "true")
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Topics, Controls } from "config/flowtypes";
|
import type { Topics, Controls } from "config/flowtypes";
|
||||||
import { mdi } from "config/icon";
|
import { svg } from "config/icon";
|
||||||
import { hex, rainbow } from "config/colors";
|
import { hex, rainbow } from "config/colors";
|
||||||
import * as types from "config/types";
|
import * as types from "config/types";
|
||||||
import { tasmota, esper } from "./utils";
|
import { tasmota, esper } from "./utils";
|
||||||
|
import * as icons from "@mdi/js";
|
||||||
|
|
||||||
export const topics: Topics = {
|
export const topics: Topics = {
|
||||||
|
...tasmota.topics("8", "ledOlymp"),
|
||||||
|
...esper.topics("afba45", "alarm"),
|
||||||
videogames: {
|
videogames: {
|
||||||
state: {
|
state: {
|
||||||
name: "/service/openhab/out/pca301_videogames/state",
|
name: "/service/openhab/out/pca301_videogames/state",
|
||||||
|
|
@ -38,96 +41,70 @@ export const topics: Topics = {
|
||||||
type: types.option({ on: "ON", off: "OFF" })
|
type: types.option({ on: "ON", off: "OFF" })
|
||||||
},
|
},
|
||||||
defaultValue: "off"
|
defaultValue: "off"
|
||||||
},
|
}
|
||||||
...tasmota.topics("2", "printerOlymp"),
|
|
||||||
...tasmota.topics("8", "ledOlymp"),
|
|
||||||
...esper.topics("afba45", "alarm")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const controls: Controls = {
|
export const controls: Controls = {
|
||||||
ledOlymp: {
|
ledOlymp: {
|
||||||
name: "LED Olymp",
|
name: "LED Olymp",
|
||||||
position: [196, 154],
|
position: [196, 154],
|
||||||
icon: mdi("white-balance-iridescent rotate-45"),
|
icon: svg(icons.mdiWhiteBalanceIridescent).rotate(45).color(
|
||||||
iconColor: tasmota.iconColor("ledOlymp", rainbow),
|
tasmota.iconColor("ledOlymp", rainbow)),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "LED Olymp",
|
text: "LED Olymp",
|
||||||
topic: "ledOlymp",
|
topic: "ledOlymp",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
videogames: {
|
videogames: {
|
||||||
name: "Videospiele",
|
name: "Videospiele",
|
||||||
position: [100, 100],
|
position: [100, 100],
|
||||||
icon: mdi("gamepad-variant"),
|
icon: svg(icons.mdiGamepadVariant).color(({videogames}) =>
|
||||||
iconColor: ({videogames}) =>
|
(videogames === "on" ? hex("#00FF00") : hex("#000000"))),
|
||||||
(videogames === "on" ? hex("#00FF00") : hex("#000000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Videospiele",
|
text: "Videospiele",
|
||||||
topic: "videogames",
|
topic: "videogames",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
olympPC: {
|
olympPC: {
|
||||||
name: "Rechner",
|
name: "Rechner",
|
||||||
position: [297, 90],
|
position: [297, 90],
|
||||||
icon: mdi("desktop-classic"),
|
icon: svg(icons.mdiDesktopClassic).color(({olympPC}) =>
|
||||||
iconColor: ({olympPC}) =>
|
(olympPC === "on" ? hex("#00FF00") : hex("#000000"))),
|
||||||
(olympPC === "on" ? hex("#00FF00") : hex("#000000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Rechner",
|
text: "Rechner",
|
||||||
topic: "olympPC",
|
topic: "olympPC",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
printerOlymp: {
|
|
||||||
name: "Drucker",
|
|
||||||
position: [335, 90],
|
|
||||||
icon: mdi("printer"),
|
|
||||||
iconColor: tasmota.iconColor("printerOlymp"),
|
|
||||||
ui: [
|
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
text: "Drucker",
|
|
||||||
topic: "printerOlymp",
|
|
||||||
icon: mdi("power")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "link",
|
|
||||||
link: "http://annette.rzl/",
|
|
||||||
text: "Open Annette",
|
|
||||||
icon: mdi("open-in-new")
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
rundumleuchte: {
|
rundumleuchte: {
|
||||||
name: "Rundumleuchte",
|
name: "Rundumleuchte",
|
||||||
position: [310, 275],
|
position: [310, 275],
|
||||||
icon: mdi("alarm-light"),
|
icon: svg(icons.mdiAlarmLight).color(({rundumleuchte}) =>
|
||||||
iconColor: ({rundumleuchte}) =>
|
(rundumleuchte === "on" ? hex("#F0DF10") : hex("#000000"))),
|
||||||
(rundumleuchte === "on" ? hex("#F0DF10") : hex("#000000")),
|
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Rundumleuchte",
|
text: "Rundumleuchte",
|
||||||
topic: "rundumleuchte",
|
topic: "rundumleuchte",
|
||||||
icon: mdi("power")
|
icon: svg(icons.mdiPower)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
alarm: {
|
alarm: {
|
||||||
name: "Alarm",
|
name: "Alarm",
|
||||||
position: [340, 250],
|
position: [340, 250],
|
||||||
icon: mdi("alarm-bell"),
|
icon: svg(icons.mdiAlarmBell),
|
||||||
iconColor: () => hex("#000000"),
|
iconColor: () => hex("#000000"),
|
||||||
ui: esper.statistics("alarm")
|
ui: esper.statistics("alarm")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Topics, Controls } from "config/flowtypes";
|
import type { Topics, Controls } from "config/flowtypes";
|
||||||
import { mdi } from "config/icon";
|
import { svg } from "config/icon";
|
||||||
import { hex } from "config/colors";
|
import { hex } from "config/colors";
|
||||||
import * as types from "config/types";
|
import * as types from "config/types";
|
||||||
|
import * as icons from "@mdi/js";
|
||||||
|
|
||||||
export const topics: Topics = {
|
export const topics: Topics = {
|
||||||
onkyoConnection: {
|
onkyoConnection: {
|
||||||
|
|
@ -94,6 +95,10 @@ export const topics: Topics = {
|
||||||
NPR08: "somafmLush",
|
NPR08: "somafmLush",
|
||||||
NPR09: "somafmBeatblender",
|
NPR09: "somafmBeatblender",
|
||||||
NPR0a: "ponyville",
|
NPR0a: "ponyville",
|
||||||
|
NPR0b: "deutschlandradio",
|
||||||
|
NPR0c: "somafmSuburbsOfGoa",
|
||||||
|
NPR0d: "somafmSonicUniverse",
|
||||||
|
NPR0e: "somafmChrismasLounge",
|
||||||
otherwise: "unknown"
|
otherwise: "unknown"
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -110,6 +115,10 @@ export const topics: Topics = {
|
||||||
somafmLush: "NPR08",
|
somafmLush: "NPR08",
|
||||||
somafmBeatblender: "NPR09",
|
somafmBeatblender: "NPR09",
|
||||||
ponyville: "NPR0a",
|
ponyville: "NPR0a",
|
||||||
|
deutschlandradio: "NPR0b",
|
||||||
|
somafmSuburbsOfGoa: "NPR0c",
|
||||||
|
somafmSonicUniverse: "NPR0d",
|
||||||
|
somafmChrismasLounge: "NPR0e",
|
||||||
otherwise: "NPR00"
|
otherwise: "NPR00"
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -124,12 +133,12 @@ export const controls: Controls = {
|
||||||
iconColor: ({onkyoConnection, onkyoPower}) =>
|
iconColor: ({onkyoConnection, onkyoPower}) =>
|
||||||
(onkyoConnection !== "connected" ? hex("#888888") :
|
(onkyoConnection !== "connected" ? hex("#888888") :
|
||||||
(onkyoPower === "on" ? hex("#00FF00") : hex("#000000"))),
|
(onkyoPower === "on" ? hex("#00FF00") : hex("#000000"))),
|
||||||
icon: mdi("audio-video"),
|
icon: svg(icons.mdiAudioVideo),
|
||||||
ui: [
|
ui: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Power",
|
text: "Power",
|
||||||
icon: mdi("power"),
|
icon: svg(icons.mdiPower),
|
||||||
topic: "onkyoPower",
|
topic: "onkyoPower",
|
||||||
enableCondition: (state) => state.onkyoConnection === "connected"
|
enableCondition: (state) => state.onkyoConnection === "connected"
|
||||||
},
|
},
|
||||||
|
|
@ -143,14 +152,14 @@ export const controls: Controls = {
|
||||||
topic: "onkyoVolume",
|
topic: "onkyoVolume",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 50,
|
max: 50,
|
||||||
icon: mdi("volume-high"),
|
icon: svg(icons.mdiVolumeHigh),
|
||||||
enableCondition: (state) => state.onkyoConnection === "connected"
|
enableCondition: (state) => state.onkyoConnection === "connected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
text: "Mute",
|
text: "Mute",
|
||||||
topic: "onkyoMute",
|
topic: "onkyoMute",
|
||||||
icon: mdi("volume-off"),
|
icon: svg(icons.mdiVolumeOff),
|
||||||
enableCondition: (state) => state.onkyoConnection === "connected"
|
enableCondition: (state) => state.onkyoConnection === "connected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -168,7 +177,7 @@ export const controls: Controls = {
|
||||||
pult: "Pult",
|
pult: "Pult",
|
||||||
front: "Front HDMI"
|
front: "Front HDMI"
|
||||||
},
|
},
|
||||||
icon: mdi("usb"),
|
icon: svg(icons.mdiUsb),
|
||||||
enableCondition: (state) => state.onkyoConnection === "connected"
|
enableCondition: (state) => state.onkyoConnection === "connected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -186,9 +195,13 @@ export const controls: Controls = {
|
||||||
somafmLush: "Lush (SomaFM)",
|
somafmLush: "Lush (SomaFM)",
|
||||||
somafmBeatblender: "Beat Blender (Soma FM)",
|
somafmBeatblender: "Beat Blender (Soma FM)",
|
||||||
ponyville: "Ponyville FM",
|
ponyville: "Ponyville FM",
|
||||||
|
deutschlandradio: "Deutschlandradio",
|
||||||
|
somafmSuburbsOfGoa: "Suburbs of Goa (SomaFM)",
|
||||||
|
somafmSonicUniverse: "Sonic Universe (SomaFM)",
|
||||||
|
somafmChrismasLounge: "Christmas Lounge (SomaFM)",
|
||||||
unknown: "Unknown"
|
unknown: "Unknown"
|
||||||
},
|
},
|
||||||
icon: mdi("radio"),
|
icon: svg(icons.mdiRadio),
|
||||||
enableCondition: (state) => state.onkyoConnection === "connected"
|
enableCondition: (state) => state.onkyoConnection === "connected"
|
||||||
&& state.onkyoInputs === "netzwerk"
|
&& state.onkyoInputs === "netzwerk"
|
||||||
},
|
},
|
||||||
|
|
@ -200,7 +213,7 @@ export const controls: Controls = {
|
||||||
type: "link",
|
type: "link",
|
||||||
link: "http://mpd.rzl/mpd/player/index.php",
|
link: "http://mpd.rzl/mpd/player/index.php",
|
||||||
text: "Open MPD Interface",
|
text: "Open MPD Interface",
|
||||||
icon: mdi("open-in-new")
|
icon: svg(icons.mdiOpenInNew)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { ControlUI } from "config/flowtypes";
|
import type { ControlUI, Topics } from "config/flowtypes";
|
||||||
import { mdi } from "config/icon";
|
import { svg } from "config/icon";
|
||||||
import { hex } from "config/colors";
|
import { hex, type Color } from "config/colors";
|
||||||
import * as types from "config/types";
|
import * as types from "config/types";
|
||||||
|
import * as icons from "@mdi/js";
|
||||||
|
|
||||||
export const tasmota = {
|
export const tasmota = {
|
||||||
topics: (id: string, name: string) => ({
|
topics: (id: string, name: string): Topics => ({
|
||||||
[name]: {
|
[name]: {
|
||||||
state: {
|
state: {
|
||||||
name: `stat/sonoff${id}/POWER`,
|
name: `stat/sonoff${id}/POWER`,
|
||||||
|
|
@ -26,20 +27,20 @@ export const tasmota = {
|
||||||
defaultValue: "off"
|
defaultValue: "off"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
iconColor: (name: string, onColor: Color = hex("#00FF00")) =>
|
iconColor: (name: string, onCol: Color = hex("#00FF00")): (State => Color) =>
|
||||||
(state: State) => {
|
(state: State): Color => {
|
||||||
if (state[`${name}_online`] === "off") {
|
if (state[`${name}_online`] === "off") {
|
||||||
return hex("#888888");
|
return hex("#888888");
|
||||||
} else if (state[name] === "on") {
|
} else if (state[name] === "on") {
|
||||||
return onColor;
|
return onCol;
|
||||||
}
|
}
|
||||||
return hex("#000000");
|
return hex("#000000");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const floalt = {
|
export const floalt = {
|
||||||
color: (lightId: string) => `floalt_${lightId}_color`,
|
color: (lightId: string): string => `floalt_${lightId}_color`,
|
||||||
brightness: (lightId: string) => `floalt_${lightId}_brightness`,
|
brightness: (lightId: string): string => `floalt_${lightId}_brightness`,
|
||||||
topics: (lightId: string) => ({
|
topics: (lightId: string): Topics => ({
|
||||||
[`floalt_${lightId}_color`]: {
|
[`floalt_${lightId}_color`]: {
|
||||||
state: {
|
state: {
|
||||||
name: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${lightId}` +
|
name: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${lightId}` +
|
||||||
|
|
@ -70,9 +71,9 @@ export const floalt = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const tradfriRemote = {
|
const tradfriRemote = {
|
||||||
level: (remoteId: string) => `tradfri_remote_${remoteId}_level`,
|
level: (remoteId: string): string => `tradfri_remote_${remoteId}_level`,
|
||||||
low: (remoteId: string) => `tradfri_remote_${remoteId}_low`,
|
low: (remoteId: string): string => `tradfri_remote_${remoteId}_low`,
|
||||||
topics: (remoteId: string) => ({
|
topics: (remoteId: string): Topics => ({
|
||||||
[`tradfri_remote_${remoteId}_level`]: {
|
[`tradfri_remote_${remoteId}_level`]: {
|
||||||
state: {
|
state: {
|
||||||
name: `/service/openhab/out/tradfri_0830_gwb8d7af2b448f_${remoteId}` +
|
name: `/service/openhab/out/tradfri_0830_gwb8d7af2b448f_${remoteId}` +
|
||||||
|
|
@ -106,36 +107,36 @@ const esperStatistics = (name: string,
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Device Variant",
|
text: "Device Variant",
|
||||||
icon: mdi("chart-donut"),
|
icon: svg(icons.mdiChartDonut),
|
||||||
topic: `esper_${name}_device`
|
topic: `esper_${name}_device`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Version",
|
text: "Version",
|
||||||
icon: mdi("source-branch"),
|
icon: svg(icons.mdiSourceBranch),
|
||||||
topic: `esper_${name}_version`
|
topic: `esper_${name}_version`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "IP",
|
text: "IP",
|
||||||
icon: mdi("access-point-network"),
|
icon: svg(icons.mdiAccessPointNetwork),
|
||||||
topic: `esper_${name}_ip`
|
topic: `esper_${name}_ip`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "RSSI",
|
text: "RSSI",
|
||||||
icon: mdi("wifi"),
|
icon: svg(icons.mdiWifi),
|
||||||
topic: `esper_${name}_rssi`
|
topic: `esper_${name}_rssi`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Running since…",
|
text: "Running since…",
|
||||||
icon: mdi("av-timer"),
|
icon: svg(icons.mdiAvTimer),
|
||||||
topic: `esper_${name}_uptime`
|
topic: `esper_${name}_uptime`
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
const esperTopics = (chipId: string, name: string) => ({
|
const esperTopics = (chipId: string, name: string): Topics => ({
|
||||||
[`esper_${name}_version`]: {
|
[`esper_${name}_version`]: {
|
||||||
state: {
|
state: {
|
||||||
name: `/service/esper/${chipId}/info`,
|
name: `/service/esper/${chipId}/info`,
|
||||||
|
|
|
||||||
2
config/uwap-home/assets/layers/rooms.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
1
config/uwap-home/assets/raw/uwap-home.draw.io
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<mxfile modified="2019-08-05T02:24:45.879Z" host="www.draw.io" agent="Mozilla/5.0 (X11; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0" etag="ZEvUBlrDTQidPugJw7fi" version="11.1.1" type="device"><diagram id="YpsO6_Ys41EOyISO9KxR" name="Page-1">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</diagram></mxfile>
|
||||||
1673
config/uwap-home/index.js
Normal file
|
|
@ -2,7 +2,6 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.2.0/dist/leaflet.css" integrity="sha512-M2wvCLH6DSRazYeZRIm1JnYyh22purTM+FDB5CsyxtQJYeKq83arPe5wgbNmcFXGqiSH2XR8dT/fJISVA1r/zQ==" crossorigin=""/>
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
79
package.json
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "mqtt-control-map",
|
"name": "mqtt-control-map",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "uwap <me+mqttmap.package.json@uwap.name>",
|
"author": "uwap <me@uwap.name>",
|
||||||
"description": "control devices via mqtt on a beautiful map of your space",
|
"description": "Control Devices via mqtt, visualized on a Map",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --bail --config webpack.config.js -p --env",
|
"build": "webpack --bail --config webpack.config.js --mode production --env",
|
||||||
"dev": "webpack --bail --config webpack.config.js --mode development --env",
|
"dev": "webpack --bail --config webpack.config.js --mode development --env",
|
||||||
"watch": "webpack-dev-server --open --config webpack.config.js --mode development --env",
|
"watch": "webpack-dev-server --open --config webpack.config.js --mode development --env",
|
||||||
"travis": "./travis.sh",
|
"travis": "./travis.sh",
|
||||||
|
|
@ -12,45 +12,52 @@
|
||||||
"precommit": "yarn lint"
|
"precommit": "yarn lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^3.0.1",
|
"@emotion/react": "^11.10.5",
|
||||||
"@material-ui/lab": "^3.0.0-alpha.16",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@mdi/font": "^3.0.39",
|
"@fontsource/roboto": "^4.5.8",
|
||||||
"leaflet": "^1.3.1",
|
"@mdi/react": "^1.4.0",
|
||||||
"lodash-es": "^4.17.4",
|
"@mui/material": "^5.10.15",
|
||||||
"mqtt": "^2.14.0",
|
"@mui/styles": "^5.10.15",
|
||||||
"react": "^16.0.0",
|
"leaflet": "^1.5.1",
|
||||||
"react-dom": "^16.0.0",
|
"mqtt": "^4.2.1",
|
||||||
"react-leaflet": "^2.0.0",
|
"react": "^18.2.0",
|
||||||
"redux": "^3.7.2"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-leaflet": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.0.0-rc.1",
|
"@babel/cli": "^7.5.5",
|
||||||
"@babel/core": "^7.0.0-rc.1",
|
"@babel/core": "^7.5.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.0.0-rc.1",
|
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0-rc.1",
|
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
|
||||||
"@babel/polyfill": "^7.0.0-rc.1",
|
"@babel/preset-env": "^7.5.5",
|
||||||
"@babel/preset-env": "^7.0.0-rc.1",
|
|
||||||
"@babel/preset-flow": "^7.0.0-rc.1",
|
"@babel/preset-flow": "^7.0.0-rc.1",
|
||||||
"@babel/preset-react": "^7.0.0-rc.1",
|
"@babel/preset-react": "^7.0.0-rc.1",
|
||||||
"babel-eslint": "^10.0.0",
|
"@mdi/js": "^7.0.96",
|
||||||
"babel-loader": "^8.0.0-beta",
|
"babel-eslint": "^10.0.2",
|
||||||
"clean-webpack-plugin": "^0.1.18",
|
"babel-loader": "^9.1.0",
|
||||||
"css-loader": "^1.0.0",
|
"buffer": "^6.0.3",
|
||||||
"eslint": "^5.0.1",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"eslint-plugin-flowtype": "^3.0.0",
|
"core-js": "^3.6.0",
|
||||||
|
"css-loader": "^6.7.2",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-plugin-flowtype": "^8.0.3",
|
||||||
"eslint-plugin-fp": "^2.3.0",
|
"eslint-plugin-fp": "^2.3.0",
|
||||||
"eslint-plugin-react": "^7.6.1",
|
"eslint-plugin-react": "^7.14.3",
|
||||||
"file-loader": "^2.0.0",
|
"eslint-plugin-react-hooks": "^4.1.2",
|
||||||
|
"file-loader": "^6.1.0",
|
||||||
"flow": "^0.2.3",
|
"flow": "^0.2.3",
|
||||||
"flow-bin": "^0.84.0",
|
"flow-bin": "^0.193.0",
|
||||||
"flow-typed": "^2.3.0",
|
"flow-typed": "^3.2.1",
|
||||||
"html-webpack-plugin": "^3.1.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"husky": "^1.0.0",
|
"husky": "^8.0.2",
|
||||||
"style-loader": "^0.23.0",
|
"lodash-es": "^4.17.15",
|
||||||
"webpack": "^4.3.0",
|
"process": "^0.11.10",
|
||||||
"webpack-cli": "^3.0.0",
|
"style-loader": "^3.3.1",
|
||||||
"webpack-dev-server": "^3.1.1",
|
"url": "^0.11.0",
|
||||||
"webpack-shell-plugin": "^0.5.0"
|
"webpack": "^5.75.0",
|
||||||
|
"webpack-cli": "^5.0.0",
|
||||||
|
"webpack-dev-server": "^4.11.1",
|
||||||
|
"webpack-shell-plugin-next": "^2.3.1"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,10 @@ import throttle from "lodash/throttle";
|
||||||
|
|
||||||
import type { Config, Control, Topics } from "config/flowtypes";
|
import type { Config, Control, Topics } from "config/flowtypes";
|
||||||
|
|
||||||
import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider";
|
import { withStyles } from "@mui/styles";
|
||||||
import createMuiTheme from "@material-ui/core/styles/createMuiTheme";
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
import withStyles from "@material-ui/core/styles/withStyles";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import * as Colors from "@material-ui/core/colors";
|
import Typography from "@mui/material/Typography";
|
||||||
import Snackbar from "@material-ui/core/Snackbar";
|
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
|
||||||
import Typography from "@material-ui/core/Typography";
|
|
||||||
|
|
||||||
import SideBar from "components/SideBar";
|
import SideBar from "components/SideBar";
|
||||||
import ControlMap from "components/ControlMap";
|
import ControlMap from "components/ControlMap";
|
||||||
|
|
@ -39,6 +36,14 @@ export type AppState = {
|
||||||
error: ?string
|
error: ?string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
*const App = (props: AppProps) => {
|
||||||
|
* const topics = Array.isArray(props.config.topics) ?
|
||||||
|
* Object.assign({}, ...props.config.topics) : props.config.topics;
|
||||||
|
* const [mqttConnected, setMqttConnected] = useState(false);
|
||||||
|
*};
|
||||||
|
*/
|
||||||
|
|
||||||
class App extends React.PureComponent<AppProps & Classes, AppState> {
|
class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
controlMap: React.Node
|
controlMap: React.Node
|
||||||
|
|
||||||
|
|
@ -55,7 +60,8 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
onDisconnect: () => this.setState({ mqttConnected: false }),
|
onDisconnect: () => this.setState({ mqttConnected: false }),
|
||||||
subscribe: map(
|
subscribe: map(
|
||||||
filter(keys(this.topics), (x) => this.topics[x].state != null),
|
filter(keys(this.topics), (x) => this.topics[x].state != null),
|
||||||
(x) => this.topics[x].state.name)
|
(x) => (this.topics[x].state != null ? this.topics[x].state.name : "")
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
mqttConnected: false,
|
mqttConnected: false,
|
||||||
search: "",
|
search: "",
|
||||||
|
|
@ -76,22 +82,24 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
Object.assign({}, ...this.props.config.topics) : this.props.config.topics;
|
Object.assign({}, ...this.props.config.topics) : this.props.config.topics;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles() {
|
static styles(theme) {
|
||||||
return {
|
return {
|
||||||
drawerPaper: {
|
contentElement: {
|
||||||
width: 320
|
transition: theme.transitions.create(["width"], {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen
|
||||||
|
})
|
||||||
|
},
|
||||||
|
contentElementShifted: {
|
||||||
|
width: "calc(100% - 340px)",
|
||||||
|
transition: theme.transitions.create(["width"], {
|
||||||
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static theme(config: Config) {
|
|
||||||
return createMuiTheme({
|
|
||||||
palette: {
|
|
||||||
primary: Colors[config.space.color]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
receiveMessage(rawTopic: string, message: Buffer) {
|
receiveMessage(rawTopic: string, message: Buffer) {
|
||||||
try {
|
try {
|
||||||
const topics = filter(
|
const topics = filter(
|
||||||
|
|
@ -105,8 +113,8 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
for (let i in topics) {
|
for (let i in topics) {
|
||||||
const topic = topics[i];
|
const topic = topics[i];
|
||||||
const stateTopic = this.topics[topic].state;
|
const stateTopic = this.topics[topic].state;
|
||||||
const parseVal = stateTopic ? stateTopic.type : null;
|
const typeConversion = stateTopic?.type?.from ?? stateTopic?.type;
|
||||||
const val = parseVal == null ? message.toString() : parseVal(message);
|
const val = (typeConversion ?? ((x: Buffer) => x.toString()))(message);
|
||||||
this.setMqttStateDebounced(
|
this.setMqttStateDebounced(
|
||||||
{mqttState: Object.assign({},
|
{mqttState: Object.assign({},
|
||||||
merge(this.state.mqttState, { [topic]: val}))});
|
merge(this.state.mqttState, { [topic]: val}))});
|
||||||
|
|
@ -126,16 +134,16 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
this.setState({drawerOpened: false});
|
this.setState({drawerOpened: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeState = (topic: string, value: string) => {
|
changeState = (topic: string, val: string) => {
|
||||||
try {
|
try {
|
||||||
if (this.topics[topic].command == null) {
|
const commandTopic = this.topics[topic].command;
|
||||||
|
if (commandTopic == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rawTopic = this.topics[topic].command.name;
|
const rawTopic = commandTopic.name;
|
||||||
const transformValue = this.topics[topic].command.type;
|
const typeConversion = commandTopic?.type?.to ?? commandTopic.type;
|
||||||
const val =
|
const value = (typeConversion ?? Buffer.from)(val);
|
||||||
transformValue == null ? value : transformValue(Buffer.from(value));
|
this.state.mqttSend(rawTopic, value);
|
||||||
this.state.mqttSend(rawTopic, Buffer.from(val));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err.toString() });
|
this.setState({ error: err.toString() });
|
||||||
}
|
}
|
||||||
|
|
@ -147,19 +155,24 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
state: this.state.mqttState,
|
state: this.state.mqttState,
|
||||||
changeState: this.changeState
|
changeState: this.changeState
|
||||||
}}>
|
}}>
|
||||||
<TopBar title={`${this.props.config.space.name} Map`}
|
<div className={
|
||||||
connected={this.state.mqttConnected}
|
this.state.drawerOpened
|
||||||
|
? this.props.classes.contentElementShifted
|
||||||
|
: this.props.classes.contentElement
|
||||||
|
}>
|
||||||
|
<TopBar connected={this.state.mqttConnected}
|
||||||
onSearch={(s) => this.setState({ search: s })} />
|
onSearch={(s) => this.setState({ search: s })} />
|
||||||
|
{this.controlMap(this.state.search)}
|
||||||
|
</div>
|
||||||
<SideBar open={this.state.drawerOpened}
|
<SideBar open={this.state.drawerOpened}
|
||||||
control={this.state.selectedControl}
|
control={this.state.selectedControl}
|
||||||
onCloseRequest={this.closeDrawer}
|
onCloseRequest={this.closeDrawer}
|
||||||
icon={this.state.selectedControl == null ? null :
|
icon={this.state.selectedControl == null ? null :
|
||||||
this.state.selectedControl.icon(this.state.mqttState)}
|
this.state.selectedControl.icon.render(this.state.mqttState)}
|
||||||
>
|
>
|
||||||
{this.state.selectedControl == null
|
{this.state.selectedControl == null
|
||||||
|| <UiItemList controls={this.state.selectedControl.ui} />}
|
|| <UiItemList controls={this.state.selectedControl.ui} />}
|
||||||
</SideBar>
|
</SideBar>
|
||||||
{this.controlMap(this.state.search)}
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: "bottom",
|
vertical: "bottom",
|
||||||
|
|
@ -189,11 +202,6 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: AppProps) => {
|
|
||||||
const StyledApp = withStyles(App.styles)(App);
|
|
||||||
return (
|
export default withStyles(App.styles)(App);
|
||||||
<MuiThemeProvider theme={App.theme(props.config)}>
|
|
||||||
<StyledApp {...props} />
|
|
||||||
</MuiThemeProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Map, ImageOverlay, Marker, LayersControl } from "react-leaflet";
|
import { MapContainer, 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 filter from "lodash/filter";
|
import filter from "lodash/filter";
|
||||||
import reduce from "lodash/reduce";
|
import reduce from "lodash/reduce";
|
||||||
import MqttContext from "mqtt/context";
|
import MqttContext from "mqtt/context";
|
||||||
import type { Controls, Control, UIControl, ControlUI } from "config/flowtypes";
|
import type {
|
||||||
|
Controls, Control, UIControl, ControlUI, Layer
|
||||||
|
} from "config/flowtypes";
|
||||||
|
import { renderToString } from "react-dom/server";
|
||||||
|
|
||||||
export type Point = [number, number];
|
export type Point = [number, number];
|
||||||
|
|
||||||
|
|
@ -28,21 +31,11 @@ const center = (props: ControlMapProps): Point =>
|
||||||
props.height / 2
|
props.height / 2
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const iconColor = (control: Control, state: State): string => {
|
|
||||||
if (control.iconColor != null) {
|
|
||||||
return control.iconColor(state);
|
|
||||||
}
|
|
||||||
return "#000";
|
|
||||||
};
|
|
||||||
|
|
||||||
const createLeafletIcon = (control: Control, state: State) => {
|
const createLeafletIcon = (control: Control, state: State) => {
|
||||||
const icon = control.icon(state);
|
|
||||||
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),
|
||||||
html: `<i class="${iconClass}"
|
html: renderToString(control.icon.render(state))
|
||||||
style="line-height: 1; color: ${iconColor(control, state)}"></i>`
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -52,7 +45,9 @@ const renderMarker = (props: ControlMapProps) =>
|
||||||
{({ state }) => (
|
{({ state }) => (
|
||||||
<Marker position={convertPoint(control.position)}
|
<Marker position={convertPoint(control.position)}
|
||||||
icon={createLeafletIcon(control, state)}
|
icon={createLeafletIcon(control, state)}
|
||||||
onClick={() => props.onChangeControl(control)}
|
eventHandlers={{
|
||||||
|
click: () => props.onChangeControl(control)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
</Marker>
|
</Marker>
|
||||||
)}
|
)}
|
||||||
|
|
@ -125,13 +120,13 @@ const renderLayers = (props: ControlMapProps) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const ControlMap = (props: ControlMapProps) => (
|
const ControlMap = (props: ControlMapProps) => (
|
||||||
<Map center={center(props)}
|
<MapContainer center={center(props)}
|
||||||
zoom={props.zoom}
|
zoom={props.zoom}
|
||||||
crs={CRS.Simple}
|
crs={CRS.Simple}
|
||||||
leaflet={{}}>
|
leaflet={{}}>
|
||||||
{renderMarkers(props)}
|
{renderMarkers(props)}
|
||||||
{renderLayers(props)}
|
{renderLayers(props)}
|
||||||
</Map>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ControlMap;
|
export default ControlMap;
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,55 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import withStyles from "@material-ui/core/styles/withStyles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import Drawer from "@material-ui/core/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import AppBar from "@material-ui/core/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Toolbar from "@material-ui/core/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import List from "@material-ui/core/List";
|
import List from "@mui/material/List";
|
||||||
import { renderRawIcon } from "config/icon";
|
import ReactIcon from "@mdi/react";
|
||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
|
||||||
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?: ?RawIcon,
|
icon?: ?React.Node,
|
||||||
children?: React.Node
|
children?: React.Node
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = SideBarProps & Classes;
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
drawerPaper: {
|
||||||
|
width: 340
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: theme.spacing(1)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const SideBar = (props: Props) => (
|
const SideBar = (props: SideBarProps) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
<Drawer open={props.open}
|
<Drawer open={props.open}
|
||||||
anchor="right"
|
anchor="right"
|
||||||
onClose={props.onCloseRequest}
|
onClose={props.onCloseRequest}
|
||||||
classes={{paper: props.classes.drawerPaper}}
|
classes={{paper: classes.drawerPaper}}
|
||||||
variant="persistent"
|
variant="persistent"
|
||||||
>
|
>
|
||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<span>
|
<span>
|
||||||
{props.icon == null || renderRawIcon(props.icon, "mdi-36px")}
|
{props.icon == null || props.icon}
|
||||||
</span>
|
</span>
|
||||||
<Typography variant="title" className={props.classes.flex}>
|
<Typography variant="subtitle1" className={classes.title}>
|
||||||
{props.control == null ? "" : props.control.name}
|
{props.control == null ? "" : props.control.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={props.onCloseRequest}>
|
<IconButton onClick={props.onCloseRequest}>
|
||||||
<i className="mdi mdi-close mdi-36px"></i>
|
<ReactIcon path={mdiClose} size={1.5} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|
@ -47,15 +57,7 @@ const SideBar = (props: Props) => (
|
||||||
<React.Fragment>{props.children}</React.Fragment>
|
<React.Fragment>{props.children}</React.Fragment>
|
||||||
</List>
|
</List>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = {
|
|
||||||
drawerPaper: {
|
|
||||||
width: 340
|
|
||||||
},
|
|
||||||
flex: {
|
|
||||||
flex: 1
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withStyles(styles)(SideBar);
|
export default SideBar;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import AppBar from "@material-ui/core/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Toolbar from "@material-ui/core/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
import InputBase from "@mui/material/InputBase";
|
||||||
import InputBase from "@material-ui/core/InputBase";
|
import { styled } from "@mui/styles";
|
||||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
import { alpha } from "@mui/material/styles";
|
||||||
import { withStyles } from "@material-ui/core/styles";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ReactIcon from "@mdi/react";
|
||||||
|
import { mdiMap, mdiGithub, mdiMagnify } from "@mdi/js";
|
||||||
|
|
||||||
export type TopBarProps = {
|
export type TopBarProps = {
|
||||||
title: string,
|
|
||||||
connected: boolean,
|
connected: boolean,
|
||||||
onSearch: string => void
|
onSearch: string => void
|
||||||
};
|
};
|
||||||
|
|
@ -21,77 +23,76 @@ export type SearchBarProps = {
|
||||||
|
|
||||||
const renderConnectionIndicator = (connected: boolean) => {
|
const renderConnectionIndicator = (connected: boolean) => {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return (<i style={{fontSize: 32}} className="mdi mdi-map"></i>);
|
return (<ReactIcon path={mdiMap} size={2} />);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<CircularProgress size={32} style={{color: "rgba(0, 0, 0, 0.54)"}} />
|
<CircularProgress size={32} style={{color: "rgba(0, 0, 0, 0.54)"}} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchStyles = (theme) => ({
|
const Search = styled('div')(({ theme }) => ({
|
||||||
search: {
|
position: 'relative',
|
||||||
position: "relative",
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.15),
|
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.25)
|
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
||||||
},
|
},
|
||||||
marginRight: theme.spacing.unit * 2,
|
marginRight: theme.spacing(2),
|
||||||
marginLeft: 0,
|
marginLeft: 0,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
[theme.breakpoints.up("sm")]: {
|
[theme.breakpoints.up('sm')]: {
|
||||||
marginLeft: theme.spacing.unit * 3,
|
marginLeft: theme.spacing(3),
|
||||||
width: "auto"
|
width: 'auto',
|
||||||
}
|
|
||||||
},
|
},
|
||||||
searchIcon: {
|
}));
|
||||||
width: theme.spacing.unit * 6,
|
|
||||||
height: "100%",
|
|
||||||
position: "absolute",
|
|
||||||
pointerEvents: "none",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
fontSize: "24px"
|
|
||||||
},
|
|
||||||
inputRoot: {
|
|
||||||
color: "inherit",
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
inputInput: {
|
|
||||||
paddingTop: theme.spacing.unit,
|
|
||||||
paddingRight: theme.spacing.unit,
|
|
||||||
paddingBottom: theme.spacing.unit,
|
|
||||||
paddingLeft: theme.spacing.unit * 6,
|
|
||||||
transition: theme.transitions.create("width"),
|
|
||||||
width: "100%",
|
|
||||||
[theme.breakpoints.up("md")]: {
|
|
||||||
width: 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const RawSearch = (props: SearchBarProps & Classes) => (
|
const SearchIconWrapper = styled('div')(({ theme }) => ({
|
||||||
<div className={props.classes.search}>
|
padding: theme.spacing(0, 2),
|
||||||
<i className={`mdi mdi-magnify ${props.classes.searchIcon}`}></i>
|
height: '100%',
|
||||||
<InputBase placeholder="Search…" type="search"
|
position: 'absolute',
|
||||||
onChange={(e) => props.onSearch(e.target.value)}
|
pointerEvents: 'none',
|
||||||
classes={{
|
display: 'flex',
|
||||||
root: props.classes.inputRoot,
|
alignItems: 'center',
|
||||||
input: props.classes.inputInput
|
justifyContent: 'center',
|
||||||
}} />
|
}));
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Search = withStyles(searchStyles)(RawSearch);
|
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
||||||
|
color: 'inherit',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: theme.spacing(1, 1, 1, 0),
|
||||||
|
// vertical padding + font size from searchIcon
|
||||||
|
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||||
|
transition: theme.transitions.create('width'),
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: '20ch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const openOnGithub = () => window.open(
|
||||||
|
"https://github.com/uwap/mqtt-control-map", "_blank");
|
||||||
|
|
||||||
const TopBar = (props: TopBarProps) => (
|
const TopBar = (props: TopBarProps) => (
|
||||||
<AppBar position="static">
|
<AppBar position="static" color="primary">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
{renderConnectionIndicator(props.connected)}
|
{renderConnectionIndicator(props.connected)}
|
||||||
<Search onSearch={props.onSearch} />
|
<Search>
|
||||||
|
<SearchIconWrapper>
|
||||||
|
<ReactIcon path={mdiMagnify} size={1} />
|
||||||
|
</SearchIconWrapper>
|
||||||
|
<StyledInputBase
|
||||||
|
placeholder="Search…"
|
||||||
|
inputProps={{ 'aria-label': 'search' }}
|
||||||
|
onChange={(e) => props.onSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Search>
|
||||||
<span style={{flex: 1}}></span>
|
<span style={{flex: 1}}></span>
|
||||||
<Typography variant="title">{props.title}</Typography>
|
<Tooltip title="View on Github">
|
||||||
|
<IconButton onClick={openOnGithub}>
|
||||||
|
<ReactIcon path={mdiGithub} size={1.5} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
32
src/components/UiItemList.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
|
||||||
|
import type { ControlUI } from "config/flowtypes";
|
||||||
|
|
||||||
|
import UiItem from "components/UiItems";
|
||||||
|
|
||||||
|
export type UiItemListProps = {
|
||||||
|
controls: Array<ControlUI>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UiItemList(props: UiItemListProps): React.Node {
|
||||||
|
return props.controls.map((control, key) => {
|
||||||
|
if (control.type == null) {
|
||||||
|
throw new Error(
|
||||||
|
"A control is missing the \"type\" parameter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (control.type === "section") {
|
||||||
|
return (
|
||||||
|
<UiItem item={control} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ListItem key={key}>
|
||||||
|
<UiItem item={control} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,271 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from "react";
|
|
||||||
import keys from "lodash/keys";
|
|
||||||
import map from "lodash/map";
|
|
||||||
import throttle from "lodash/throttle";
|
|
||||||
import { renderRawIcon } from "config/icon";
|
|
||||||
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, UISlider
|
|
||||||
} from "config/flowtypes";
|
|
||||||
|
|
||||||
import keyOf from "utils/keyOf";
|
|
||||||
|
|
||||||
type UiItemProps<I> = {
|
|
||||||
item: I,
|
|
||||||
state: State,
|
|
||||||
onChangeState: (topic: string, nextState: string) => void
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line flowtype/no-weak-types
|
|
||||||
export default class UiItem<I:Object>
|
|
||||||
extends React.Component<UiItemProps<I>> {
|
|
||||||
constructor(props: UiItemProps<I>) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
runPrimaryAction() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO: The type system can't really check if the enableCondition is of
|
|
||||||
* any function type or if it is a TopicDependentOption or a
|
|
||||||
* StateDependentOption. This should be fixed.
|
|
||||||
*/
|
|
||||||
rawIsEnabled(props: UiItemProps<I>) {
|
|
||||||
if (Object.keys(props.item).includes("enableCondition") &&
|
|
||||||
typeof props.item.enableCondition == "function") {
|
|
||||||
const enableCondition = props.item.enableCondition;
|
|
||||||
const state = props.state;
|
|
||||||
return enableCondition(state);
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return this.rawIsEnabled(this.props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UiControl<I: UIControl> extends UiItem<I> {
|
|
||||||
constructor(props: UiItemProps<I>) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeState(next: string) {
|
|
||||||
if (this.props.item.topic == null) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing topic in ${this.props.item.type} "${this.props.item.text}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.debouncedChange(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
debouncedChange = throttle((next: string) =>
|
|
||||||
this.props.onChangeState(this.props.item.topic, next), 50, {
|
|
||||||
leading: true,
|
|
||||||
trailing: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
shouldComponentUpdate(nextProps: UiItemProps<I>) { // TODO: Fix Flow
|
|
||||||
return nextProps.item.topic !== this.props.item.topic
|
|
||||||
|| nextProps.state[nextProps.item.topic] !==
|
|
||||||
this.props.state[this.props.item.topic]
|
|
||||||
|| this.isEnabled() !== this.rawIsEnabled(nextProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
const control = this.props.item;
|
|
||||||
const topic: string = control.topic || "";
|
|
||||||
const value = this.props.state[topic];
|
|
||||||
if (value == null) {
|
|
||||||
if (topic === "") {
|
|
||||||
throw new Error(
|
|
||||||
`Missing topic in ${control.type} "${control.text}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`Unknown topic "${topic}" in ${control.type} "${control.text}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Toggle extends UiControl<UIToggle> {
|
|
||||||
isToggled = () => {
|
|
||||||
const value = this.getValue();
|
|
||||||
const control = this.props.item;
|
|
||||||
const isChecked = control.toggled ||
|
|
||||||
((i, _s) => i === (control.on || "on"));
|
|
||||||
const checked = isChecked(value, this.props.state);
|
|
||||||
return checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
runPrimaryAction = () => {
|
|
||||||
if (this.isEnabled()) {
|
|
||||||
const control = this.props.item;
|
|
||||||
const toggled = this.isToggled();
|
|
||||||
const on = control.on == null ? "on" : control.on;
|
|
||||||
const off = control.off == null ? "off" : control.off;
|
|
||||||
const next = toggled ? off : on;
|
|
||||||
this.changeState(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return [
|
|
||||||
<ListItemText key="label" primary={this.props.item.text} />,
|
|
||||||
<ListItemSecondaryAction key="action">
|
|
||||||
<Switch label={this.props.item.text}
|
|
||||||
checked={this.isToggled()}
|
|
||||||
onChange={this.runPrimaryAction}
|
|
||||||
disabled={!this.isEnabled()}
|
|
||||||
color="primary" />
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DropDown extends UiControl<UIDropDown> {
|
|
||||||
runPrimaryAction = (next?: string) => {
|
|
||||||
if (this.isEnabled()) {
|
|
||||||
const control = this.props.item;
|
|
||||||
const optionKeys = keys(control.options);
|
|
||||||
const value = this.getValue();
|
|
||||||
const valueIndex = keyOf(optionKeys, value);
|
|
||||||
if (next == null) {
|
|
||||||
this.changeState(optionKeys[(valueIndex + 1) % optionKeys.length]);
|
|
||||||
} else {
|
|
||||||
this.changeState(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const control = this.props.item;
|
|
||||||
const value = this.getValue();
|
|
||||||
const id = `${control.topic}-${control.text}`;
|
|
||||||
const options = control.options;
|
|
||||||
if (options == null) {
|
|
||||||
throw new Error(
|
|
||||||
`Parameter "options" missing for ${control.type} "${control.text}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FormControl>
|
|
||||||
<InputLabel htmlFor={id}>{control.text}</InputLabel>
|
|
||||||
<Select value={value}
|
|
||||||
onChange={(event) => this.runPrimaryAction(event.target.value)}
|
|
||||||
disabled={!this.isEnabled()}
|
|
||||||
input={<Input id={id} />}
|
|
||||||
>
|
|
||||||
{map(options, (v, k) => <MenuItem value={k} key={k}>{v}</MenuItem>)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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={this.runPrimaryAction}
|
|
||||||
disabled={!this.isEnabled()}
|
|
||||||
style={{marginLeft: 40}} />
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Link extends UiItem<UILink> {
|
|
||||||
runPrimaryAction = () => {
|
|
||||||
const control = this.props.item;
|
|
||||||
if (control.link == null) {
|
|
||||||
throw new Error(
|
|
||||||
`Parameter "link" missing for ${control.type} "${control.text}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.isEnabled()) {
|
|
||||||
window.open(control.link, "_blank");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="raised"
|
|
||||||
onClick={this.runPrimaryAction}
|
|
||||||
color="primary"
|
|
||||||
disabled={!this.isEnabled()}
|
|
||||||
>
|
|
||||||
{this.props.item.icon == null ? ""
|
|
||||||
: renderRawIcon(this.props.item.icon(this.props.state), "mdi-24px")}
|
|
||||||
{this.props.item.text}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Section extends UiItem<UISection> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ListSubheader>{this.props.item.text}</ListSubheader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Text extends UiControl<UIText> {
|
|
||||||
render() {
|
|
||||||
return [
|
|
||||||
<ListItemText key="label" secondary={this.props.item.text} />,
|
|
||||||
<ListItemText key="vr" primary={this.getValue()} align="right" />
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
const value = val * 100 / max - min;
|
|
||||||
return [
|
|
||||||
<ListItemText key="label" secondary={this.props.item.text} />,
|
|
||||||
<div style={{ flex: "10 1 auto" }} key="progressbar">
|
|
||||||
<LinearProgress variant="determinate" value={value} />
|
|
||||||
</div>
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from "react";
|
|
||||||
import ListItem from "@material-ui/core/ListItem";
|
|
||||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
|
||||||
import { renderRawIcon } from "config/icon";
|
|
||||||
|
|
||||||
import type { ControlUI } from "config/flowtypes";
|
|
||||||
|
|
||||||
import { Toggle, DropDown, Link,
|
|
||||||
Section, Text, Progress, Slider } from "./UiItem";
|
|
||||||
import MqttContext from "mqtt/context";
|
|
||||||
import type { MqttContextValue } from "mqtt/context";
|
|
||||||
|
|
||||||
export type UiItemListProps = {
|
|
||||||
controls: Array<ControlUI>
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class UiItemList extends React.PureComponent<UiItemListProps> {
|
|
||||||
constructor(props: UiItemListProps) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.props.controls.map((control, key) => {
|
|
||||||
if (control.type == null) {
|
|
||||||
throw new Error(
|
|
||||||
"A control is missing the \"type\" parameter"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (control.type === "section") {
|
|
||||||
return (
|
|
||||||
<MqttContext.Consumer>
|
|
||||||
{this.renderListItem(control, key)}
|
|
||||||
</MqttContext.Consumer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ListItem key={key}>
|
|
||||||
<MqttContext.Consumer>
|
|
||||||
{this.renderListItem(control, key)}
|
|
||||||
</MqttContext.Consumer>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderListItem(control: ControlUI, key: number) {
|
|
||||||
return (mqtt: MqttContextValue) => {
|
|
||||||
const node = this.renderControl(control, key.toString(), mqtt);
|
|
||||||
if (control.icon == null || control.type === "link") {
|
|
||||||
return node;
|
|
||||||
} else {
|
|
||||||
const listIconNode = (
|
|
||||||
<ListItemIcon key={`${key.toString()}-liicon`}>
|
|
||||||
{renderRawIcon(control.icon(mqtt.state), "mdi-24px")}
|
|
||||||
</ListItemIcon>
|
|
||||||
);
|
|
||||||
return [listIconNode, node];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderControl(control: ControlUI, key: string, mqtt: MqttContextValue) {
|
|
||||||
const props = {
|
|
||||||
state: Object.assign({}, mqtt.state),
|
|
||||||
onChangeState: mqtt.changeState,
|
|
||||||
key: `${key}-licontrol`
|
|
||||||
};
|
|
||||||
switch (control.type) {
|
|
||||||
case "toggle": {
|
|
||||||
return <Toggle item={control} {...props} />;
|
|
||||||
}
|
|
||||||
case "dropDown": {
|
|
||||||
return <DropDown item={control} {...props} />;
|
|
||||||
}
|
|
||||||
case "section": {
|
|
||||||
return <Section item={control} {...props} />;
|
|
||||||
}
|
|
||||||
case "link": {
|
|
||||||
return <Link item={control} {...props} />;
|
|
||||||
}
|
|
||||||
case "slider": {
|
|
||||||
return <Slider item={control} {...props} />;
|
|
||||||
}
|
|
||||||
case "text": {
|
|
||||||
return <Text item={control} {...props} />;
|
|
||||||
}
|
|
||||||
case "progress": {
|
|
||||||
return <Progress item={control} {...props} />;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown UI type "${control.type}" for "${control.text}" component`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
src/components/UiItems/DropDown.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import map from "lodash/map";
|
||||||
|
import createComponent from "./base";
|
||||||
|
import { isDisabled, getValue } from "./utils";
|
||||||
|
|
||||||
|
import type { UIDropDown } from "config/flowtypes";
|
||||||
|
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import FormControl from "@mui/material/FormControl";
|
||||||
|
import InputLabel from "@mui/material/InputLabel";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Input from "@mui/material/Input";
|
||||||
|
|
||||||
|
const componentId = (item: UIDropDown) => `dropdown-${item.topic}`;
|
||||||
|
|
||||||
|
const DropDownOptions = (options) =>
|
||||||
|
map(options, (v, k) => <MenuItem value={k} key={k}>{v}</MenuItem>);
|
||||||
|
|
||||||
|
const onChangeEvent = (item: UIDropDown, changeState) =>
|
||||||
|
(event) => changeState(item, event.target.value);
|
||||||
|
|
||||||
|
const BaseComponent = ({Icon}, item, state, changeState) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Icon item={item} state={state} />
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel htmlFor={componentId(item)}>{item.text}</InputLabel>
|
||||||
|
<Select value={getValue(item, state)}
|
||||||
|
onChange={onChangeEvent(item, changeState)}
|
||||||
|
disabled={isDisabled(item, state)}
|
||||||
|
input={<Input id={componentId(item)} />}
|
||||||
|
>
|
||||||
|
{DropDownOptions(item.options)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
|
id: "dropDown",
|
||||||
|
name: "Drop Down",
|
||||||
|
desc: `
|
||||||
|
The Drop Down can be used to select from a small range of different options.
|
||||||
|
`,
|
||||||
|
parameters: {
|
||||||
|
text: "A descriptive label for the drop down",
|
||||||
|
topic: "The topic id"
|
||||||
|
},
|
||||||
|
baseComponent: BaseComponent
|
||||||
|
});
|
||||||
46
src/components/UiItems/Link.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import createComponent from "./base";
|
||||||
|
import { isEnabled, isDisabled } from "./utils";
|
||||||
|
|
||||||
|
import type { UILink } from "config/flowtypes";
|
||||||
|
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
|
const followLink = (item, state) => () => {
|
||||||
|
if (isEnabled(item, state)) {
|
||||||
|
return window.open(item.link, "_blank");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = ({item, state}) => {
|
||||||
|
if (item.icon == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.icon.render(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BaseComponent = (_h, item: UILink, state, _changeState) => (
|
||||||
|
<Button
|
||||||
|
variant="raised"
|
||||||
|
onClick={followLink(item, state)}
|
||||||
|
color="primary"
|
||||||
|
disabled={isDisabled(item, state)}
|
||||||
|
>
|
||||||
|
<Icon item={item} state={state} />
|
||||||
|
{item.text}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
|
id: "link",
|
||||||
|
name: "Link",
|
||||||
|
desc: `
|
||||||
|
The link is a button that opens a web page in a new tab.
|
||||||
|
`,
|
||||||
|
parameters: {
|
||||||
|
text: "A descriptive label for the link"
|
||||||
|
},
|
||||||
|
baseComponent: BaseComponent
|
||||||
|
});
|
||||||
38
src/components/UiItems/Progress.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import createComponent from "./base";
|
||||||
|
import { getValue } from "./utils";
|
||||||
|
|
||||||
|
import type { UIProgress } from "config/flowtypes";
|
||||||
|
|
||||||
|
import LinearProgress from "@mui/material/LinearProgress";
|
||||||
|
|
||||||
|
const progressVal = (item, state) => {
|
||||||
|
const min = item.min || 0;
|
||||||
|
const max = item.max || 100;
|
||||||
|
const val = parseFloat(getValue(item, state));
|
||||||
|
return val * 100 / max - min;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BaseComponent = ({Icon, Label}, item: UIProgress, state, _c) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Icon item={item} state={state} />
|
||||||
|
<Label />
|
||||||
|
<div style={{ flex: "10 1 auto" }} key="progressbar">
|
||||||
|
<LinearProgress variant="determinate" value={progressVal(item, state)} />
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
|
id: "progress",
|
||||||
|
name: "Progress Bar",
|
||||||
|
desc: `
|
||||||
|
The progress bar is used to display a progress value from MQTT
|
||||||
|
`,
|
||||||
|
parameters: {
|
||||||
|
text: "A descriptive label for the progress bar",
|
||||||
|
topic: "The topic id"
|
||||||
|
},
|
||||||
|
baseComponent: BaseComponent
|
||||||
|
});
|
||||||
23
src/components/UiItems/Section.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import createComponent from "./base";
|
||||||
|
|
||||||
|
import type { UISection } from "config/flowtypes";
|
||||||
|
|
||||||
|
import ListSubheader from "@mui/material/ListSubheader";
|
||||||
|
|
||||||
|
const BaseComponent = (_b, item: UISection, _state, _changeState) => (
|
||||||
|
<ListSubheader>{item.text}</ListSubheader>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
|
id: "section",
|
||||||
|
name: "Section",
|
||||||
|
desc: `
|
||||||
|
The section is a divider that can visually group components.
|
||||||
|
`,
|
||||||
|
parameters: {
|
||||||
|
text: "The text that is being displayed"
|
||||||
|
},
|
||||||
|
baseComponent: BaseComponent
|
||||||
|
});
|
||||||
40
src/components/UiItems/Slider.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import createComponent from "./base";
|
||||||
|
import { isDisabled, getValue } from "./utils";
|
||||||
|
|
||||||
|
import type { UISlider } from "config/flowtypes";
|
||||||
|
|
||||||
|
import SliderComponent from "@mui/material/Slider";
|
||||||
|
|
||||||
|
const changeSliderValue = (item: UISlider, changeState) => (_e, v) =>
|
||||||
|
changeState(item, v.toString());
|
||||||
|
|
||||||
|
const BaseComponent = ({Icon, Label}, item, state, changeState) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Icon item={item} state={state} />
|
||||||
|
<Label />
|
||||||
|
<SliderComponent
|
||||||
|
value={parseFloat(getValue(item, state))}
|
||||||
|
min={item.min ?? 0} max={item.max ?? 100}
|
||||||
|
step={item.step}
|
||||||
|
marks={item.marks ?? false}
|
||||||
|
onChange={changeSliderValue(item, changeState)}
|
||||||
|
disabled={isDisabled(item, state)}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
style={{marginLeft: 40}} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
|
id: "slider",
|
||||||
|
name: "Slider",
|
||||||
|
desc: `
|
||||||
|
The Slider can be used to choose a number between two a min and a max value.
|
||||||
|
`,
|
||||||
|
parameters: {
|
||||||
|
text: "A descriptive label for the slider",
|
||||||
|
topic: "The topic id"
|
||||||
|
},
|
||||||
|
baseComponent: BaseComponent
|
||||||
|
});
|
||||||
29
src/components/UiItems/Text.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import createComponent from "./base";
|
||||||
|
import { getValue } from "./utils";
|
||||||
|
|
||||||
|
import type { UIText } from "config/flowtypes";
|
||||||
|
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
|
||||||
|
const BaseComponent = ({Icon}, item: UIText, state, _changeState) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Icon item={item} state={state} />
|
||||||
|
<ListItemText key="label" secondary={item.text} />
|
||||||
|
<ListItemText key="vr" primary={getValue(item, state)} align="right" />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
|
id: "text",
|
||||||
|
name: "Text",
|
||||||
|
desc: `
|
||||||
|
The Text is used to display an MQTT value.
|
||||||
|
`,
|
||||||
|
parameters: {
|
||||||
|
text: "A descriptive label",
|
||||||
|
topic: "The topic id"
|
||||||
|
},
|
||||||
|
baseComponent: BaseComponent
|
||||||
|
});
|
||||||
53
src/components/UiItems/Toggle.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import createComponent from "./base";
|
||||||
|
import { isDisabled, isEnabled, getValue } from "./utils";
|
||||||
|
|
||||||
|
import type { UIToggle } from "config/flowtypes";
|
||||||
|
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
|
||||||
|
const isToggled = (item: UIToggle, state: State) => {
|
||||||
|
const isChecked = item.toggled ||
|
||||||
|
((i, _s) => i === (item.on || "on"));
|
||||||
|
const checked = isChecked(getValue(item, state), state);
|
||||||
|
return checked;
|
||||||
|
};
|
||||||
|
|
||||||
|
const doToggle = (item: UIToggle, state: State, changeState) => () => {
|
||||||
|
if (isEnabled(item, state)) {
|
||||||
|
const toggled = isToggled(item, state);
|
||||||
|
const on = item.on == null ? "on" : item.on;
|
||||||
|
const off = item.off == null ? "off" : item.off;
|
||||||
|
const next = toggled ? off : on;
|
||||||
|
return changeState(item, next);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BaseComponent = ({Icon, Label, Action}, item, state, changeState) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Icon item={item} state={state} />
|
||||||
|
<Label />
|
||||||
|
<Action>
|
||||||
|
<Switch label={item.text}
|
||||||
|
checked={isToggled(item, state)}
|
||||||
|
onChange={doToggle(item, state, changeState)}
|
||||||
|
disabled={isDisabled(item, state)}
|
||||||
|
color="primary" />
|
||||||
|
</Action>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
|
id: "toggle",
|
||||||
|
name: "Toggle Button",
|
||||||
|
desc: `
|
||||||
|
The toggle button can be used to toggle between two values.
|
||||||
|
`,
|
||||||
|
parameters: {
|
||||||
|
text: "A descriptive label for the toggle button",
|
||||||
|
topic: "The topic id"
|
||||||
|
},
|
||||||
|
baseComponent: BaseComponent
|
||||||
|
});
|
||||||
76
src/components/UiItems/base.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
import MqttContext from "mqtt/context";
|
||||||
|
|
||||||
|
import ListItemSecondaryAction from "@mui/material/ListItemSecondaryAction";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
|
||||||
|
import throttle from "lodash/throttle";
|
||||||
|
import type { Icon } from "config/icon";
|
||||||
|
|
||||||
|
export type Helpers = {
|
||||||
|
Icon: (props: { item: { +icon?: Icon }, state: State }) => React.Node,
|
||||||
|
Label: (props: {}) => React.Node,
|
||||||
|
Action: (props: {}) => React.Node
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BaseComponent<T> = (
|
||||||
|
helpers: Helpers,
|
||||||
|
item: T,
|
||||||
|
state: State,
|
||||||
|
nextValue: <T: { +topic: string }> (item: T, next: string) => void
|
||||||
|
) => React.Node;
|
||||||
|
|
||||||
|
export type Component<T> = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
desc: string,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Map<$Keys<T>, string> doesn't really work :(
|
||||||
|
* See https://github.com/facebook/flow/issues/5276
|
||||||
|
* If there is progress on the issue try to make it $Exact as well
|
||||||
|
*/
|
||||||
|
parameters: Map<$Keys<T>, string>,
|
||||||
|
baseComponent: BaseComponent<T>
|
||||||
|
};
|
||||||
|
|
||||||
|
type SuperT = $ReadOnly<{ text: string }>;
|
||||||
|
|
||||||
|
const IconHelper = ({item, state}: { item: { +icon?: Icon }, state: State }) =>
|
||||||
|
( <ListItemIcon>
|
||||||
|
{item.icon == null || item.icon.size(1).render(state)}
|
||||||
|
</ListItemIcon>
|
||||||
|
);
|
||||||
|
|
||||||
|
const createHelpers = <T: SuperT> (item: T) =>
|
||||||
|
({
|
||||||
|
Icon: IconHelper,
|
||||||
|
Label: () => (
|
||||||
|
<ListItemText primary={item.text} />
|
||||||
|
),
|
||||||
|
Action: (props) => (
|
||||||
|
<ListItemSecondaryAction {...props} />
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const debouncedChangeState = (chState: (tpc: string, nxt: string) => void) => (
|
||||||
|
throttle(<T: { +topic: string }> (item: T, next: string) =>
|
||||||
|
chState(item.topic, next), 50, {
|
||||||
|
leading: true,
|
||||||
|
trailing: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const createComponent = <T: SuperT> (component: Component<T>) => ({
|
||||||
|
component: (item: T) => (
|
||||||
|
<MqttContext.Consumer>
|
||||||
|
{({state, changeState}) => component.baseComponent(
|
||||||
|
createHelpers(item), item, state, debouncedChangeState(changeState)
|
||||||
|
)}
|
||||||
|
</MqttContext.Consumer>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createComponent;
|
||||||
44
src/components/UiItems/index.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// @flow
|
||||||
|
import Toggle from "./Toggle";
|
||||||
|
import DropDown from "./DropDown";
|
||||||
|
import Section from "./Section";
|
||||||
|
import Link from "./Link";
|
||||||
|
import Slider from "./Slider";
|
||||||
|
import Text from "./Text";
|
||||||
|
import Progress from "./Progress";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import type { ControlUI } from "config/flowtypes";
|
||||||
|
|
||||||
|
const Control = ({item}: {item: ControlUI}): React.Node => {
|
||||||
|
switch (item.type) {
|
||||||
|
case "toggle": {
|
||||||
|
return Toggle.component(item);
|
||||||
|
}
|
||||||
|
case "dropDown": {
|
||||||
|
return DropDown.component(item);
|
||||||
|
}
|
||||||
|
case "section": {
|
||||||
|
return Section.component(item);
|
||||||
|
}
|
||||||
|
case "link": {
|
||||||
|
return Link.component(item);
|
||||||
|
}
|
||||||
|
case "slider": {
|
||||||
|
return Slider.component(item);
|
||||||
|
}
|
||||||
|
case "text": {
|
||||||
|
return Text.component(item);
|
||||||
|
}
|
||||||
|
case "progress": {
|
||||||
|
return Progress.component(item);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(
|
||||||
|
`Unknown UI type "${item.type}" for "${item.text}" component`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Control;
|
||||||
27
src/components/UiItems/utils.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// @flow
|
||||||
|
import type { Enableable, UIControl } from "config/flowtypes";
|
||||||
|
|
||||||
|
export const getValue = <T: UIControl> (item: T, state: State) => {
|
||||||
|
const value = state[item.topic];
|
||||||
|
if (value == null) {
|
||||||
|
if (item.topic === "") {
|
||||||
|
throw new Error(
|
||||||
|
`Missing topic in ${item.type} "${item.text}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Unknown topic "${item.topic}" in ${item.type} "${item.text}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isEnabled = <T: Enableable> (item: T, state: State) => {
|
||||||
|
if (item.enableCondition != null) {
|
||||||
|
return item.enableCondition(state);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDisabled = <T: Enableable> (item: T, state: State) =>
|
||||||
|
!isEnabled(item, state);
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Color } from "config/colors";
|
|
||||||
import type { Icon } from "config/icon";
|
import type { Icon } from "config/icon";
|
||||||
|
|
||||||
export type TopicType = (msg: Buffer) => string;
|
export type TopicType = {
|
||||||
|
from: (msg: Buffer) => string,
|
||||||
|
to: (newstate: string) => Buffer
|
||||||
|
};
|
||||||
|
|
||||||
export type StateCommand = {
|
export type StateTopicType = TopicType | ((msg: Buffer) => string);
|
||||||
|
export type CommandTopicType = TopicType | ((newstate: string) => Buffer);
|
||||||
|
|
||||||
|
export type StateCommand<T> = {
|
||||||
name: string,
|
name: string,
|
||||||
type: TopicType
|
type: T
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Topic = {
|
export type Topic = {
|
||||||
state?: StateCommand,
|
state?: StateCommand<StateTopicType>,
|
||||||
command?: StateCommand,
|
command?: StateCommand<CommandTopicType>,
|
||||||
defaultValue: string
|
defaultValue: string
|
||||||
};
|
};
|
||||||
export type Topics = Map<string, Topic>;
|
export type Topics = Map<string, Topic>;
|
||||||
|
|
@ -22,9 +27,9 @@ export interface UIControl {
|
||||||
+topic: string
|
+topic: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Enableable {
|
export type Enableable = $ReadOnly<{
|
||||||
enableCondition?: (s: State) => boolean
|
enableCondition?: (s: State) => boolean
|
||||||
}
|
}>;
|
||||||
|
|
||||||
export type UIToggle = $ReadOnly<{|
|
export type UIToggle = $ReadOnly<{|
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
|
|
@ -53,9 +58,10 @@ export type UISlider = $ReadOnly<{|
|
||||||
topic: string,
|
topic: string,
|
||||||
icon?: Icon,
|
icon?: Icon,
|
||||||
enableCondition?: (s: State) => boolean,
|
enableCondition?: (s: State) => boolean,
|
||||||
|
marks?: boolean | Array<{ value: number, label: string}>,
|
||||||
min?: number,
|
min?: number,
|
||||||
max?: number,
|
max?: number,
|
||||||
step?: number
|
step?: ?number
|
||||||
|}>;
|
|}>;
|
||||||
|
|
||||||
export type UISection = $ReadOnly<{|
|
export type UISection = $ReadOnly<{|
|
||||||
|
|
@ -100,7 +106,6 @@ export type Control = {
|
||||||
name: string,
|
name: string,
|
||||||
position: [number, number],
|
position: [number, number],
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
iconColor?: (state: State) => Color,
|
|
||||||
ui: Array<ControlUI>
|
ui: Array<ControlUI>
|
||||||
};
|
};
|
||||||
export type Controls = Map<string, Control>;
|
export type Controls = Map<string, Control>;
|
||||||
|
|
@ -116,6 +121,17 @@ export type Space = {
|
||||||
mqtt: string
|
mqtt: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Layer = {
|
||||||
|
image: string,
|
||||||
|
name: string,
|
||||||
|
baseLayer?: boolean,
|
||||||
|
defaultVisibility: "visible" | "hidden",
|
||||||
|
opacity?: number,
|
||||||
|
bounds: {
|
||||||
|
topLeft: Point,
|
||||||
|
bottomRight: Point
|
||||||
|
}
|
||||||
|
};
|
||||||
export type Config = {
|
export type Config = {
|
||||||
space: Space,
|
space: Space,
|
||||||
topics: Topics | Array<Topics>,
|
topics: Topics | Array<Topics>,
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,88 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import React from "react";
|
||||||
import ReactContext from "mqtt/context";
|
import ReactIcon from "@mdi/react";
|
||||||
|
import { type Color } from "./colors";
|
||||||
|
import * as mdiIcons from "@mdi/js";
|
||||||
|
|
||||||
export opaque type RawIcon: string = string;
|
type IconPropHelper = {
|
||||||
|
size?: number,
|
||||||
export type Icon = (State) => RawIcon;
|
rotate?: number,
|
||||||
|
horizontal?: boolean,
|
||||||
export const rawMdi = (name: string): RawIcon => {
|
vertical?: boolean,
|
||||||
return `mdi ${name.split(" ").map((icon) => "mdi-".concat(icon)).join(" ")}`;
|
color?: Color
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mdi = (icon: string) => () => rawMdi(icon);
|
export type Icon = {
|
||||||
|
render: (s: State) => React.Node,
|
||||||
|
size: (n: number) => Icon,
|
||||||
|
rotate: (n: number) => Icon,
|
||||||
|
flip: () => Icon,
|
||||||
|
flipV: () => Icon,
|
||||||
|
color: (c: Color | (State) => Color) => Icon,
|
||||||
|
applyProps: (props: IconPropHelper) => Icon
|
||||||
|
};
|
||||||
|
|
||||||
export const mdiBattery = (topic: string) => (state: State) => {
|
const iconChainUtils = <T> (cb: (x: T, p?: IconPropHelper) => Icon,
|
||||||
|
p1: T, p?: IconPropHelper) => ({
|
||||||
|
size: (n: number) => cb(p1, {...p, size: n}),
|
||||||
|
rotate: (n: number) => cb(p1, {...p, rotate: n}),
|
||||||
|
flip: () => cb(p1, {...p, horizontal: !p?.horizontal ?? true}),
|
||||||
|
flipV: () => cb(p1, {...p, vertical: !p?.vertical ?? true}),
|
||||||
|
color: (c: Color | (State) => Color) => cb(p1, {...p, color: c}),
|
||||||
|
applyProps: (props: IconPropHelper) => cb(p1, {...p, ...props})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const svg = (data: string, props?: IconPropHelper): Icon => {
|
||||||
|
const propColor = ((c: ?Color | (State) => Color) => (state: State) => {
|
||||||
|
if (typeof c === "function") {
|
||||||
|
return c(state);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
})(props?.color);
|
||||||
|
return {
|
||||||
|
render: (state) => (
|
||||||
|
<ReactIcon path={data} size={props?.size ?? 1.5}
|
||||||
|
rotate={props?.rotate ?? 0}
|
||||||
|
horizontal={props?.horizontal ?? false}
|
||||||
|
vertical={props?.vertical ?? false}
|
||||||
|
color={propColor(state)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
...iconChainUtils(svg, data, props)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withState = (f: (s: State) => Icon,
|
||||||
|
props?: IconPropHelper): Icon => ({
|
||||||
|
render: (state) => f(state).applyProps(props).render(state),
|
||||||
|
...iconChainUtils(withState, f, props)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mdiBattery = (topic: string): Icon => withState((state) => {
|
||||||
const rawval = state[topic];
|
const rawval = state[topic];
|
||||||
const val = parseInt(rawval, 10);
|
const val = parseInt(rawval, 10);
|
||||||
if (isNaN(val)) {
|
if (isNaN(val)) {
|
||||||
return rawMdi("battery-unknown");
|
return svg(mdiIcons.mdiBatteryUnknown);
|
||||||
} else if (val > 95) {
|
} else if (val > 95) {
|
||||||
return rawMdi("battery");
|
return svg(mdiIcons.mdiBattery);
|
||||||
} else if (val > 85) {
|
} else if (val > 85) {
|
||||||
return rawMdi("battery-90");
|
return svg(mdiIcons.mdiBattery90);
|
||||||
} else if (val > 75) {
|
} else if (val > 75) {
|
||||||
return rawMdi("battery-80");
|
return svg(mdiIcons.mdiBattery80);
|
||||||
} else if (val > 65) {
|
} else if (val > 65) {
|
||||||
return rawMdi("battery-70");
|
return svg(mdiIcons.mdiBattery70);
|
||||||
} else if (val > 55) {
|
} else if (val > 55) {
|
||||||
return rawMdi("battery-60");
|
return svg(mdiIcons.mdiBattery60);
|
||||||
} else if (val > 45) {
|
} else if (val > 45) {
|
||||||
return rawMdi("battery-50");
|
return svg(mdiIcons.mdiBattery50);
|
||||||
} else if (val > 35) {
|
} else if (val > 35) {
|
||||||
return rawMdi("battery-40");
|
return svg(mdiIcons.mdiBattery40);
|
||||||
} else if (val > 25) {
|
} else if (val > 25) {
|
||||||
return rawMdi("battery-30");
|
return svg(mdiIcons.mdiBattery30);
|
||||||
} else if (val > 15) {
|
} else if (val > 15) {
|
||||||
return rawMdi("battery-20");
|
return svg(mdiIcons.mdiBattery20);
|
||||||
} else {
|
|
||||||
return rawMdi("battery-10");
|
|
||||||
}
|
}
|
||||||
};
|
return svg(mdiIcons.mdiBattery10);
|
||||||
|
});
|
||||||
export const renderRawIcon =
|
|
||||||
(icon: RawIcon, extraClass?: string): React.Node => {
|
|
||||||
return <i className={`${extraClass || ""} ${icon}`}></i>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renderIcon =
|
|
||||||
(icon: Icon, extraClass?: string): React.Node => {
|
|
||||||
return (
|
|
||||||
<ReactContext.Consumer>
|
|
||||||
{({state}) => renderRawIcon(icon(state), extraClass)}
|
|
||||||
</ReactContext.Consumer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { TopicType } from "config/flowtypes";
|
import type { TopicType } from "config/flowtypes";
|
||||||
import at from "lodash/at";
|
import at from "lodash/at";
|
||||||
|
import set from "lodash/set";
|
||||||
|
|
||||||
export const string: TopicType = (msg: Buffer) => msg.toString();
|
export const string: TopicType = {
|
||||||
|
from: (msg: Buffer) => msg.toString(),
|
||||||
|
to: (msg: string) => Buffer.from(msg)
|
||||||
|
};
|
||||||
|
|
||||||
export const json = (path: string, innerType?: TopicType): TopicType => {
|
export const json = (path: string, innerType?: TopicType): TopicType => {
|
||||||
const parseAgain = innerType == null ? (x) => x.toString() : innerType;
|
const parseAgain = innerType?.from ?? ((x) => x.toString());
|
||||||
return (msg) => parseAgain(Buffer.from(
|
const parseFirst = innerType?.to ?? ((x) => Buffer.from(x));
|
||||||
at(JSON.parse(msg.toString()), path)[0].toString()));
|
return {
|
||||||
|
from: (msg) => parseAgain(Buffer.from(
|
||||||
|
at(JSON.parse(msg.toString()), path)[0].toString())),
|
||||||
|
to: (msg) => Buffer.from(
|
||||||
|
JSON.stringify(set({}, path, parseFirst(msg).toString())))
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TypeOptionParam = { otherwise?: string, [string]: string };
|
export type TypeOptionParam = { otherwise?: string, [string]: string };
|
||||||
|
|
@ -16,13 +25,17 @@ export const option = (values: TypeOptionParam): TopicType => {
|
||||||
if (values.otherwise != null) {
|
if (values.otherwise != null) {
|
||||||
return values.otherwise;
|
return values.otherwise;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
return x;
|
||||||
`Value ${x.toString()} cannot by mapped by the option parameters given`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const mapVal = (x) => (values[x] != null ? values[x] : defaultValue(x));
|
const mapVal = (x) => (values[x] != null ? values[x] : defaultValue(x));
|
||||||
return (x) => mapVal(x.toString());
|
return {
|
||||||
|
from: (x) => mapVal(x.toString()),
|
||||||
|
to: (x) => Buffer.from(mapVal(x))
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const jsonArray = (msg: Buffer) => JSON.parse(msg.toString()).join(", ");
|
export const jsonArray = {
|
||||||
|
from: (msg: Buffer) => JSON.parse(msg.toString()).join(", "),
|
||||||
|
to: (msg: string) => Buffer.from(`[${msg}]`)
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default function connectMqtt(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return (topic: string, message: Buffer) => {
|
return (topic: string, message: Buffer) => {
|
||||||
client.publish(topic, message, null, (error) => {
|
client.publish(topic, message, {}, (error) => {
|
||||||
if (error == null && settings.onMessageSent != null) {
|
if (error == null && settings.onMessageSent != null) {
|
||||||
settings.onMessageSent(topic, message);
|
settings.onMessageSent(topic, message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import "core-js/stable";
|
||||||
|
import "regenerator-runtime/runtime";
|
||||||
|
import "../node_modules/leaflet/dist/leaflet.css";
|
||||||
|
import '@fontsource/roboto/300.css';
|
||||||
|
import '@fontsource/roboto/400.css';
|
||||||
|
import '@fontsource/roboto/500.css';
|
||||||
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
import App from "components/App";
|
import App from "components/App";
|
||||||
|
import { createTheme } from "@mui/material/styles";
|
||||||
|
import { ThemeProvider } from "@mui/styles";
|
||||||
|
import * as Colors from "@mui/material/colors";
|
||||||
|
|
||||||
import "../node_modules/@mdi/font/css/materialdesignicons.min.css";
|
|
||||||
import "../css/styles.css";
|
import "../css/styles.css";
|
||||||
|
|
||||||
import type { Config } from "config/flowtypes";
|
import type { Config } from "config/flowtypes";
|
||||||
|
|
@ -13,6 +23,21 @@ const config: Config = window.config;
|
||||||
|
|
||||||
document.title = `${config.space.name} Map`;
|
document.title = `${config.space.name} Map`;
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: Colors[config.space.color][500]
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: Colors.orange[500]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const contentElement: Element = document.getElementById("content");
|
const contentElement: Element = document.getElementById("content");
|
||||||
ReactDOM.render(<App config={config} />, contentElement);
|
ReactDOM.render((
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<App config={config} />
|
||||||
|
</ThemeProvider>
|
||||||
|
), contentElement);
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import React from "react";
|
||||||
|
|
||||||
export type MqttContextValue = {
|
export type MqttContextValue = {
|
||||||
state: State,
|
state: State,
|
||||||
changeState: (topic: string, value: string) => void
|
changeState: (topic: string, value: string) => State
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.createContext({
|
export default React.createContext({
|
||||||
state: {},
|
state: {},
|
||||||
changeState: (_topic, _val) => {}
|
changeState: (_topic, _val) => ({})
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,3 @@ declare type Classes = {
|
||||||
declare type State = Map<string,string>;
|
declare type State = Map<string,string>;
|
||||||
|
|
||||||
declare type Point = [number, number];
|
declare type Point = [number, number];
|
||||||
|
|
||||||
declare type Layer = {
|
|
||||||
image: string,
|
|
||||||
name: string,
|
|
||||||
baseLayer?: boolean,
|
|
||||||
defaultVisibility: "visible" | "hidden",
|
|
||||||
opacity?: number,
|
|
||||||
bounds: {
|
|
||||||
topLeft: Point,
|
|
||||||
bottomRight: Point
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const WebpackShellPlugin = require('webpack-shell-plugin');
|
const WebpackShellPlugin = require('webpack-shell-plugin-next');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-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']
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const configPath = env => {
|
const configPath = env => {
|
||||||
if (env === true) {
|
const filtered = Object.keys(env).filter((e) => !e.startsWith("WEBPACK"));
|
||||||
|
if (filtered.length < 1) {
|
||||||
throw "No config file was provided.";
|
throw "No config file was provided.";
|
||||||
}
|
}
|
||||||
return path.resolve(__dirname, `config/${env}`);
|
return path.resolve(__dirname, `config/${filtered[0]}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = env => ({
|
module.exports = env => ({
|
||||||
entry: {
|
entry: {
|
||||||
main: ["@babel/polyfill", configPath(env),
|
main: [configPath(env),
|
||||||
path.resolve(__dirname, 'src/index.jsx')]
|
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'],
|
||||||
alias: {
|
alias: {
|
||||||
'lodash': 'lodash-es'
|
'lodash': 'lodash-es',
|
||||||
}
|
"leaflet": "leaflet/dist/leaflet-src.esm.js"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
|
@ -34,16 +36,26 @@ module.exports = env => ({
|
||||||
rules: [
|
rules: [
|
||||||
// TODO: CSS follow imports and minify + sourcemap on production
|
// TODO: CSS follow imports and minify + sourcemap on production
|
||||||
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
|
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
|
||||||
{ test: /\.(woff2?|eot|ttf|svg)$/, loader: "file-loader" },
|
{ test: /\.(woff2?|eot|ttf|svg|png)$/, use: [ { loader: "file-loader", options: { esModule: false } } ] },
|
||||||
{ test: /\.js(x)?$/, loader: "babel-loader?cacheDirectory=true" }
|
{ test: /\.js(x)?$/, use: ["babel-loader?cacheDirectory=true"] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CleanWebpackPlugin(["dist"]),
|
new CleanWebpackPlugin(),
|
||||||
new WebpackShellPlugin({onBuildStart:preBuildScripts}),
|
new WebpackShellPlugin({
|
||||||
|
onBuildStart: {
|
||||||
|
scripts: preBuildScripts,
|
||||||
|
blocking: true,
|
||||||
|
parallel: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
title: 'Space Map',
|
title: 'Space Map',
|
||||||
template: 'index.ejs'
|
template: 'index.ejs'
|
||||||
}),
|
}),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
Buffer: ['buffer', 'Buffer'],
|
||||||
|
process: 'process/browser',
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
||||||