Compare commits

..

121 commits

Author SHA1 Message Date
b1c59d866a Fix watch command not working 2022-11-23 12:07:32 +01:00
3528e8179d
Merge pull request #177 from Ranlvor/patch-41
Various uwap-home changes
2022-11-23 11:45:37 +01:00
a67aaade0a
Merge pull request #155 from uwap/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.20 to 4.17.21
2022-11-23 11:36:25 +01:00
dependabot[bot]
bb8f5be644
Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-23 10:32:54 +00:00
dd198d8aaa Update webpack and leaflet 2022-11-23 11:31:40 +01:00
60178de93b Update dependencies 2022-11-22 21:23:53 +01:00
e89fd10c23 Update to mui v5 2022-11-22 21:21:06 +01:00
6f3fb4dc0a Update dependencies 2022-11-22 20:13:13 +01:00
Ranlvor
8e002e3b1a
add button for wled office auto on/off 2022-11-15 17:56:52 +01:00
Ranlvor
58c9ae1125
remove debugstatement accidently left in 2022-10-29 13:15:58 +02:00
Ranlvor
c2ed2fd37a
add option to disable auto-reclose on blinds 2022-10-29 13:15:01 +02:00
Ranlvor
eb7d1f7c5d
Addjust blind icon threshold to maximum window-open-save extention 2022-09-06 00:16:39 +02:00
Ranlvor
b6c7ba89cb
show office blinds position in icon 2022-09-05 21:40:51 +02:00
Ranlvor
14d72d626c add office blinds 2022-08-20 12:50:05 +02:00
Ranlvor
d73ee95178
add fountain 2022-08-17 14:15:58 +02:00
Ranlvor
fa8d403076
two more boolean home-rust-settings 2022-04-22 13:14:22 +02:00
Ranlvor
38de936bdd
+soundbar 2022-04-21 21:06:49 +02:00
Ranlvor
060dfa61d8
add link to kodi webinterface 2022-01-28 23:34:50 +01:00
Ranlvor
ca2bef4248
office: +color_temp 2022-01-18 19:23:20 +01:00
Ranlvor
f2f5e3441b
add kittchen bulbs 2022-01-18 16:02:08 +01:00
Ranlvor
eab431f13b
show office led lwt
because it takes a few seconds to respond to a brightness request when current state ist standby, but it immediatly leaves standby giving feedback that something is happening
2022-01-15 14:37:46 +01:00
Ranlvor
53911db686
+wled office 2022-01-13 21:43:18 +01:00
Ranlvor
51d410fbd3
Webinterface Bulb Office 2021-12-21 19:26:02 +01:00
Ranlvor
4d39149e77
bulb_office is now rgb, too 2021-12-21 16:10:33 +01:00
Ranlvor
5b62bc9325
add wled strip 2021-12-11 23:13:58 +01:00
Ranlvor
9e8765711a
show mic state on lamp color, too 2021-12-11 12:15:14 +01:00
Ranlvor
cbc3084eff
show mic/webcam status 2021-12-11 12:10:37 +01:00
Ranlvor
9b0c554e54
allow all temperature warning levels to be set individually 2021-12-11 12:00:17 +01:00
Ranlvor
4806973c0a
changes 2021-10-04 22:51:49 +02:00
Ranlvor
167d58d670
uwap-home: add printer 2021-05-20 20:49:54 +02:00
Ranlvor
b4b85fdb17
uwap-home: add toggle for projector auto off 2021-05-13 18:44:09 +02:00
Ranlvor
8c91dee9b8
uwap-home: add projector 2021-05-12 21:26:29 +02:00
Ranlvor
db62d59f67
uwap-home, NAS: revert to LWL based nas-online-check
Because the network topology changed and now the nas is on an non-MQTT-switchport
2021-04-29 20:05:40 +02:00
Ranlvor
3208de65d4
Add snapcast and tvheadend webinterfaces 2021-04-29 20:04:31 +02:00
Ranlvor
bb334ca7bc
uwap-home: Add UI for new Livingroom/Diningroom-Heating-Controller 2021-03-20 13:07:52 +01:00
Ranlvor
fa07cdc784
Switch an die neue stelle gepackt 2021-03-20 12:59:44 +01:00
Ranlvor
9d44540ebc
uwap-home: move nas to new location and make it behave more like tucana for faster feedback 2021-02-20 17:04:22 +01:00
Ranlvor
e6b26dbac1
Add switch for office_heating/self_control
This switch decides if the control algorithm for the office temperature runs inside of home-rust or in the heating valve itself. Former guarentees 100% open/close to avoid noise, later is way better (states like partial open, lower latency, lower temperature-shootovers and shootunders, etc, etc)
2021-01-14 20:36:16 +01:00
Ranlvor
27a9544092
uwap-home, heating: support temperature offsets 2020-12-23 13:08:56 +01:00
Ranlvor
6b78cc0a2f
uwap-home: Allow manual control of office heater 2020-12-23 10:08:33 +01:00
Ranlvor
6e63404724
bugfix for boostactiveendtime 2020-12-19 16:37:16 +01:00
Ranlvor
6028302a64
refactoring to give bedroom the new diningroom-controls 2020-12-19 16:33:33 +01:00
Ranlvor
a6aef2ee9a
uwap-home, heater diningroom: show boost/window open state 2020-12-19 16:19:42 +01:00
Ranlvor
01ed36e5be
uwap-home: Display current kitchen temperature
Because we are subscribed to that topic anyway
2020-12-07 20:25:00 +01:00
Ranlvor
a25d7cc01d
uwap-home: Add kittchen temperature warning 2020-12-07 20:20:21 +01:00
Ranlvor
57655b0f66
uwap-home: nicer topic order 2020-12-07 20:03:01 +01:00
Ranlvor
b7b0bd7cac
uwap-home, lights livingroom: add new winter mode 2020-12-07 19:58:23 +01:00
Ranlvor
6c80eb2bd4
uwap-home: Add new light in the office 2020-12-07 19:57:52 +01:00
Ranlvor
9a070dcc2b
summeractive is now a JSON-Value instead of a dedicated topic 2020-11-16 10:04:37 +01:00
Ranlvor
424aef1991
Properly include Buffer to fix linter errors
Thank you for the line, @uwap.
2020-11-15 03:58:38 +01:00
Ranlvor
e3eb924f1f
Formating for the linter 2020-11-15 03:30:51 +01:00
Ranlvor
9e388f4ae9
+Heating Bedroom 2020-11-15 03:27:03 +01:00
Ranlvor
dc2728b59a
uglify the code for the linter 2020-11-10 23:45:09 +01:00
Ranlvor
94aafdfbdd
Use the wrong type (String instead of Number) to make Flow happy 2020-11-10 23:34:09 +01:00
Ranlvor
ee4800d087
Add heater diningroom 2020-11-10 23:21:22 +01:00
Ranlvor
b898455f56
Pat the linter 2020-11-08 14:47:21 +01:00
Ranlvor
4e316f9e47
rework Lüftung/Heizung Büro 2020-11-08 14:42:17 +01:00
Ranlvor
fdb49f79a5
uwap-home: Add heating controls office 2020-11-06 19:36:51 +01:00
Ranlvor
39c364c742
use new mark feature to clean up fan settings 2020-10-19 18:35:51 +02:00
Ranlvor
c298a58a47
Merge remote-tracking branch 'origin/master' into fork/patch-41 2020-10-19 18:29:53 +02:00
0a0639e994 Update dependencies 2020-10-19 07:40:14 +02:00
d9096b13e4 Various small changes 2020-10-19 07:12:38 +02:00
a44eea520a Better sliders!
Add marks and value indication. (Fixes #146)
2020-10-19 06:24:04 +02:00
0f08e9f1ee Minor home-map changes 2020-10-19 05:49:35 +02:00
ccd9bcd3b5 Types are now bidirectional
It is still possible to define types as functions (Buffer => string for 
state topics, string => Buffer for command topics). Otherwise types have 
from and to properties.
(Fixes #101)
2020-10-19 05:45:09 +02:00
Ranlvor
550e0c7479
Fix spelling error the linter tempted me to do 2020-10-17 19:06:49 +02:00
Ranlvor
3fd432b2d2
Make the linter happy 2020-10-17 16:50:21 +02:00
Ranlvor
263507871b
Change some default values to home-rust compile-time-defaults
Because after losing MQTT-Broker-State and restarting home-rust it
reverts to compile-time-defaults but does not write this defaults
back to the MQTT-Broker. So the map should just assume that any
parameter not received via MQTT should be set to the compile-time-values.
2020-10-17 16:48:07 +02:00
Ranlvor
65452ac0cb
Add Light Diningroom 2020-10-17 16:28:15 +02:00
Ranlvor
d50ea6fd49
fix linting error 2020-10-17 16:23:22 +02:00
Ranlvor
53d547bc65
Merge remote-tracking branch 'origin/master' into fork/patch-41
Convert to new icon format.
2020-10-17 16:23:01 +02:00
2997ff8862 Various improvements 2020-10-08 20:31:42 +02:00
e43842fbed Link to the wiki 2020-10-08 18:05:40 +02:00
1c1de6356c Define icon transformations on withState
(Closes #148)
2020-10-08 17:40:42 +02:00
9a5557db03 Fix sidebar close icon not being visible
(Closes #147)
2020-10-08 17:24:51 +02:00
8376f404e4 Fix eslint errors 2020-10-08 08:46:06 +02:00
856aab41ad Improve the entire icon logic
- Tree Shaking for Icons (Closes #53)
- New API for the config (See 
https://github.com/uwap/mqtt-control-map/wiki/Icons)
- Icons can now be colored everywhere, not just on the map
2020-10-08 08:36:56 +02:00
43a33c3ab3 Fix minor eslint warnings 2020-10-05 22:02:30 +02:00
Ranlvor
5bae8025bd
Add LEDs Livingroom 2020-10-05 02:25:59 +02:00
Ranlvor
d338093eee
Fix icon raspberry-pi 2020-10-05 00:50:33 +02:00
8917402888 Fix whirlpool type comparison 2020-10-04 19:00:52 +02:00
af89238999 Greenkeeper is deprecated 2020-10-04 18:56:00 +02:00
Ranlvor
42e7697bf8
Add basic controls of the Vorstandswhirlpool (#104) 2020-10-04 18:54:29 +02:00
Ranlvor
66ff91da2d
uwap-home: Add second hallway light (#135)
* uwap-home: Add second hallway light

* show bulb on/off state on map icon

* Add icon for nas

* Use new zigbee2mqtt state-topics

The new state-topics-feature of zigbee2mqtt makes the translation-layer
in home-rust obsolete for simple (non color-changing) bulbs.

* uwap-home: Show tucana, Show Lüften Erinnerung

* Refactor topics: more template-functions, less spaghetti code

* uwap-home: Add office switch

* tucana: Support displaying half duplex, too

* Increase target temperature slider range

After a hot summer it can be usefull to be able to set the slider to e.G. 25°C
2020-10-04 18:52:37 +02:00
d39e547623 Fix svg import & update dependencies
(Fixes #132)
2020-10-04 18:46:37 +02:00
greenkeeper[bot]
2e1d9d83c8 Update file-loader to the latest version 🚀 (#131)
* chore(package): update file-loader to version 5.0.0

* chore(package): update lockfile yarn.lock
2019-12-09 08:24:05 +01:00
greenkeeper[bot]
9af88a02a4 Update @mdi/font to the latest version 🚀 (#123)
* fix(package): update @mdi/font to version 4.0.96

* chore(package): update lockfile

https://npm.im/greenkeeper-lockfile
2019-12-09 08:23:49 +01:00
Ranlvor
195728631a uwap-home-stuff (#127)
* Move Icons to match new background

* Use step 0.1 for temperatures

Because I want to set the target to 21.5 (which is the max of the slider anyway)

* Add officeLight
2019-12-09 08:23:33 +01:00
Ranlvor
890960fe23 RZL: Update onkyo station list (#130) 2019-11-24 13:40:24 +01:00
Ranlvor
15cf79f547 RZL: +Licht Dunstabzugshaube (#129) 2019-11-12 23:07:12 +01:00
greenkeeper[bot]
c29775389b Update eslint-plugin-flowtype to the latest version 🚀 (#121)
* chore(package): update eslint-plugin-flowtype to version 4.0.0

* chore(package): update lockfile

https://npm.im/greenkeeper-lockfile
2019-10-08 19:13:59 +02:00
Ranlvor
23a193625c fix cashdesk port (#126) 2019-10-08 19:12:54 +02:00
db36cdf37c Merge branch 'master' of github.com:uwap/mqtt-control-map 2019-08-08 07:25:10 +02:00
b4979ca910 Ignore react-leaflet type checking 2019-08-08 07:24:11 +02:00
Ranlvor
c87e739c60 Move Icons to match new background (#118) 2019-08-08 07:06:12 +02:00
greenkeeper[bot]
e3fb07eb5e Update flow-bin to the latest version 🚀 (#84)
* chore(package): update flow-bin to version 0.82.0

* chore(package): update lockfile yarn.lock
2019-08-05 04:45:57 +02:00
0b08d8e0ad Update more dependencies 2019-08-05 04:41:49 +02:00
b173050dce Update dev dependencies 2019-08-05 04:36:46 +02:00
edb3ae4ff9 Give uwap-home a proper svg 2019-08-05 04:28:00 +02:00
Ranlvor
226bfa3602 Add uwap-home (#102) 2019-08-05 04:00:10 +02:00
greenkeeper[bot]
7a57cf8e7f Update css-loader to the latest version 🚀 (#109)
* chore(package): update css-loader to version 3.0.0

* chore(package): update lockfile yarn.lock
2019-08-05 03:53:10 +02:00
greenkeeper[bot]
392c9f6507 Update husky to the latest version 🚀 (#114)
* chore(package): update husky to version 3.0.0

* chore(package): update lockfile

https://npm.im/greenkeeper-lockfile
2019-08-05 03:49:50 +02:00
greenkeeper[bot]
c7aa754d6a Update file-loader to the latest version 🚀 (#108)
* chore(package): update file-loader to version 4.0.0

* chore(package): update lockfile yarn.lock
2019-06-11 17:32:03 +02:00
greenkeeper[bot]
6d292c3433 Update mqtt to the latest version 🚀 (#106)
* fix(package): update mqtt to version 3.0.0

* chore(package): update lockfile yarn.lock
2019-06-11 17:31:46 +02:00
Ranlvor
6c641f4eba Fix Ultimaker-API-Translation (#110) 2019-06-11 15:56:22 +02:00
greenkeeper[bot]
45bb9034fe Update husky to the latest version 🚀 (#103)
* chore(package): update husky to version 2.0.0

* chore(package): update lockfile yarn.lock
2019-04-28 12:17:27 +02:00
Ranlvor
4588d450fb RZL Door: Show Power Consumption and Link to Dashboard (#100)
* RZL Door: Show Power Consumption and Link to Dashboard
2019-03-17 17:56:29 +01:00
Ranlvor
8ac32ced41 Tradfri-Topics: Typofix (#99)
The all-lights and the kittchen sink topics currently do not work because
the config uses the wrong topic names. This commit fixes the issues.
2019-03-17 14:55:58 +01:00
Ranlvor
a149fed592 Move Printer from Olymp to Fablab/alte Werkstatt (#98)
* Move Printer from Olymp to Fablab/alte Werkstatt
2019-03-13 18:40:29 +01:00
greenkeeper[bot]
43dcf12281 Update clean-webpack-plugin to the latest version 🚀 (#97)
* chore(package): update clean-webpack-plugin to version 2.0.0

* chore(package): update lockfile yarn.lock

* Update webpack.config.js
2019-03-08 08:50:28 +01:00
Ranlvor
50083999da RZL: Add deko-led-thingy „Pilze“ (#96) 2019-02-19 18:14:07 +01:00
greenkeeper[bot]
45e2e7c6fc Update file-loader to the latest version 🚀 (#95)
* chore(package): update file-loader to version 3.0.0

* chore(package): update lockfile yarn.lock
2019-01-17 06:20:17 +01:00
greenkeeper[bot]
2d528a72c6 Update css-loader to the latest version 🚀 (#93)
* chore(package): update css-loader to version 2.0.0

* chore(package): update lockfile yarn.lock
2018-12-17 01:21:49 +01:00
9d0f47299a RZL: Change corridor opacity 2018-11-13 17:25:11 +01:00
6ab1c3bfae Merge branch 'master' of github.com:uwap/mqtt-control-map 2018-11-13 17:20:54 +01:00
3aaf7a0430 RZL: update floorplan
Fixes #57
2018-11-13 17:20:29 +01:00
greenkeeper[bot]
56c39461c4 Update clean-webpack-plugin to the latest version 🚀 (#89)
* chore(package): update clean-webpack-plugin to version 1.0.0

* chore(package): update lockfile yarn.lock
2018-11-13 06:44:47 +01:00
4d40321975 Proper drawer behaviour
* Move the content to the left when the drawer is opened
2018-11-12 05:20:44 +01:00
0a2c46c37b Fix bugs with toggle and drop down 2018-11-11 16:08:38 +01:00
ee2dbe0f7b Add Github and Feedback link 2018-11-10 04:52:44 +01:00
0a027fd7c2 Completely rewrite the UI Components
to a new functional way of defining them with advantages towards generating docs and a potential editor functionality
2018-11-10 01:34:31 +01:00
46 changed files with 7489 additions and 5555 deletions

View file

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

View file

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

View file

@ -1,5 +1,8 @@
[ignore]
[untyped]
.*/node_modules/react-leaflet
[include]
[libs]
@ -7,9 +10,8 @@ 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,10 +4,6 @@ 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,7 +1,6 @@
# 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
@ -12,33 +11,6 @@ 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.
## Config
## Documentation
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. |
The documentation can be found in our [mqtt-control-map wiki](https://github.com/uwap/mqtt-control-map/wiki).

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

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

View file

@ -2,8 +2,9 @@
import type { Config } from "config/flowtypes";
import * as types from "config/types";
import { hex, rainbow } from "config/colors";
import { mdi, rawMdi } from "config/icon";
import { svg, withState } from "config/icon";
import { esper, tasmota } from "./utils";
import * as icons from "@mdi/js";
import * as onkyo from "./onkyo";
import * as olymp from "./olymp";
@ -137,6 +138,14 @@ const config: Config = {
},
defaultValue: ""
},
powerConsumption: {
state: {
name: "/service/power/hauptraum/power",
type: (msg) =>
(Number.parseFloat(msg.toString()) / 1000).toFixed(2) + " kW"
},
defaultValue: ""
},
projector: {
state: {
name: "/service/beamer/state",
@ -169,8 +178,8 @@ const config: Config = {
type: types.option({
unreachable: "unavailable",
booting: "unavailable",
prePrint: "printing",
postPrint: "printing",
"pre_print": "printing",
"post_print": "printing",
printing: "printing",
idle: "idle",
error: "error",
@ -217,11 +226,42 @@ 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
tasmota.topics("2", "printerAnnette"),
tasmota.topics("6", "snackbar"),
tasmota.topics("7", "infoscreen"),
tasmota.topics("9", "pilze"),
esper.topics("afba40", "flyfry"),
@ -236,29 +276,28 @@ const config: Config = {
ledStahltrager: {
name: "LED Stahlträger",
position: [340, 590],
icon: mdi("white-balance-iridescent"),
iconColor: ({ledStahltraeger}) =>
(ledStahltraeger === "on" ? rainbow : hex("#000000")),
icon: svg(icons.mdiWhiteBalanceIridescent).color(({ledStahltraeger}) =>
(ledStahltraeger === "on" ? rainbow : hex("#000000"))),
ui: [
{
type: "toggle",
text: "Stahlträger LED",
topic: "ledStahltraeger",
icon: mdi("power")
icon: svg(icons.mdiPower)
}
]
},
snackbar: {
name: "Snackbar",
position: [510, 500],
icon: mdi("fridge"),
iconColor: tasmota.iconColor("snackbar", hex("#E20074")),
icon: svg(icons.mdiFridge).color(
tasmota.iconColor("snackbar", hex("#E20074"))),
ui: [
{
type: "toggle",
text: "Snackbar",
topic: "snackbar",
icon: mdi("power")
icon: svg(icons.mdiPower)
},
{
type: "section",
@ -268,7 +307,7 @@ const config: Config = {
type: "text",
text: "LED-Streifen",
topic: "snackbarLedOnline",
icon: mdi("white-balance-iridescent")
icon: svg(icons.mdiWhiteBalanceIridescent)
},
{
type: "dropDown",
@ -287,7 +326,7 @@ const config: Config = {
"11": "Rainbow Pattern",
"12": "Fire Pattern"
},
icon: mdi("settings"),
icon: svg(icons.mdiCog),
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
},
{
@ -296,7 +335,7 @@ const config: Config = {
topic: "snackbarDimmmer",
min: 0,
max: 100,
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
},
{
@ -305,7 +344,7 @@ const config: Config = {
topic: "snackbarSpeed",
min: 0,
max: 20,
icon: mdi("speedometer"),
icon: svg(icons.mdiSpeedometer),
enableCondition: ({ snackbarLedOnline }) => snackbarLedOnline === "on"
}
]
@ -313,142 +352,151 @@ const config: Config = {
twinkle: {
name: "Twinkle",
position: [530, 560],
icon: ({twinkle}) =>
(twinkle === "on" ? rawMdi("led-on flip-v") : rawMdi("led-off flip-v")),
iconColor: ({twinkle}) => (twinkle === "on" ? rainbow : hex("#000000")),
icon: withState(({twinkle}) =>
(twinkle === "on" ? svg(icons.mdiLedOn).flipV().color(rainbow)
: svg(icons.mdiLedOff).flipV())
),
ui: [
{
type: "toggle",
text: "Twinkle",
topic: "twinkle",
icon: mdi("power")
icon: svg(icons.mdiPower)
}
]
},
fan: {
name: "Ventilator",
position: [530, 440],
icon: mdi("fan"),
iconColor: ({fan}) => (fan === "on" ? hex("#00FF00") : hex("#000000")),
icon: svg(icons.mdiFan).color(({fan}) =>
(fan === "on" ? hex("#00FF00") : hex("#000000"))),
ui: [
{
type: "toggle",
text: "Ventilator",
topic: "fan",
icon: mdi("power")
icon: svg(icons.mdiPower)
}
]
},
cashdesk: {
name: "Cashdesk",
position: [510, 467],
icon: mdi("coin"),
icon: svg(icons.mdiCurrencyUsdCircle),
ui: [
{
type: "link",
link: "http://cashdesk.rzl:8081/",
link: "http://cashdesk.rzl:8000/",
text: "Open Cashdesk",
icon: mdi("open-in-new")
icon: svg(icons.mdiOpenInNew)
}
]
},
flyfry: {
name: "Fliegenbratgerät",
position: [450, 570],
icon: mdi("fire"),
iconColor: ({flyfry}) =>
(flyfry === "on" ? hex("#6666FF") : hex("#000000")),
icon: svg(icons.mdiFire).color(({flyfry}) =>
(flyfry === "on" ? hex("#6666FF") : hex("#000000"))),
ui: esper.statistics("flyfry", [
{
type: "toggle",
text: "Fliegenbratgerät",
topic: "flyfry",
icon: mdi("power")
icon: svg(icons.mdiPower)
}
])
},
projector: {
name: "Beamer",
position: [380, 590],
icon: mdi("projector flip-v"),
iconColor: ({projector}) =>
icon: svg(icons.mdiProjector).flipV().color(({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: mdi("power")
icon: svg(icons.mdiPower)
}
]
},
loetarbeitsplatz4: {
name: "Lötarbeitsplatz",
position: [205, 455],
icon: mdi("eyedropper-variant"),
iconColor: ({loetarbeitsplatz4}) =>
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000")),
icon: svg(icons.mdiEyedropperVariant).color(({loetarbeitsplatz4}) =>
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000"))),
ui: [
{
type: "text",
text: "Status",
topic: "loetarbeitsplatz4",
icon: mdi("eyedropper-variant")
icon: svg(icons.mdiEyedropperVariant)
}
]
},
loetarbeitsplatz5: {
name: "Lötarbeitsplatz",
position: [205, 405],
icon: mdi("eyedropper-variant"),
iconColor: ({loetarbeitsplatz5}) =>
(loetarbeitsplatz5 === "on" ? hex("#FF0000") : hex("#000000")),
icon: svg(icons.mdiEyedropperVariant).color(({loetarbeitsplatz4}) =>
(loetarbeitsplatz4 === "on" ? hex("#FF0000") : hex("#000000"))),
ui: [
{
type: "text",
text: "Status",
topic: "loetarbeitsplatz5",
icon: mdi("eyedropper-variant")
icon: svg(icons.mdiEyedropperVariant)
}
]
},
door: {
name: "Tür",
position: [455, 350],
icon: mdi("swap-vertical"),
iconColor: ({doorStatus}) =>
(doorStatus === "on" ? hex("#00FF00") : hex("#FF0000")),
icon: svg(icons.mdiSwapVertical).color(({doorStatus}) =>
(doorStatus === "on" ? hex("#00FF00") : hex("#FF0000"))),
ui: [
{
type: "link",
link: "http://s.rzl.so",
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",
text: "Anwesend",
topic: "presenceStatus",
icon: mdi("account")
icon: svg(icons.mdiAccount)
},
{
type: "text",
text: "Devices",
topic: "devicesStatus",
icon: mdi("wifi")
icon: svg(icons.mdiWifi)
},
{
type: "toggle",
text: "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: {
name: "Infoscreen",
position: [255, 495],
icon: mdi("television-guide flip-v"),
iconColor: tasmota.iconColor("infoscreen", hex("#4444FF")),
icon: svg(icons.mdiTelevisionGuide).flipV().color(
tasmota.iconColor("infoscreen", hex("#4444FF"))
),
ui: [
{
type: "toggle",
text: "Infoscreen",
topic: "infoscreen",
icon: mdi("power")
icon: svg(icons.mdiPower)
},
{
type: "link",
link: "http://cashdesk.rzl:3030/rzl",
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: {
name: "Ultimaker 3",
position: [754, 560],
icon: mdi("printer-3d"),
iconColor: ({printer3DStatus}) =>
icon: svg(icons.mdiPrinter3d).color(({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: mdi("open-in-new")
icon: svg(icons.mdiOpenInNew)
},
{
type: "section",
@ -498,7 +561,7 @@ const config: Config = {
},
{
type: "progress",
icon: mdi("rotate-right"),
icon: svg(icons.mdiRotateRight),
min: 0,
max: 1,
text: "Printing Progress",
@ -507,7 +570,7 @@ const config: Config = {
{
type: "text",
text: "Time Left",
icon: mdi("clock"),
icon: svg(icons.mdiClock),
topic: "printer3Dremaining"
}
]
@ -515,31 +578,87 @@ const config: Config = {
partkeepr: {
name: "Partkeepr",
position: [48, 450],
icon: mdi("chip"),
icon: svg(icons.mdiChip),
ui: [
{
type: "link",
link: "http://partkeepr.rzl/",
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: {
name: "Strom Fablab",
position: [613, 537],
icon: ({nebenraumPowerStatus}) =>
(nebenraumPowerStatus === "on" ? rawMdi("flash") : rawMdi("flash-off")),
iconColor: ({nebenraumPowerStatus}) =>
(nebenraumPowerStatus === "on" ? hex("#00ff00") : hex("#000000")),
icon: withState(({nebenraumPowerStatus}) =>
(nebenraumPowerStatus === "on" ?
svg(icons.mdiFlash).color(hex("#00FF00")) : svg(icons.mdiFlashOff))),
ui: [
{
type: "text",
icon: mdi("power"),
icon: svg(icons.mdiPower),
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: [
@ -551,7 +670,7 @@ const config: Config = {
opacity: 0.7,
bounds: {
topLeft: [0, 0],
bottomRight: [1000, 700]
bottomRight: [1320, 720]
}
},
{
@ -561,7 +680,7 @@ const config: Config = {
opacity: 0.4,
bounds: {
topLeft: [0, 0],
bottomRight: [1000, 700]
bottomRight: [1320, 720]
}
},
{
@ -571,7 +690,7 @@ const config: Config = {
opacity: 0.8,
bounds: {
topLeft: [0, 0],
bottomRight: [1000, 700]
bottomRight: [1320, 720]
}
}
]

View file

@ -1,9 +1,10 @@
// @flow
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 * 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 = {
//Kuechen-Floalts
@ -20,14 +21,16 @@ export const topics: Topics = {
...tradfri.remote.topics("65542"),
...tradfri.remote.topics("65546"),
...tasmota.topics("10", "lichtDunstabzug"),
kitchenLightColor: {
state: {
name: "/service/openhab/out/kitchenLight_allColor_temperature"
name: "/service/openhab/out/kitchen_light_all_color_temperature"
+ "/state",
type: types.string
},
command: {
name: "/service/openhab/in/kitchenLight_allColor_temperature"
name: "/service/openhab/in/kitchen_light_all_color_temperature"
+ "/command",
type: types.string
},
@ -35,11 +38,11 @@ export const topics: Topics = {
},
kitchenLightBrightness: {
state: {
name: "/service/openhab/out/kitchenLight_allBrightness/state",
name: "/service/openhab/out/kitchen_light_all_brightness/state",
type: types.string
},
command: {
name: "/service/openhab/in/kitchenLight_allBrightness/command",
name: "/service/openhab/in/kitchen_light_all_brightness/command",
type: types.string
},
defaultValue: "0"
@ -47,12 +50,12 @@ export const topics: Topics = {
kitchenSinkLightBrightness: {
state: {
name: "/service/openhab/out/tradfri_0100_"
+ "gwb8d7af2b448f_65545Brightness/state",
+ "gwb8d7af2b448f_65545_brightness/state",
type: types.string
},
command: {
name: "/service/openhab/in/tradfri_0100_"
+ "gwb8d7af2b448f_65545Brightness/command",
+ "gwb8d7af2b448f_65545_brightness/command",
type: types.string
},
defaultValue: "0"
@ -63,7 +66,7 @@ export const controls: Controls = {
kitchenLight: {
name: "Deckenlicht Küche",
position: [325, 407],
icon: mdi("ceiling-light"),
icon: svg(icons.mdiCeilingLight),
ui: [
{
type: "toggle",
@ -72,14 +75,14 @@ export const controls: Controls = {
toggled: (n) => parseInt(n, 10) > 0,
topic: "kitchenLightBrightness",
text: "Ein/Ausschalten",
icon: mdi("power")
icon: svg(icons.mdiPower)
},
{
type: "slider",
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: "kitchenLightBrightness"
},
{
@ -87,7 +90,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: mdi("weather-sunset-down"),
icon: svg(icons.mdiWeatherSunsetDown),
topic: "kitchenLightColor"
},
{
@ -99,7 +102,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: floalt.brightness("65537")
},
{
@ -107,7 +110,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: mdi("weather-sunset-down"),
icon: svg(icons.mdiWeatherSunsetDown),
topic: floalt.color("65537")
},
{
@ -119,7 +122,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: floalt.brightness("65538")
},
{
@ -127,7 +130,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: mdi("weather-sunset-down"),
icon: svg(icons.mdiWeatherSunsetDown),
topic: floalt.color("65538")
},
{
@ -139,7 +142,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: floalt.brightness("65539")
},
{
@ -147,7 +150,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: mdi("weather-sunset-down"),
icon: svg(icons.mdiWeatherSunsetDown),
topic: floalt.color("65539")
},
{
@ -159,7 +162,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: floalt.brightness("65540")
},
{
@ -167,7 +170,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: mdi("weather-sunset-down"),
icon: svg(icons.mdiWeatherSunsetDown),
topic: floalt.color("65540")
}
]
@ -175,7 +178,7 @@ export const controls: Controls = {
kitchenSinkLight: {
name: "Licht Spüle",
position: [300, 345],
icon: mdi("wall-sconce-flat"),
icon: svg(icons.mdiWallSconceFlat),
ui: [
{
type: "toggle",
@ -184,14 +187,14 @@ export const controls: Controls = {
toggled: (n) => parseInt(n, 10) > 0,
topic: "kitchenSinkLightBrightness",
text: "Ein/Ausschalten",
icon: mdi("power")
icon: svg(icons.mdiPower)
},
{
type: "slider",
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: "kitchenSinkLightBrightness"
}
]
@ -199,7 +202,7 @@ export const controls: Controls = {
kitchenCounterLight: {
name: "Deckenlicht Theke",
position: [400, 440],
icon: mdi("ceiling-light"),
icon: svg(icons.mdiCeilingLight),
ui: [
{
type: "section",
@ -210,7 +213,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: floalt.brightness("65544")
},
{
@ -218,7 +221,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: mdi("weather-sunset-down"),
icon: svg(icons.mdiWeatherSunsetDown),
topic: floalt.color("65544")
},
{
@ -230,7 +233,7 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Helligkeit",
icon: mdi("brightness-7"),
icon: svg(icons.mdiBrightness7),
topic: floalt.brightness("65543")
},
{
@ -238,15 +241,29 @@ export const controls: Controls = {
min: 0,
max: 100,
text: "Farbtemperatur",
icon: mdi("weather-sunset-down"),
icon: svg(icons.mdiWeatherSunsetDown),
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: {
name: "Fernbedinungen",
position: [400, 344],
icon: mdi("light-switch"),
icon: svg(icons.mdiLightSwitch),
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,11 +1,14 @@
// @flow
import type { Topics, Controls } from "config/flowtypes";
import { mdi } from "config/icon";
import { svg } 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",
@ -38,96 +41,70 @@ export const topics: Topics = {
type: types.option({ on: "ON", off: "OFF" })
},
defaultValue: "off"
},
...tasmota.topics("2", "printerOlymp"),
...tasmota.topics("8", "ledOlymp"),
...esper.topics("afba45", "alarm")
}
};
export const controls: Controls = {
ledOlymp: {
name: "LED Olymp",
position: [196, 154],
icon: mdi("white-balance-iridescent rotate-45"),
iconColor: tasmota.iconColor("ledOlymp", rainbow),
icon: svg(icons.mdiWhiteBalanceIridescent).rotate(45).color(
tasmota.iconColor("ledOlymp", rainbow)),
ui: [
{
type: "toggle",
text: "LED Olymp",
topic: "ledOlymp",
icon: mdi("power")
icon: svg(icons.mdiPower)
}
]
},
videogames: {
name: "Videospiele",
position: [100, 100],
icon: mdi("gamepad-variant"),
iconColor: ({videogames}) =>
(videogames === "on" ? hex("#00FF00") : hex("#000000")),
icon: svg(icons.mdiGamepadVariant).color(({videogames}) =>
(videogames === "on" ? hex("#00FF00") : hex("#000000"))),
ui: [
{
type: "toggle",
text: "Videospiele",
topic: "videogames",
icon: mdi("power")
icon: svg(icons.mdiPower)
}
]
},
olympPC: {
name: "Rechner",
position: [297, 90],
icon: mdi("desktop-classic"),
iconColor: ({olympPC}) =>
(olympPC === "on" ? hex("#00FF00") : hex("#000000")),
icon: svg(icons.mdiDesktopClassic).color(({olympPC}) =>
(olympPC === "on" ? hex("#00FF00") : hex("#000000"))),
ui: [
{
type: "toggle",
text: "Rechner",
topic: "olympPC",
icon: mdi("power")
}
]
},
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")
icon: svg(icons.mdiPower)
}
]
},
rundumleuchte: {
name: "Rundumleuchte",
position: [310, 275],
icon: mdi("alarm-light"),
iconColor: ({rundumleuchte}) =>
(rundumleuchte === "on" ? hex("#F0DF10") : hex("#000000")),
icon: svg(icons.mdiAlarmLight).color(({rundumleuchte}) =>
(rundumleuchte === "on" ? hex("#F0DF10") : hex("#000000"))),
ui: [
{
type: "toggle",
text: "Rundumleuchte",
topic: "rundumleuchte",
icon: mdi("power")
icon: svg(icons.mdiPower)
}
]
},
alarm: {
name: "Alarm",
position: [340, 250],
icon: mdi("alarm-bell"),
icon: svg(icons.mdiAlarmBell),
iconColor: () => hex("#000000"),
ui: esper.statistics("alarm")
}

View file

@ -1,8 +1,9 @@
// @flow
import type { Topics, Controls } from "config/flowtypes";
import { mdi } from "config/icon";
import { svg } 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: {
@ -94,6 +95,10 @@ export const topics: Topics = {
NPR08: "somafmLush",
NPR09: "somafmBeatblender",
NPR0a: "ponyville",
NPR0b: "deutschlandradio",
NPR0c: "somafmSuburbsOfGoa",
NPR0d: "somafmSonicUniverse",
NPR0e: "somafmChrismasLounge",
otherwise: "unknown"
})
},
@ -110,6 +115,10 @@ export const topics: Topics = {
somafmLush: "NPR08",
somafmBeatblender: "NPR09",
ponyville: "NPR0a",
deutschlandradio: "NPR0b",
somafmSuburbsOfGoa: "NPR0c",
somafmSonicUniverse: "NPR0d",
somafmChrismasLounge: "NPR0e",
otherwise: "NPR00"
})
},
@ -124,12 +133,12 @@ export const controls: Controls = {
iconColor: ({onkyoConnection, onkyoPower}) =>
(onkyoConnection !== "connected" ? hex("#888888") :
(onkyoPower === "on" ? hex("#00FF00") : hex("#000000"))),
icon: mdi("audio-video"),
icon: svg(icons.mdiAudioVideo),
ui: [
{
type: "toggle",
text: "Power",
icon: mdi("power"),
icon: svg(icons.mdiPower),
topic: "onkyoPower",
enableCondition: (state) => state.onkyoConnection === "connected"
},
@ -143,14 +152,14 @@ export const controls: Controls = {
topic: "onkyoVolume",
min: 0,
max: 50,
icon: mdi("volume-high"),
icon: svg(icons.mdiVolumeHigh),
enableCondition: (state) => state.onkyoConnection === "connected"
},
{
type: "toggle",
text: "Mute",
topic: "onkyoMute",
icon: mdi("volume-off"),
icon: svg(icons.mdiVolumeOff),
enableCondition: (state) => state.onkyoConnection === "connected"
},
{
@ -168,7 +177,7 @@ export const controls: Controls = {
pult: "Pult",
front: "Front HDMI"
},
icon: mdi("usb"),
icon: svg(icons.mdiUsb),
enableCondition: (state) => state.onkyoConnection === "connected"
},
{
@ -186,9 +195,13 @@ export const controls: Controls = {
somafmLush: "Lush (SomaFM)",
somafmBeatblender: "Beat Blender (Soma FM)",
ponyville: "Ponyville FM",
deutschlandradio: "Deutschlandradio",
somafmSuburbsOfGoa: "Suburbs of Goa (SomaFM)",
somafmSonicUniverse: "Sonic Universe (SomaFM)",
somafmChrismasLounge: "Christmas Lounge (SomaFM)",
unknown: "Unknown"
},
icon: mdi("radio"),
icon: svg(icons.mdiRadio),
enableCondition: (state) => state.onkyoConnection === "connected"
&& state.onkyoInputs === "netzwerk"
},
@ -200,7 +213,7 @@ export const controls: Controls = {
type: "link",
link: "http://mpd.rzl/mpd/player/index.php",
text: "Open MPD Interface",
icon: mdi("open-in-new")
icon: svg(icons.mdiOpenInNew)
}
]
}

View file

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

View 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

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@
<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+mqttmap.package.json@uwap.name>",
"description": "control devices via mqtt on a beautiful map of your space",
"author": "uwap <me@uwap.name>",
"description": "Control Devices via mqtt, visualized on a Map",
"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",
"watch": "webpack-dev-server --open --config webpack.config.js --mode development --env",
"travis": "./travis.sh",
@ -12,45 +12,52 @@
"precommit": "yarn lint"
},
"dependencies": {
"@material-ui/core": "^3.0.1",
"@material-ui/lab": "^3.0.0-alpha.16",
"@mdi/font": "^3.0.39",
"leaflet": "^1.3.1",
"lodash-es": "^4.17.4",
"mqtt": "^2.14.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-leaflet": "^2.0.0",
"redux": "^3.7.2"
"@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",
"leaflet": "^1.5.1",
"mqtt": "^4.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.1.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0-rc.1",
"@babel/core": "^7.0.0-rc.1",
"@babel/plugin-proposal-class-properties": "^7.0.0-rc.1",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0-rc.1",
"@babel/polyfill": "^7.0.0-rc.1",
"@babel/preset-env": "^7.0.0-rc.1",
"@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/preset-env": "^7.5.5",
"@babel/preset-flow": "^7.0.0-rc.1",
"@babel/preset-react": "^7.0.0-rc.1",
"babel-eslint": "^10.0.0",
"babel-loader": "^8.0.0-beta",
"clean-webpack-plugin": "^0.1.18",
"css-loader": "^1.0.0",
"eslint": "^5.0.1",
"eslint-plugin-flowtype": "^3.0.0",
"@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",
"eslint-plugin-fp": "^2.3.0",
"eslint-plugin-react": "^7.6.1",
"file-loader": "^2.0.0",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^4.1.2",
"file-loader": "^6.1.0",
"flow": "^0.2.3",
"flow-bin": "^0.85.0",
"flow-typed": "^2.3.0",
"html-webpack-plugin": "^3.1.0",
"husky": "^1.0.0",
"style-loader": "^0.23.0",
"webpack": "^4.3.0",
"webpack-cli": "^3.0.0",
"webpack-dev-server": "^3.1.1",
"webpack-shell-plugin": "^0.5.0"
"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"
},
"license": "MIT"
}

View file

@ -9,13 +9,10 @@ import throttle from "lodash/throttle";
import type { Config, Control, Topics } from "config/flowtypes";
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 { withStyles } from "@mui/styles";
import Snackbar from "@mui/material/Snackbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import SideBar from "components/SideBar";
import ControlMap from "components/ControlMap";
@ -39,6 +36,14 @@ 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
@ -55,7 +60,8 @@ 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.name)
(x) => (this.topics[x].state != null ? this.topics[x].state.name : "")
)
}),
mqttConnected: false,
search: "",
@ -76,22 +82,24 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
Object.assign({}, ...this.props.config.topics) : this.props.config.topics;
}
static styles() {
static styles(theme) {
return {
drawerPaper: {
width: 320
contentElement: {
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) {
try {
const topics = filter(
@ -105,8 +113,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 parseVal = stateTopic ? stateTopic.type : null;
const val = parseVal == null ? message.toString() : parseVal(message);
const typeConversion = stateTopic?.type?.from ?? stateTopic?.type;
const val = (typeConversion ?? ((x: Buffer) => x.toString()))(message);
this.setMqttStateDebounced(
{mqttState: Object.assign({},
merge(this.state.mqttState, { [topic]: val}))});
@ -126,16 +134,16 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
this.setState({drawerOpened: false});
}
changeState = (topic: string, value: string) => {
changeState = (topic: string, val: string) => {
try {
if (this.topics[topic].command == null) {
const commandTopic = this.topics[topic].command;
if (commandTopic == null) {
return;
}
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));
const rawTopic = commandTopic.name;
const typeConversion = commandTopic?.type?.to ?? commandTopic.type;
const value = (typeConversion ?? Buffer.from)(val);
this.state.mqttSend(rawTopic, value);
} catch (err) {
this.setState({ error: err.toString() });
}
@ -147,19 +155,24 @@ class App extends React.PureComponent<AppProps & Classes, AppState> {
state: this.state.mqttState,
changeState: this.changeState
}}>
<TopBar title={`${this.props.config.space.name} Map`}
connected={this.state.mqttConnected}
onSearch={(s) => this.setState({ search: s })} />
<div className={
this.state.drawerOpened
? this.props.classes.contentElementShifted
: this.props.classes.contentElement
}>
<TopBar connected={this.state.mqttConnected}
onSearch={(s) => this.setState({ search: s })} />
{this.controlMap(this.state.search)}
</div>
<SideBar open={this.state.drawerOpened}
control={this.state.selectedControl}
onCloseRequest={this.closeDrawer}
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
|| <UiItemList controls={this.state.selectedControl.ui} />}
</SideBar>
{this.controlMap(this.state.search)}
<Snackbar
anchorOrigin={{
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 (
<MuiThemeProvider theme={App.theme(props.config)}>
<StyledApp {...props} />
</MuiThemeProvider>
);
};
export default withStyles(App.styles)(App);

View file

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

View file

@ -1,61 +1,63 @@
// @flow
import * as React from "react";
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 { 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 type { RawIcon } from "config/icon";
import type { Control } from "config/flowtypes";
export type SideBarProps = {
control: ?Control,
open: boolean,
onCloseRequest: () => void,
icon?: ?RawIcon,
icon?: ?React.Node,
children?: React.Node
};
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.flex}>
{props.control == null ? "" : props.control.name}
</Typography>
<IconButton onClick={props.onCloseRequest}>
<i className="mdi mdi-close mdi-36px"></i>
</IconButton>
</Toolbar>
</AppBar>
<List id="drawer_uiComponents">
<React.Fragment>{props.children}</React.Fragment>
</List>
</Drawer>
);
const styles = {
const useStyles = makeStyles((theme) => ({
drawerPaper: {
width: 340
},
flex: {
flex: 1
title: {
flex: 1,
marginLeft: theme.spacing(1)
}
}));
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 withStyles(styles)(SideBar);
export default SideBar;

View file

@ -1,16 +1,18 @@
// @flow
import React from "react";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import CircularProgress from "@material-ui/core/CircularProgress";
import InputBase from "@material-ui/core/InputBase";
import { fade } from "@material-ui/core/styles/colorManipulator";
import { withStyles } from "@material-ui/core/styles";
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";
export type TopBarProps = {
title: string,
connected: boolean,
onSearch: string => void
};
@ -21,77 +23,76 @@ export type SearchBarProps = {
const renderConnectionIndicator = (connected: boolean) => {
if (connected) {
return (<i style={{fontSize: 32}} className="mdi mdi-map"></i>);
return (<ReactIcon path={mdiMap} size={2} />);
}
return (
<CircularProgress size={32} style={{color: "rgba(0, 0, 0, 0.54)"}} />
);
};
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)
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',
},
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 TopBar = (props: TopBarProps) => (
<AppBar position="static">
<AppBar position="static" color="primary">
<Toolbar>
{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>
<Typography variant="title">{props.title}</Typography>
<Tooltip title="View on Github">
<IconButton onClick={openOnGithub}>
<ReactIcon path={mdiGithub} size={1.5} />
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>
);

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

View file

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

View file

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

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

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

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

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

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

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

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

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

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

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

View file

@ -1,17 +1,22 @@
// @flow
import type { Color } from "config/colors";
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,
type: TopicType
type: T
}
export type Topic = {
state?: StateCommand,
command?: StateCommand,
state?: StateCommand<StateTopicType>,
command?: StateCommand<CommandTopicType>,
defaultValue: string
};
export type Topics = Map<string, Topic>;
@ -22,9 +27,9 @@ export interface UIControl {
+topic: string
}
export interface Enableable {
export type Enableable = $ReadOnly<{
enableCondition?: (s: State) => boolean
}
}>;
export type UIToggle = $ReadOnly<{|
type: "toggle",
@ -53,9 +58,10 @@ 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<{|
@ -100,7 +106,6 @@ export type Control = {
name: string,
position: [number, number],
icon: Icon,
iconColor?: (state: State) => Color,
ui: Array<ControlUI>
};
export type Controls = Map<string, Control>;
@ -116,6 +121,17 @@ 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,55 +1,88 @@
// @flow
import * as React from "react";
import ReactContext from "mqtt/context";
import React from "react";
import ReactIcon from "@mdi/react";
import { type Color } from "./colors";
import * as mdiIcons from "@mdi/js";
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(" ")}`;
type IconPropHelper = {
size?: number,
rotate?: number,
horizontal?: boolean,
vertical?: boolean,
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 val = parseInt(rawval, 10);
if (isNaN(val)) {
return rawMdi("battery-unknown");
return svg(mdiIcons.mdiBatteryUnknown);
} else if (val > 95) {
return rawMdi("battery");
return svg(mdiIcons.mdiBattery);
} else if (val > 85) {
return rawMdi("battery-90");
return svg(mdiIcons.mdiBattery90);
} else if (val > 75) {
return rawMdi("battery-80");
return svg(mdiIcons.mdiBattery80);
} else if (val > 65) {
return rawMdi("battery-70");
return svg(mdiIcons.mdiBattery70);
} else if (val > 55) {
return rawMdi("battery-60");
return svg(mdiIcons.mdiBattery60);
} else if (val > 45) {
return rawMdi("battery-50");
return svg(mdiIcons.mdiBattery50);
} else if (val > 35) {
return rawMdi("battery-40");
return svg(mdiIcons.mdiBattery40);
} else if (val > 25) {
return rawMdi("battery-30");
return svg(mdiIcons.mdiBattery30);
} else if (val > 15) {
return rawMdi("battery-20");
} else {
return rawMdi("battery-10");
return svg(mdiIcons.mdiBattery20);
}
};
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>
);
};
return svg(mdiIcons.mdiBattery10);
});

View file

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

View file

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

View file

@ -1,10 +1,20 @@
// @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";
@ -13,6 +23,21 @@ 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(<App config={config} />, contentElement);
ReactDOM.render((
<ThemeProvider theme={theme}>
<App config={config} />
</ThemeProvider>
), contentElement);

View file

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

View file

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

9346
yarn.lock

File diff suppressed because it is too large Load diff