Compare commits

..

2 commits

Author SHA1 Message Date
greenkeeper[bot]
a917598270
chore(package): update lockfile yarn.lock 2020-01-10 07:44:32 +00:00
greenkeeper[bot]
724e1bc776
chore(package): update husky to version 4.0.6
Closes #133
2020-01-10 07:44:28 +00:00
37 changed files with 6137 additions and 6386 deletions

View file

@ -1,10 +1,7 @@
{
"presets": [
["@babel/preset-env", {
modules: false,
corejs: "3.6",
useBuiltIns: "entry",
targets: "last 3 years"
modules: false
}],
"@babel/preset-react",
"@babel/preset-flow"

View file

@ -7,8 +7,7 @@ module.exports = {
"extends": [
"eslint:recommended",
"plugin:flowtype/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {

View file

@ -10,8 +10,9 @@ types/types.js
types/mqtt.js
[options]
esproposal.export_star_as=enable
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=src
module.system.node.resolve_dirname=./src
[lints]
all=warn

View file

@ -4,6 +4,10 @@ node_js:
script:
- yarn travis
before_install: yarn global add greenkeeper-lockfile@1
before_script: greenkeeper-lockfile-update
after_script: greenkeeper-lockfile-upload
after_success:
- ./travis-upload-artifacts.sh

View file

@ -1,6 +1,7 @@
# MQTT Control Map
[![Build Status](https://travis-ci.org/uwap/mqtt-control-map.svg?branch=master)](https://travis-ci.org/uwap/mqtt-control-map)
[![Greenkeeper badge](https://badges.greenkeeper.io/uwap/mqtt-control-map.svg)](https://greenkeeper.io/)
## Development / Configuration
@ -11,6 +12,33 @@ your the mqtt control map for the given CONFIG everytime something changes.
for the given config.
4. run `yarn build CONFIG` to generate all files for production use.
## Documentation
## Config
The documentation can be found in our [mqtt-control-map wiki](https://github.com/uwap/mqtt-control-map/wiki).
See `config/`.
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. |

View file

@ -1,8 +1,8 @@
// @flow
import type { Config } from "config/flowtypes";
import { hex } from "config/colors";
import * as types from "config/types";
import * as icons from "@mdi/js";
import { svg } from "config/icon";
import { mdi } from "config/icon";
const config: Config = {
space: {
@ -32,19 +32,20 @@ const config: Config = {
hauptraumTableLight: {
name: "Hauptraum Tisch",
position: [450, 450],
icon: svg(icons.mdiWhiteBalanceIridescent),
icon: mdi("white-balance-iridescent"),
iconColor: () => hex("#000000"),
ui: [
{
type: "toggle",
text: "Licht",
topic: "hauptraumTableLight",
icon: svg(icons.mdiPower)
icon: mdi("power")
},
{
type: "toggle",
text: "Licht",
topic: "hauptraumTableLightOnHack",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
}

View file

@ -2,9 +2,8 @@
import type { Config } from "config/flowtypes";
import * as types from "config/types";
import { hex, rainbow } from "config/colors";
import { svg, withState } from "config/icon";
import { mdi, rawMdi } from "config/icon";
import { esper, tasmota } from "./utils";
import * as icons from "@mdi/js";
import * as onkyo from "./onkyo";
import * as olymp from "./olymp";
@ -226,35 +225,6 @@ const config: Config = {
type: types.option({ on: "ON", off: "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
@ -276,28 +246,29 @@ const config: Config = {
ledStahltrager: {
name: "LED Stahlträger",
position: [340, 590],
icon: svg(icons.mdiWhiteBalanceIridescent).color(({ledStahltraeger}) =>
(ledStahltraeger === "on" ? rainbow : hex("#000000"))),
icon: mdi("white-balance-iridescent"),
iconColor: ({ledStahltraeger}) =>
(ledStahltraeger === "on" ? rainbow : hex("#000000")),
ui: [
{
type: "toggle",
text: "Stahlträger LED",
topic: "ledStahltraeger",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
snackbar: {
name: "Snackbar",
position: [510, 500],
icon: svg(icons.mdiFridge).color(
tasmota.iconColor("snackbar", hex("#E20074"))),
icon: mdi("fridge"),
iconColor: tasmota.iconColor("snackbar", hex("#E20074")),
ui: [
{
type: "toggle",
text: "Snackbar",
topic: "snackbar",
icon: svg(icons.mdiPower)
icon: mdi("power")
},
{
type: "section",
@ -307,7 +278,7 @@ const config: Config = {
type: "text",
text: "LED-Streifen",
topic: "snackbarLedOnline",
icon: svg(icons.mdiWhiteBalanceIridescent)
icon: mdi("white-balance-iridescent")
},
{
type: "dropDown",
@ -326,7 +297,7 @@ const config: Config = {
"11": "Rainbow Pattern",
"12": "Fire Pattern"
},
icon: svg(icons.mdiCog),
icon: mdi("settings"),
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
},
{
@ -335,7 +306,7 @@ const config: Config = {
topic: "snackbarDimmmer",
min: 0,
max: 100,
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
},
{
@ -344,7 +315,7 @@ const config: Config = {
topic: "snackbarSpeed",
min: 0,
max: 20,
icon: svg(icons.mdiSpeedometer),
icon: mdi("speedometer"),
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
}
]
@ -352,151 +323,155 @@ const config: Config = {
twinkle: {
name: "Twinkle",
position: [530, 560],
icon: withState(({twinkle}) =>
(twinkle === "on" ? svg(icons.mdiLedOn).flipV().color(rainbow)
: svg(icons.mdiLedOff).flipV())
),
icon: ({twinkle}) =>
(twinkle === "on" ? rawMdi("led-on flip-v") : rawMdi("led-off flip-v")),
iconColor: ({twinkle}) => (twinkle === "on" ? rainbow : hex("#000000")),
ui: [
{
type: "toggle",
text: "Twinkle",
topic: "twinkle",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
fan: {
name: "Ventilator",
position: [530, 440],
icon: svg(icons.mdiFan).color(({fan}) =>
(fan === "on" ? hex("#00FF00") : hex("#000000"))),
icon: mdi("fan"),
iconColor: ({fan}) => (fan === "on" ? hex("#00FF00") : hex("#000000")),
ui: [
{
type: "toggle",
text: "Ventilator",
topic: "fan",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
cashdesk: {
name: "Cashdesk",
position: [510, 467],
icon: svg(icons.mdiCurrencyUsdCircle),
icon: mdi("coin"),
ui: [
{
type: "link",
link: "http://cashdesk.rzl:8000/",
text: "Open Cashdesk",
icon: svg(icons.mdiOpenInNew)
icon: mdi("open-in-new")
}
]
},
flyfry: {
name: "Fliegenbratgerät",
position: [450, 570],
icon: svg(icons.mdiFire).color(({flyfry}) =>
(flyfry === "on" ? hex("#6666FF") : hex("#000000"))),
icon: mdi("fire"),
iconColor: ({flyfry}) =>
(flyfry === "on" ? hex("#6666FF") : hex("#000000")),
ui: esper.statistics("flyfry", [
{
type: "toggle",
text: "Fliegenbratgerät",
topic: "flyfry",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
])
},
projector: {
name: "Beamer",
position: [380, 590],
icon: svg(icons.mdiProjector).flipV().color(({projector}) =>
icon: mdi("projector flip-v"),
iconColor: ({projector}) =>
({
transientOn: hex("#b3b300"),
transientOff: hex("#b3b300"),
on: hex("#00ff00"),
off: hex("#000000"),
unknown: hex("#888888")
})[projector]),
})[projector],
ui: [
{
type: "toggle",
text: "Beamer",
topic: "projector",
toggled: (val) => val === "transientOn" || val === "on",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
loetarbeitsplatz4: {
name: "Lötarbeitsplatz",
position: [205, 455],
icon: svg(icons.mdiEyedropperVariant).color(({loetarbeitsplatz4}) =>
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000"))),
icon: mdi("eyedropper-variant"),
iconColor: ({loetarbeitsplatz4}) =>
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000")),
ui: [
{
type: "text",
text: "Status",
topic: "loetarbeitsplatz4",
icon: svg(icons.mdiEyedropperVariant)
icon: mdi("eyedropper-variant")
}
]
},
loetarbeitsplatz5: {
name: "Lötarbeitsplatz",
position: [205, 405],
icon: svg(icons.mdiEyedropperVariant).color(({loetarbeitsplatz4}) =>
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000"))),
icon: mdi("eyedropper-variant"),
iconColor: ({loetarbeitsplatz5}) =>
(loetarbeitsplatz5 === "on" ? hex("#FF0000") : hex("#000000")),
ui: [
{
type: "text",
text: "Status",
topic: "loetarbeitsplatz5",
icon: svg(icons.mdiEyedropperVariant)
icon: mdi("eyedropper-variant")
}
]
},
door: {
name: "Tür",
position: [455, 350],
icon: svg(icons.mdiSwapVertical).color(({doorStatus}) =>
(doorStatus === "on" ? hex("#00FF00") : hex("#FF0000"))),
icon: mdi("swap-vertical"),
iconColor: ({doorStatus}) =>
(doorStatus === "on" ? hex("#00FF00") : hex("#FF0000")),
ui: [
{
type: "link",
link: "http://s.rzl.so",
text: "Open Status Page",
icon: svg(icons.mdiOpenInNew)
icon: mdi("open-in-new")
},
{
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)
icon: mdi("open-in-new")
},
{
type: "text",
text: "Anwesend",
topic: "presenceStatus",
icon: svg(icons.mdiAccount)
icon: mdi("account")
},
{
type: "text",
text: "Devices",
topic: "devicesStatus",
icon: svg(icons.mdiWifi)
icon: mdi("wifi")
},
{
type: "toggle",
text: "Deko",
topic: "deko",
icon: svg(icons.mdiInvertColors)
icon: mdi("invert-colors")
},
{
type: "text",
text: "Power Hauptraum",
topic: "powerConsumption",
icon: svg(icons.mdiSpeedometer)
icon: mdi("speedometer")
}
]
@ -504,56 +479,56 @@ const config: Config = {
infoscreen: {
name: "Infoscreen",
position: [255, 495],
icon: svg(icons.mdiTelevisionGuide).flipV().color(
tasmota.iconColor("infoscreen", hex("#4444FF"))
),
icon: mdi("television-guide flip-v"),
iconColor: tasmota.iconColor("infoscreen", hex("#4444FF")),
ui: [
{
type: "toggle",
text: "Infoscreen",
topic: "infoscreen",
icon: svg(icons.mdiPower)
icon: mdi("power")
},
{
type: "link",
link: "http://cashdesk.rzl:3030/rzl",
text: "Open Infoscreen",
icon: svg(icons.mdiOpenInNew)
icon: mdi("open-in-new")
}
]
},
pilze: {
name: "Pilze",
position: [48, 499],
icon: withState(({pilze}) =>
(pilze === "on" ? svg(icons.mdiLedOn) : svg(icons.mdiLedOff))).color(
tasmota.iconColor("pilze", rainbow)),
icon: ({pilze}) =>
(pilze === "on" ? rawMdi("led-on") : rawMdi("led-off")),
iconColor: tasmota.iconColor("pilze", rainbow),
ui: [
{
type: "toggle",
text: "Pilze",
topic: "pilze",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
printer3D: {
name: "Ultimaker 3",
position: [754, 560],
icon: svg(icons.mdiPrinter3d).color(({printer3DStatus}) =>
icon: mdi("printer-3d"),
iconColor: ({printer3DStatus}) =>
({
awaitingInteraction: hex("#b3b300"),
printing: hex("#00ff00"),
idle: hex("#000000"),
unavailable: hex("#888888"),
error: hex("#ff0000")
})[printer3DStatus]),
})[printer3DStatus],
ui: [
{
type: "link",
link: "http://ultimaker.rzl/",
text: "Open Webinterface",
icon: svg(icons.mdiOpenInNew)
icon: mdi("open-in-new")
},
{
type: "section",
@ -561,7 +536,7 @@ const config: Config = {
},
{
type: "progress",
icon: svg(icons.mdiRotateRight),
icon: mdi("rotate-right"),
min: 0,
max: 1,
text: "Printing Progress",
@ -570,7 +545,7 @@ const config: Config = {
{
type: "text",
text: "Time Left",
icon: svg(icons.mdiClock),
icon: mdi("clock"),
topic: "printer3Dremaining"
}
]
@ -578,87 +553,51 @@ const config: Config = {
partkeepr: {
name: "Partkeepr",
position: [48, 450],
icon: svg(icons.mdiChip),
icon: mdi("chip"),
ui: [
{
type: "link",
link: "http://partkeepr.rzl/",
text: "Open Partkeepr",
icon: svg(icons.mdiOpenInNew)
icon: mdi("open-in-new")
}
]
},
printerAnnette: {
name: "Drucker",
position: [800, 350],
icon: svg(icons.mdiPrinter).color(tasmota.iconColor("printerAnnette")),
icon: mdi("printer"),
iconColor: tasmota.iconColor("printerAnnette"),
ui: [
{
type: "toggle",
text: "Drucker",
topic: "printerAnnette",
icon: svg(icons.mdiPower)
icon: mdi("power")
},
{
type: "link",
link: "http://annette.rzl/",
text: "Open Annette",
icon: svg(icons.mdiOpenInNew)
icon: mdi("open-in-new")
}
]
},
nebenraumPowerStatus: {
name: "Strom Fablab",
position: [613, 537],
icon: withState(({nebenraumPowerStatus}) =>
(nebenraumPowerStatus === "on" ?
svg(icons.mdiFlash).color(hex("#00FF00")) : svg(icons.mdiFlashOff))),
icon: ({nebenraumPowerStatus}) =>
(nebenraumPowerStatus === "on" ? rawMdi("flash") : rawMdi("flash-off")),
iconColor: ({nebenraumPowerStatus}) =>
(nebenraumPowerStatus === "on" ? hex("#00ff00") : hex("#000000")),
ui: [
{
type: "text",
icon: svg(icons.mdiPower),
icon: mdi("power"),
text: "Strom Fablab",
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: [

View file

@ -1,10 +1,9 @@
// @flow
import type { Topics, Controls } from "config/flowtypes";
import { svg, mdiBattery } from "config/icon";
import { mdi, mdiBattery } from "config/icon";
import { hex } from "config/colors";
import * as types from "config/types";
import { floalt, tradfri, tasmota } from "./utils";
import * as icons from "@mdi/js";
export const topics: Topics = {
//Kuechen-Floalts
@ -66,7 +65,7 @@ export const controls: Controls = {
kitchenLight: {
name: "Deckenlicht Küche",
position: [325, 407],
icon: svg(icons.mdiCeilingLight),
icon: mdi("ceiling-light"),
ui: [
{
type: "toggle",
@ -75,14 +74,14 @@ export const controls: Controls = {
toggled: (n) => parseInt(n, 10) > 0,
topic: "kitchenLightBrightness",
text: "Ein/Ausschalten",
icon: svg(icons.mdiPower)
icon: mdi("power")
},
{
type: "slider",
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: "kitchenLightBrightness"
},
{
@ -90,7 +89,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: svg(icons.mdiWeatherSunsetDown),
icon: mdi("weather-sunset-down"),
topic: "kitchenLightColor"
},
{
@ -102,7 +101,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: floalt.brightness("65537")
},
{
@ -110,7 +109,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: svg(icons.mdiWeatherSunsetDown),
icon: mdi("weather-sunset-down"),
topic: floalt.color("65537")
},
{
@ -122,7 +121,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: floalt.brightness("65538")
},
{
@ -130,7 +129,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: svg(icons.mdiWeatherSunsetDown),
icon: mdi("weather-sunset-down"),
topic: floalt.color("65538")
},
{
@ -142,7 +141,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: floalt.brightness("65539")
},
{
@ -150,7 +149,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: svg(icons.mdiWeatherSunsetDown),
icon: mdi("weather-sunset-down"),
topic: floalt.color("65539")
},
{
@ -162,7 +161,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: floalt.brightness("65540")
},
{
@ -170,7 +169,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: svg(icons.mdiWeatherSunsetDown),
icon: mdi("weather-sunset-down"),
topic: floalt.color("65540")
}
]
@ -178,7 +177,7 @@ export const controls: Controls = {
kitchenSinkLight: {
name: "Licht Spüle",
position: [300, 345],
icon: svg(icons.mdiWallSconceFlat),
icon: mdi("wall-sconce-flat"),
ui: [
{
type: "toggle",
@ -187,14 +186,14 @@ export const controls: Controls = {
toggled: (n) => parseInt(n, 10) > 0,
topic: "kitchenSinkLightBrightness",
text: "Ein/Ausschalten",
icon: svg(icons.mdiPower)
icon: mdi("power")
},
{
type: "slider",
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: "kitchenSinkLightBrightness"
}
]
@ -202,7 +201,7 @@ export const controls: Controls = {
kitchenCounterLight: {
name: "Deckenlicht Theke",
position: [400, 440],
icon: svg(icons.mdiCeilingLight),
icon: mdi("ceiling-light"),
ui: [
{
type: "section",
@ -213,7 +212,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: floalt.brightness("65544")
},
{
@ -221,7 +220,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: svg(icons.mdiWeatherSunsetDown),
icon: mdi("weather-sunset-down"),
topic: floalt.color("65544")
},
{
@ -233,7 +232,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: svg(icons.mdiBrightness7),
icon: mdi("brightness-7"),
topic: floalt.brightness("65543")
},
{
@ -241,7 +240,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: svg(icons.mdiWeatherSunsetDown),
icon: mdi("weather-sunset-down"),
topic: floalt.color("65543")
}
]
@ -249,21 +248,21 @@ export const controls: Controls = {
lichtDunstabzug: {
name: "Licht Dunstabzugshaube",
position: [252, 405],
icon: svg(icons.mdiCeilingLight),
icon: mdi("ceiling-light"),
iconColor: tasmota.iconColor("lichtDunstabzug"),
ui: [
{
type: "toggle",
text: "Licht Dunstabzugshaube",
topic: "lichtDunstabzug",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
remotes: {
name: "Fernbedinungen",
position: [400, 344],
icon: svg(icons.mdiLightSwitch),
icon: mdi("light-switch"),
iconColor: (state) => //if any remote is low make icon red
(["65536", "65542", "65546", "65547"]
.some((x) => state[tradfri.remote.low(x)] === "true")

View file

@ -1,14 +1,11 @@
// @flow
import type { Topics, Controls } from "config/flowtypes";
import { svg } from "config/icon";
import { mdi } from "config/icon";
import { hex, rainbow } from "config/colors";
import * as types from "config/types";
import { tasmota, esper } from "./utils";
import * as icons from "@mdi/js";
export const topics: Topics = {
...tasmota.topics("8", "ledOlymp"),
...esper.topics("afba45", "alarm"),
videogames: {
state: {
name: "/service/openhab/out/pca301_videogames/state",
@ -41,70 +38,75 @@ export const topics: Topics = {
type: types.option({ on: "ON", off: "OFF" })
},
defaultValue: "off"
}
},
...tasmota.topics("8", "ledOlymp"),
...esper.topics("afba45", "alarm")
};
export const controls: Controls = {
ledOlymp: {
name: "LED Olymp",
position: [196, 154],
icon: svg(icons.mdiWhiteBalanceIridescent).rotate(45).color(
tasmota.iconColor("ledOlymp", rainbow)),
icon: mdi("white-balance-iridescent rotate-45"),
iconColor: tasmota.iconColor("ledOlymp", rainbow),
ui: [
{
type: "toggle",
text: "LED Olymp",
topic: "ledOlymp",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
videogames: {
name: "Videospiele",
position: [100, 100],
icon: svg(icons.mdiGamepadVariant).color(({videogames}) =>
(videogames === "on" ? hex("#00FF00") : hex("#000000"))),
icon: mdi("gamepad-variant"),
iconColor: ({videogames}) =>
(videogames === "on" ? hex("#00FF00") : hex("#000000")),
ui: [
{
type: "toggle",
text: "Videospiele",
topic: "videogames",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
olympPC: {
name: "Rechner",
position: [297, 90],
icon: svg(icons.mdiDesktopClassic).color(({olympPC}) =>
(olympPC === "on" ? hex("#00FF00") : hex("#000000"))),
icon: mdi("desktop-classic"),
iconColor: ({olympPC}) =>
(olympPC === "on" ? hex("#00FF00") : hex("#000000")),
ui: [
{
type: "toggle",
text: "Rechner",
topic: "olympPC",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
rundumleuchte: {
name: "Rundumleuchte",
position: [310, 275],
icon: svg(icons.mdiAlarmLight).color(({rundumleuchte}) =>
(rundumleuchte === "on" ? hex("#F0DF10") : hex("#000000"))),
icon: mdi("alarm-light"),
iconColor: ({rundumleuchte}) =>
(rundumleuchte === "on" ? hex("#F0DF10") : hex("#000000")),
ui: [
{
type: "toggle",
text: "Rundumleuchte",
topic: "rundumleuchte",
icon: svg(icons.mdiPower)
icon: mdi("power")
}
]
},
alarm: {
name: "Alarm",
position: [340, 250],
icon: svg(icons.mdiAlarmBell),
icon: mdi("alarm-bell"),
iconColor: () => hex("#000000"),
ui: esper.statistics("alarm")
}

View file

@ -1,9 +1,8 @@
// @flow
import type { Topics, Controls } from "config/flowtypes";
import { svg } from "config/icon";
import { mdi } from "config/icon";
import { hex } from "config/colors";
import * as types from "config/types";
import * as icons from "@mdi/js";
export const topics: Topics = {
onkyoConnection: {
@ -133,12 +132,12 @@ export const controls: Controls = {
iconColor: ({onkyoConnection, onkyoPower}) =>
(onkyoConnection !== "connected" ? hex("#888888") :
(onkyoPower === "on" ? hex("#00FF00") : hex("#000000"))),
icon: svg(icons.mdiAudioVideo),
icon: mdi("audio-video"),
ui: [
{
type: "toggle",
text: "Power",
icon: svg(icons.mdiPower),
icon: mdi("power"),
topic: "onkyoPower",
enableCondition: (state) => state.onkyoConnection === "connected"
},
@ -152,14 +151,14 @@ export const controls: Controls = {
topic: "onkyoVolume",
min: 0,
max: 50,
icon: svg(icons.mdiVolumeHigh),
icon: mdi("volume-high"),
enableCondition: (state) => state.onkyoConnection === "connected"
},
{
type: "toggle",
text: "Mute",
topic: "onkyoMute",
icon: svg(icons.mdiVolumeOff),
icon: mdi("volume-off"),
enableCondition: (state) => state.onkyoConnection === "connected"
},
{
@ -177,7 +176,7 @@ export const controls: Controls = {
pult: "Pult",
front: "Front HDMI"
},
icon: svg(icons.mdiUsb),
icon: mdi("usb"),
enableCondition: (state) => state.onkyoConnection === "connected"
},
{
@ -201,7 +200,7 @@ export const controls: Controls = {
somafmChrismasLounge: "Christmas Lounge (SomaFM)",
unknown: "Unknown"
},
icon: svg(icons.mdiRadio),
icon: mdi("radio"),
enableCondition: (state) => state.onkyoConnection === "connected"
&& state.onkyoInputs === "netzwerk"
},
@ -213,7 +212,7 @@ export const controls: Controls = {
type: "link",
link: "http://mpd.rzl/mpd/player/index.php",
text: "Open MPD Interface",
icon: svg(icons.mdiOpenInNew)
icon: mdi("open-in-new")
}
]
}

View file

@ -1,12 +1,11 @@
// @flow
import type { ControlUI, Topics } from "config/flowtypes";
import { svg } from "config/icon";
import { hex, type Color } from "config/colors";
import type { ControlUI } from "config/flowtypes";
import { mdi } from "config/icon";
import { hex } from "config/colors";
import * as types from "config/types";
import * as icons from "@mdi/js";
export const tasmota = {
topics: (id: string, name: string): Topics => ({
topics: (id: string, name: string) => ({
[name]: {
state: {
name: `stat/sonoff${id}/POWER`,
@ -27,20 +26,20 @@ export const tasmota = {
defaultValue: "off"
}
}),
iconColor: (name: string, onCol: Color = hex("#00FF00")): (State => Color) =>
(state: State): Color => {
iconColor: (name: string, onColor: Color = hex("#00FF00")) =>
(state: State) => {
if (state[`${name}_online`] === "off") {
return hex("#888888");
} else if (state[name] === "on") {
return onCol;
return onColor;
}
return hex("#000000");
}
};
export const floalt = {
color: (lightId: string): string => `floalt_${lightId}_color`,
brightness: (lightId: string): string => `floalt_${lightId}_brightness`,
topics: (lightId: string): Topics => ({
color: (lightId: string) => `floalt_${lightId}_color`,
brightness: (lightId: string) => `floalt_${lightId}_brightness`,
topics: (lightId: string) => ({
[`floalt_${lightId}_color`]: {
state: {
name: `/service/openhab/out/tradfri_0220_gwb8d7af2b448f_${lightId}` +
@ -71,9 +70,9 @@ export const floalt = {
};
const tradfriRemote = {
level: (remoteId: string): string => `tradfri_remote_${remoteId}_level`,
low: (remoteId: string): string => `tradfri_remote_${remoteId}_low`,
topics: (remoteId: string): Topics => ({
level: (remoteId: string) => `tradfri_remote_${remoteId}_level`,
low: (remoteId: string) => `tradfri_remote_${remoteId}_low`,
topics: (remoteId: string) => ({
[`tradfri_remote_${remoteId}_level`]: {
state: {
name: `/service/openhab/out/tradfri_0830_gwb8d7af2b448f_${remoteId}` +
@ -107,36 +106,36 @@ const esperStatistics = (name: string,
{
type: "text",
text: "Device Variant",
icon: svg(icons.mdiChartDonut),
icon: mdi("chart-donut"),
topic: `esper_${name}_device`
},
{
type: "text",
text: "Version",
icon: svg(icons.mdiSourceBranch),
icon: mdi("source-branch"),
topic: `esper_${name}_version`
},
{
type: "text",
text: "IP",
icon: svg(icons.mdiAccessPointNetwork),
icon: mdi("access-point-network"),
topic: `esper_${name}_ip`
},
{
type: "text",
text: "RSSI",
icon: svg(icons.mdiWifi),
icon: mdi("wifi"),
topic: `esper_${name}_rssi`
},
{
type: "text",
text: "Running since…",
icon: svg(icons.mdiAvTimer),
icon: mdi("av-timer"),
topic: `esper_${name}_uptime`
}
])
);
const esperTopics = (chipId: string, name: string): Topics => ({
const esperTopics = (chipId: string, name: string) => ({
[`esper_${name}_version`]: {
state: {
name: `/service/esper/${chipId}/info`,

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
<html>
<head>
<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>
</head>
<body>

View file

@ -1,10 +1,10 @@
{
"name": "mqtt-control-map",
"version": "1.0.0",
"author": "uwap <me@uwap.name>",
"description": "Control Devices via mqtt, visualized on a Map",
"author": "uwap <me+mqttmap.package.json@uwap.name>",
"description": "control devices via mqtt on a beautiful map of your space",
"scripts": {
"build": "webpack --bail --config webpack.config.js --mode production --env",
"build": "webpack --bail --config webpack.config.js -p --env",
"dev": "webpack --bail --config webpack.config.js --mode development --env",
"watch": "webpack-dev-server --open --config webpack.config.js --mode development --env",
"travis": "./travis.sh",
@ -12,52 +12,45 @@
"precommit": "yarn lint"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@fontsource/roboto": "^4.5.8",
"@mdi/react": "^1.4.0",
"@mui/material": "^5.10.15",
"@mui/styles": "^5.10.15",
"@material-ui/core": "^3.0.1",
"@material-ui/lab": "^3.0.0-alpha.16",
"@mdi/font": "^4.0.96",
"leaflet": "^1.5.1",
"mqtt": "^4.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.1.0"
"lodash-es": "^4.17.15",
"mqtt": "^3.0.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-leaflet": "^2.4.0",
"redux": "^4.0.4"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.5",
"@babel/preset-flow": "^7.0.0-rc.1",
"@babel/preset-react": "^7.0.0-rc.1",
"@mdi/js": "^7.0.96",
"babel-eslint": "^10.0.2",
"babel-loader": "^9.1.0",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.6.0",
"css-loader": "^6.7.2",
"eslint": "^8.28.0",
"eslint-plugin-flowtype": "^8.0.3",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^2.0.0",
"css-loader": "^3.0.0",
"eslint": "^6.1.0",
"eslint-plugin-flowtype": "^4.0.0",
"eslint-plugin-fp": "^2.3.0",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^4.1.2",
"file-loader": "^6.1.0",
"file-loader": "^5.0.0",
"flow": "^0.2.3",
"flow-bin": "^0.193.0",
"flow-typed": "^3.2.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.2",
"lodash-es": "^4.17.15",
"process": "^0.11.10",
"style-loader": "^3.3.1",
"url": "^0.11.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.1",
"webpack-shell-plugin-next": "^2.3.1"
"flow-bin": "^0.82.0",
"flow-typed": "^2.3.0",
"html-webpack-plugin": "^3.1.0",
"husky": "^4.0.6",
"style-loader": "^0.23.0",
"webpack": "^4.39.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2",
"webpack-shell-plugin": "^0.5.0"
},
"license": "MIT"
}

View file

@ -9,10 +9,13 @@ import throttle from "lodash/throttle";
import type { Config, Control, Topics } from "config/flowtypes";
import { withStyles } from "@mui/styles";
import Snackbar from "@mui/material/Snackbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider";
import createMuiTheme from "@material-ui/core/styles/createMuiTheme";
import withStyles from "@material-ui/core/styles/withStyles";
import * as Colors from "@material-ui/core/colors";
import 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 ControlMap from "components/ControlMap";
@ -36,14 +39,6 @@ export type AppState = {
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> {
controlMap: React.Node
@ -60,8 +55,7 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
onDisconnect: () => this.setState({ mqttConnected: false }),
subscribe: map(
filter(keys(this.topics), (x) => this.topics[x].state != null),
(x) => (this.topics[x].state != null ? this.topics[x].state.name : "")
)
(x) => this.topics[x].state.name)
}),
mqttConnected: false,
search: "",
@ -100,6 +94,14 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
};
}
static theme(config: Config) {
return createMuiTheme({
palette: {
primary: Colors[config.space.color]
}
});
}
receiveMessage(rawTopic: string, message: Buffer) {
try {
const topics = filter(
@ -113,8 +115,8 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
for (let i in topics) {
const topic = topics[i];
const stateTopic = this.topics[topic].state;
const typeConversion = stateTopic?.type?.from ?? stateTopic?.type;
const val = (typeConversion ?? ((x: Buffer) => x.toString()))(message);
const parseVal = stateTopic ? stateTopic.type : null;
const val = parseVal == null ? message.toString() : parseVal(message);
this.setMqttStateDebounced(
{mqttState: Object.assign({},
merge(this.state.mqttState, { [topic]: val}))});
@ -134,16 +136,16 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
this.setState({drawerOpened: false});
}
changeState = (topic: string, val: string) => {
changeState = (topic: string, value: string) => {
try {
const commandTopic = this.topics[topic].command;
if (commandTopic == null) {
if (this.topics[topic].command == null) {
return;
}
const rawTopic = commandTopic.name;
const typeConversion = commandTopic?.type?.to ?? commandTopic.type;
const value = (typeConversion ?? Buffer.from)(val);
this.state.mqttSend(rawTopic, value);
const rawTopic = this.topics[topic].command.name;
const transformValue = this.topics[topic].command.type;
const val =
transformValue == null ? value : transformValue(Buffer.from(value));
this.state.mqttSend(rawTopic, Buffer.from(val));
} catch (err) {
this.setState({ error: err.toString() });
}
@ -168,7 +170,7 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
control={this.state.selectedControl}
onCloseRequest={this.closeDrawer}
icon={this.state.selectedControl == null ? null :
this.state.selectedControl.icon.render(this.state.mqttState)}
this.state.selectedControl.icon(this.state.mqttState)}
>
{this.state.selectedControl == null
|| <UiItemList controls={this.state.selectedControl.ui} />}
@ -202,6 +204,11 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
}
}
export default withStyles(App.styles)(App);
export default (props: AppProps) => {
const StyledApp = withStyles(App.styles)(App);
return (
<MuiThemeProvider theme={App.theme(props.config)}>
<StyledApp {...props} />
</MuiThemeProvider>
);
};

View file

@ -1,15 +1,12 @@
// @flow
import React from "react";
import { MapContainer, ImageOverlay, Marker, LayersControl } from "react-leaflet";
import { Map, ImageOverlay, Marker, LayersControl } from "react-leaflet";
import { CRS, point, divIcon } from "leaflet";
import map from "lodash/map";
import filter from "lodash/filter";
import reduce from "lodash/reduce";
import MqttContext from "mqtt/context";
import type {
Controls, Control, UIControl, ControlUI, Layer
} from "config/flowtypes";
import { renderToString } from "react-dom/server";
import type { Controls, Control, UIControl, ControlUI } from "config/flowtypes";
export type Point = [number, number];
@ -31,11 +28,21 @@ const center = (props: ControlMapProps): Point =>
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 icon = control.icon(state);
const iconClass = `${icon} mdi-36px`;
return divIcon({
iconSize: point(36, 36),
iconAnchor: point(18, 18),
html: renderToString(control.icon.render(state))
html: `<i class="${iconClass}"
style="line-height: 1; color: ${iconColor(control, state)}"></i>`
});
};
@ -45,9 +52,7 @@ const renderMarker = (props: ControlMapProps) =>
{({ state }) => (
<Marker position={convertPoint(control.position)}
icon={createLeafletIcon(control, state)}
eventHandlers={{
click: () => props.onChangeControl(control)
}}
onClick={() => props.onChangeControl(control)}
>
</Marker>
)}
@ -120,13 +125,13 @@ const renderLayers = (props: ControlMapProps) => (
);
const ControlMap = (props: ControlMapProps) => (
<MapContainer center={center(props)}
<Map center={center(props)}
zoom={props.zoom}
crs={CRS.Simple}
leaflet={{}}>
{renderMarkers(props)}
{renderLayers(props)}
</MapContainer>
</Map>
);
export default ControlMap;

View file

@ -1,63 +1,62 @@
// @flow
import * as React from "react";
import { makeStyles } from "@mui/styles";
import Drawer from "@mui/material/Drawer";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import List from "@mui/material/List";
import ReactIcon from "@mdi/react";
import { mdiClose } from "@mdi/js";
import withStyles from "@material-ui/core/styles/withStyles";
import Drawer from "@material-ui/core/Drawer";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import List from "@material-ui/core/List";
import { renderRawIcon } from "config/icon";
import type { RawIcon } from "config/icon";
import type { Control } from "config/flowtypes";
export type SideBarProps = {
control: ?Control,
open: boolean,
onCloseRequest: () => void,
icon?: ?React.Node,
icon?: ?RawIcon,
children?: React.Node
};
const useStyles = makeStyles((theme) => ({
type Props = SideBarProps & Classes;
const SideBar = (props: Props) => (
<Drawer open={props.open}
anchor="right"
onClose={props.onCloseRequest}
classes={{paper: props.classes.drawerPaper}}
variant="persistent"
>
<AppBar position="static">
<Toolbar>
<span>
{props.icon == null || renderRawIcon(props.icon, "mdi-36px")}
</span>
<Typography variant="title" className={props.classes.title}>
{props.control == null ? "" : props.control.name}
</Typography>
<IconButton onClick={props.onCloseRequest}>
<i className="mdi mdi-close"></i>
</IconButton>
</Toolbar>
</AppBar>
<List id="drawer_uiComponents">
<React.Fragment>{props.children}</React.Fragment>
</List>
</Drawer>
);
const styles = (theme) => ({
drawerPaper: {
width: 340
},
title: {
flex: 1,
marginLeft: theme.spacing(1)
marginLeft: theme.spacing.unit
}
}));
});
const SideBar = (props: SideBarProps) => {
const classes = useStyles();
return (
<Drawer open={props.open}
anchor="right"
onClose={props.onCloseRequest}
classes={{paper: classes.drawerPaper}}
variant="persistent"
>
<AppBar position="static">
<Toolbar>
<span>
{props.icon == null || props.icon}
</span>
<Typography variant="subtitle1" className={classes.title}>
{props.control == null ? "" : props.control.name}
</Typography>
<IconButton onClick={props.onCloseRequest}>
<ReactIcon path={mdiClose} size={1.5} />
</IconButton>
</Toolbar>
</AppBar>
<List id="drawer_uiComponents">
<React.Fragment>{props.children}</React.Fragment>
</List>
</Drawer>
);
};
export default SideBar;
export default withStyles(styles)(SideBar);

View file

@ -1,16 +1,14 @@
// @flow
import React from "react";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import CircularProgress from "@mui/material/CircularProgress";
import InputBase from "@mui/material/InputBase";
import { styled } from "@mui/styles";
import { alpha } from "@mui/material/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";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import CircularProgress from "@material-ui/core/CircularProgress";
import InputBase from "@material-ui/core/InputBase";
import { fade } from "@material-ui/core/styles/colorManipulator";
import { withStyles } from "@material-ui/core/styles";
import Tooltip from "@material-ui/core/Tooltip";
import IconButton from "@material-ui/core/IconButton";
export type TopBarProps = {
connected: boolean,
@ -23,74 +21,89 @@ export type SearchBarProps = {
const renderConnectionIndicator = (connected: boolean) => {
if (connected) {
return (<ReactIcon path={mdiMap} size={2} />);
return (<i style={{fontSize: 32}} className="mdi mdi-map"></i>);
}
return (
<CircularProgress size={32} style={{color: "rgba(0, 0, 0, 0.54)"}} />
);
};
const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing(2),
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(3),
width: 'auto',
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
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 searchStyles = (theme) => ({
search: {
position: "relative",
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
"&:hover": {
backgroundColor: fade(theme.palette.common.white, 0.25)
},
marginRight: theme.spacing.unit * 2,
marginLeft: 0,
width: "100%",
[theme.breakpoints.up("sm")]: {
marginLeft: theme.spacing.unit * 3,
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) => (
<div className={props.classes.search}>
<i className={`mdi mdi-magnify ${props.classes.searchIcon}`}></i>
<InputBase placeholder="Search…" type="search"
onChange={(e) => props.onSearch(e.target.value)}
classes={{
root: props.classes.inputRoot,
input: props.classes.inputInput
}} />
</div>
);
const Search = withStyles(searchStyles)(RawSearch);
const openOnGithub = () => window.open(
"https://github.com/uwap/mqtt-control-map", "_blank");
const sendFeedback = () => window.open("mailto:mail+feedback@uwap.name");
const TopBar = (props: TopBarProps) => (
<AppBar position="static" color="primary">
<AppBar position="static">
<Toolbar>
{renderConnectionIndicator(props.connected)}
<Search>
<SearchIconWrapper>
<ReactIcon path={mdiMagnify} size={1} />
</SearchIconWrapper>
<StyledInputBase
placeholder="Search…"
inputProps={{ 'aria-label': 'search' }}
onChange={(e) => props.onSearch(e.target.value)}
/>
</Search>
<Search onSearch={props.onSearch} />
<span style={{flex: 1}}></span>
<Tooltip title="View on Github">
<IconButton onClick={openOnGithub}>
<ReactIcon path={mdiGithub} size={1.5} />
<Tooltip title="Github">
<IconButton onClick={openOnGithub} style={{ fontSize: "28px" }}>
<i className="mdi mdi-github-circle"></i>
</IconButton>
</Tooltip>
<Tooltip title="Send me feedback">
<IconButton onClick={sendFeedback} style={{ fontSize: "28px" }}>
<i className="mdi mdi-email-plus"></i>
</IconButton>
</Tooltip>
</Toolbar>

View file

@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import ListItem from "@mui/material/ListItem";
import ListItem from "@material-ui/core/ListItem";
import type { ControlUI } from "config/flowtypes";

View file

@ -6,11 +6,11 @@ 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";
import Select from "@material-ui/core/Select";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Input from "@material-ui/core/Input";
const componentId = (item: UIDropDown) => `dropdown-${item.topic}`;

View file

@ -2,10 +2,11 @@
import React from "react";
import createComponent from "./base";
import { isEnabled, isDisabled } from "./utils";
import { renderRawIcon } from "config/icon";
import type { UILink } from "config/flowtypes";
import Button from "@mui/material/Button";
import Button from "@material-ui/core/Button";
const followLink = (item, state) => () => {
if (isEnabled(item, state)) {
@ -18,7 +19,7 @@ const Icon = ({item, state}) => {
if (item.icon == null) {
return false;
}
return item.icon.render(state);
return renderRawIcon(item.icon(state), "mdi-24px");
};
const BaseComponent = (_h, item: UILink, state, _changeState) => (

View file

@ -5,7 +5,7 @@ import { getValue } from "./utils";
import type { UIProgress } from "config/flowtypes";
import LinearProgress from "@mui/material/LinearProgress";
import LinearProgress from "@material-ui/core/LinearProgress";
const progressVal = (item, state) => {
const min = item.min || 0;

View file

@ -4,7 +4,7 @@ import createComponent from "./base";
import type { UISection } from "config/flowtypes";
import ListSubheader from "@mui/material/ListSubheader";
import ListSubheader from "@material-ui/core/ListSubheader";
const BaseComponent = (_b, item: UISection, _state, _changeState) => (
<ListSubheader>{item.text}</ListSubheader>

View file

@ -5,7 +5,7 @@ import { isDisabled, getValue } from "./utils";
import type { UISlider } from "config/flowtypes";
import SliderComponent from "@mui/material/Slider";
import SliderComponent from "@material-ui/lab/Slider";
const changeSliderValue = (item: UISlider, changeState) => (_e, v) =>
changeState(item, v.toString());
@ -16,12 +16,10 @@ const BaseComponent = ({Icon, Label}, item, state, changeState) => (
<Label />
<SliderComponent
value={parseFloat(getValue(item, state))}
min={item.min ?? 0} max={item.max ?? 100}
step={item.step}
marks={item.marks ?? false}
min={item.min || 0} max={item.max || 100}
step={item.step || 1}
onChange={changeSliderValue(item, changeState)}
disabled={isDisabled(item, state)}
valueLabelDisplay="auto"
style={{marginLeft: 40}} />
</React.Fragment>
);
@ -38,3 +36,5 @@ export default createComponent({
},
baseComponent: BaseComponent
});

View file

@ -5,7 +5,7 @@ import { getValue } from "./utils";
import type { UIText } from "config/flowtypes";
import ListItemText from "@mui/material/ListItemText";
import ListItemText from "@material-ui/core/ListItemText";
const BaseComponent = ({Icon}, item: UIText, state, _changeState) => (
<React.Fragment>

View file

@ -5,7 +5,7 @@ import { isDisabled, isEnabled, getValue } from "./utils";
import type { UIToggle } from "config/flowtypes";
import Switch from "@mui/material/Switch";
import Switch from "@material-ui/core/Switch";
const isToggled = (item: UIToggle, state: State) => {
const isChecked = item.toggled ||

View file

@ -2,17 +2,18 @@
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 ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import throttle from "lodash/throttle";
import { renderRawIcon } from "config/icon";
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
Icon: (props: Object) => React.Node,
Label: (props: Object) => React.Node,
Action: (props: Object) => React.Node
};
export type BaseComponent<T> = (
@ -40,15 +41,15 @@ type SuperT = $ReadOnly<{ text: string }>;
const IconHelper = ({item, state}: { item: { +icon?: Icon }, state: State }) =>
( <ListItemIcon>
{item.icon == null || item.icon.size(1).render(state)}
{item.icon == null || renderRawIcon(item.icon(state), "mdi-24px")}
</ListItemIcon>
);
const createHelpers = <T: SuperT> (item: T) =>
({
Icon: IconHelper,
Label: () => (
<ListItemText primary={item.text} />
Label: (props) => (
<ListItemText primary={item.text} {...props} />
),
Action: (props) => (
<ListItemSecondaryAction {...props} />

View file

@ -6,11 +6,10 @@ 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 => {
const Control = ({item}: {item: ControlUI}) => {
switch (item.type) {
case "toggle": {
return Toggle.component(item);

View file

@ -1,22 +1,17 @@
// @flow
import type { Color } from "config/colors";
import type { Icon } from "config/icon";
export type TopicType = {
from: (msg: Buffer) => string,
to: (newstate: string) => Buffer
};
export type TopicType = (msg: Buffer) => string;
export type StateTopicType = TopicType | ((msg: Buffer) => string);
export type CommandTopicType = TopicType | ((newstate: string) => Buffer);
export type StateCommand<T> = {
export type StateCommand = {
name: string,
type: T
type: TopicType
}
export type Topic = {
state?: StateCommand<StateTopicType>,
command?: StateCommand<CommandTopicType>,
state?: StateCommand,
command?: StateCommand,
defaultValue: string
};
export type Topics = Map<string, Topic>;
@ -58,10 +53,9 @@ export type UISlider = $ReadOnly<{|
topic: string,
icon?: Icon,
enableCondition?: (s: State) => boolean,
marks?: boolean | Array<{ value: number, label: string}>,
min?: number,
max?: number,
step?: ?number
step?: number
|}>;
export type UISection = $ReadOnly<{|
@ -106,6 +100,7 @@ export type Control = {
name: string,
position: [number, number],
icon: Icon,
iconColor?: (state: State) => Color,
ui: Array<ControlUI>
};
export type Controls = Map<string, Control>;
@ -121,17 +116,6 @@ export type Space = {
mqtt: string
};
export type Layer = {
image: string,
name: string,
baseLayer?: boolean,
defaultVisibility: "visible" | "hidden",
opacity?: number,
bounds: {
topLeft: Point,
bottomRight: Point
}
};
export type Config = {
space: Space,
topics: Topics | Array<Topics>,

View file

@ -1,88 +1,55 @@
// @flow
import React from "react";
import ReactIcon from "@mdi/react";
import { type Color } from "./colors";
import * as mdiIcons from "@mdi/js";
import * as React from "react";
import ReactContext from "mqtt/context";
type IconPropHelper = {
size?: number,
rotate?: number,
horizontal?: boolean,
vertical?: boolean,
color?: Color
export opaque type RawIcon: string = string;
export type Icon = (State) => RawIcon;
export const rawMdi = (name: string): RawIcon => {
return `mdi ${name.split(" ").map((icon) => "mdi-".concat(icon)).join(" ")}`;
};
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 mdi = (icon: string) => () => rawMdi(icon);
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) => {
export const mdiBattery = (topic: string) => (state: State) => {
const rawval = state[topic];
const val = parseInt(rawval, 10);
if (isNaN(val)) {
return svg(mdiIcons.mdiBatteryUnknown);
return rawMdi("battery-unknown");
} else if (val > 95) {
return svg(mdiIcons.mdiBattery);
return rawMdi("battery");
} else if (val > 85) {
return svg(mdiIcons.mdiBattery90);
return rawMdi("battery-90");
} else if (val > 75) {
return svg(mdiIcons.mdiBattery80);
return rawMdi("battery-80");
} else if (val > 65) {
return svg(mdiIcons.mdiBattery70);
return rawMdi("battery-70");
} else if (val > 55) {
return svg(mdiIcons.mdiBattery60);
return rawMdi("battery-60");
} else if (val > 45) {
return svg(mdiIcons.mdiBattery50);
return rawMdi("battery-50");
} else if (val > 35) {
return svg(mdiIcons.mdiBattery40);
return rawMdi("battery-40");
} else if (val > 25) {
return svg(mdiIcons.mdiBattery30);
return rawMdi("battery-30");
} else if (val > 15) {
return svg(mdiIcons.mdiBattery20);
return rawMdi("battery-20");
} 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>
);
};

View file

@ -1,22 +1,13 @@
// @flow
import type { TopicType } from "config/flowtypes";
import at from "lodash/at";
import set from "lodash/set";
export const string: TopicType = {
from: (msg: Buffer) => msg.toString(),
to: (msg: string) => Buffer.from(msg)
};
export const string: TopicType = (msg: Buffer) => msg.toString();
export const json = (path: string, innerType?: TopicType): TopicType => {
const parseAgain = innerType?.from ?? ((x) => x.toString());
const parseFirst = innerType?.to ?? ((x) => Buffer.from(x));
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())))
};
const parseAgain = innerType == null ? (x) => x.toString() : innerType;
return (msg) => parseAgain(Buffer.from(
at(JSON.parse(msg.toString()), path)[0].toString()));
};
export type TypeOptionParam = { otherwise?: string, [string]: string };
@ -25,17 +16,13 @@ export const option = (values: TypeOptionParam): TopicType => {
if (values.otherwise != null) {
return values.otherwise;
} else {
return x;
throw new Error(
`Value ${x.toString()} cannot by mapped by the option parameters given`
);
}
};
const mapVal = (x) => (values[x] != null ? values[x] : defaultValue(x));
return {
from: (x) => mapVal(x.toString()),
to: (x) => Buffer.from(mapVal(x))
};
return (x) => mapVal(x.toString());
};
export const jsonArray = {
from: (msg: Buffer) => JSON.parse(msg.toString()).join(", "),
to: (msg: string) => Buffer.from(`[${msg}]`)
};
export const jsonArray = (msg: Buffer) => JSON.parse(msg.toString()).join(", ");

View file

@ -49,7 +49,7 @@ export default function connectMqtt(
}
});
return (topic: string, message: Buffer) => {
client.publish(topic, message, {}, (error) => {
client.publish(topic, message, null, (error) => {
if (error == null && settings.onMessageSent != null) {
settings.onMessageSent(topic, message);
}

View file

@ -1,20 +1,10 @@
// @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 ReactDOM from "react-dom";
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 type { Config } from "config/flowtypes";
@ -23,21 +13,6 @@ const config: Config = window.config;
document.title = `${config.space.name} Map`;
const theme = createTheme({
palette: {
primary: {
main: Colors[config.space.color][500]
},
secondary: {
main: Colors.orange[500]
}
}
});
// $FlowFixMe
const contentElement: Element = document.getElementById("content");
ReactDOM.render((
<ThemeProvider theme={theme}>
<App config={config} />
</ThemeProvider>
), contentElement);
ReactDOM.render(<App config={config} />, contentElement);

View file

@ -3,10 +3,10 @@ import React from "react";
export type MqttContextValue = {
state: State,
changeState: (topic: string, value: string) => State
changeState: (topic: string, value: string) => void
};
export default React.createContext({
state: {},
changeState: (_topic, _val) => ({})
changeState: (_topic, _val) => {}
});

View file

@ -10,3 +10,15 @@ declare type Classes = {
declare type State = Map<string,string>;
declare type Point = [number, number];
declare type Layer = {
image: string,
name: string,
baseLayer?: boolean,
defaultVisibility: "visible" | "hidden",
opacity?: number,
bounds: {
topLeft: Point,
bottomRight: Point
}
};

View file

@ -1,32 +1,30 @@
const path = require('path');
const webpack = require('webpack');
const WebpackShellPlugin = require('webpack-shell-plugin-next');
const WebpackShellPlugin = require('webpack-shell-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 ?
process.env.FLOW_PATH != undefined ? [process.env.FLOW_PATH] : ['flow']
: [];
const configPath = env => {
const filtered = Object.keys(env).filter((e) => !e.startsWith("WEBPACK"));
if (filtered.length < 1) {
if (env === true) {
throw "No config file was provided.";
}
return path.resolve(__dirname, `config/${filtered[0]}`);
return path.resolve(__dirname, `config/${env}`);
};
module.exports = env => ({
entry: {
main: [configPath(env),
main: ["@babel/polyfill", configPath(env),
path.resolve(__dirname, 'src/index.jsx')]
},
resolve: {
modules: [path.resolve(__dirname, "src"), "node_modules"],
extensions: ['.js', '.jsx'],
alias: {
'lodash': 'lodash-es',
"leaflet": "leaflet/dist/leaflet-src.esm.js"
},
'lodash': 'lodash-es'
}
},
output: {
path: path.resolve(__dirname, 'dist'),
@ -36,26 +34,16 @@ module.exports = env => ({
rules: [
// TODO: CSS follow imports and minify + sourcemap on production
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
{ test: /\.(woff2?|eot|ttf|svg|png)$/, use: [ { loader: "file-loader", options: { esModule: false } } ] },
{ test: /\.js(x)?$/, use: ["babel-loader?cacheDirectory=true"] }
{ test: /\.(woff2?|eot|ttf|svg)$/, loader: "file-loader" },
{ test: /\.js(x)?$/, loader: "babel-loader?cacheDirectory=true" }
]
},
plugins: [
new CleanWebpackPlugin(),
new WebpackShellPlugin({
onBuildStart: {
scripts: preBuildScripts,
blocking: true,
parallel: false
}
}),
new WebpackShellPlugin({onBuildStart:preBuildScripts}),
new HtmlWebpackPlugin({
title: 'Space Map',
template: 'index.ejs'
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser',
}),
]
});

9898
yarn.lock

File diff suppressed because it is too large Load diff