187 Commits

Author SHA1 Message Date
proddy
a57fdaa4b3 Merge remote-tracking branch 'origin/dev' into main 2021-05-04 12:21:51 +02:00
proddy
1ae738016e prep for 3.1.0 2021-05-04 12:19:01 +02:00
MichaelDvP
ee5b1b8c34 add dallassensors to api-info 2021-04-30 15:51:31 +02:00
MichaelDvP
4f98b4bb21 fix remote thermostat roomtemp 2021-04-30 15:15:07 +02:00
MichaelDvP
2b95a0d125 add boiler command selburnpow, update packages 2021-04-29 16:19:43 +02:00
MichaelDvP
87b2a05d39 add CRF200 thermostat flag and no_write 2021-04-27 14:48:36 +02:00
MichaelDvP
44d0b52424 show sent telegram on tx-error 2021-04-27 14:47:55 +02:00
MichaelDvP
de9ff6a3a1 fix heatpump value position 2021-04-27 14:47:12 +02:00
MichaelDvP
fcc4831c9f bool value info, dont show command only in mqtt/telnet 2021-04-26 14:51:49 +02:00
MichaelDvP
6f435cbcfd MC110Status to HT3 boilers 2021-04-26 14:51:02 +02:00
MichaelDvP
b01264f701 terminal linebuffers on edit 2021-04-26 14:50:23 +02:00
MichaelDvP
e6e507a470 system_info_id=0 for heartbeat output to API 2021-04-25 17:20:59 +02:00
MichaelDvP
2b60eaf462 add heatpump values #45, circuit "ww" to info 2021-04-25 14:20:39 +02:00
proddy
bf892aa5dc bump version 2021-04-25 09:53:53 +02:00
proddy
1bd834924a auto formatting 2021-04-25 09:53:45 +02:00
Proddy
e854161da9 Merge pull request #49 from MichaelDvP/dev
add min/max to values, boiler flags, ww-prefix, ha-prefix to mqtt
2021-04-25 09:47:07 +02:00
MichaelDvP
018b4af8d3 value_info bool format 2021-04-24 21:09:28 +02:00
MichaelDvP
903696726c do not show hatemp, etc. 2021-04-24 21:05:10 +02:00
MichaelDvP
0a82c28fbf add min/max to values, boiler flags, ww-prefix, ha-prefix to mqtt 2021-04-24 11:41:03 +02:00
MichaelDvP
70d8b6824c id to value_info, alternative prefix to command/value 2021-04-24 10:43:49 +02:00
MichaelDvP
c4e7747fd1 check devicename lowercase 2021-04-24 09:55:33 +02:00
MichaelDvP
661b8791b3 fix errormessage for shell-commands 2021-04-24 09:50:05 +02:00
MichaelDvP
c9a30a23ec API output utf-8 2021-04-24 09:48:42 +02:00
MichaelDvP
28fde37f93 shell reset lineold if edited 2021-04-24 09:48:20 +02:00
MichaelDvP
3797342a93 Fix codecheck complain 2021-04-22 18:45:25 +02:00
MichaelDvP
7faa0d6e65 Typo 2021-04-22 18:38:08 +02:00
MichaelDvP
23455750fa value-info enum as text with list 2021-04-22 18:24:10 +02:00
MichaelDvP
7cabae7ef5 add thermostat remotetemp 2021-04-22 13:51:12 +02:00
MichaelDvP
7baf5c1d9a Add value_info 2021-04-22 13:50:48 +02:00
proddy
36780509a9 larger dialog boxes 2021-04-21 21:17:12 +02:00
proddy
48c3aa7656 auto formatting 2021-04-21 21:17:02 +02:00
proddy
a951ebc3ed feat: add generate token endpoint and ui for generating tokens for users 2021-04-21 21:16:38 +02:00
proddy
8ea48f7c81 don't publish dallas if there are none 2021-04-21 20:37:45 +02:00
MichaelDvP
a633225ad2 Fix #47 Gateway S32 board profile 2021-04-21 08:46:30 +02:00
MichaelDvP
6b327e3ab3 move system commands to main 2021-04-21 07:44:28 +02:00
MichaelDvP
cd43a9feb8 syslog: timestamp to local, add appname 2021-04-21 07:43:37 +02:00
MichaelDvP
cf641476bf More linebuffers to shell 2021-04-21 07:43:03 +02:00
proddy
462a91b122 3.0.3b2 with mockapi & webcommands 2021-04-17 13:12:25 +02:00
proddy
67a8b4eb80 Merge remote-tracking branch 'origin/ft_webcallcmd' into dev 2021-04-17 13:08:04 +02:00
proddy
e59f349a66 increase mqtt payload max size - https://github.com/emsesp/EMS-ESP32/issues/18#issuecomment-821802433 2021-04-17 13:04:44 +02:00
proddy
031f1abd5d right align device/brand 2021-04-17 13:02:31 +02:00
MichaelDvP
73e478c50c Fix syslog level 2021-04-15 17:53:09 +02:00
MichaelDvP
14199ee4ea Fix syslog level 2021-04-15 17:52:13 +02:00
proddy
a9ec926ffb update lockfile ver 2021-04-11 10:31:10 +02:00
Proddy
9f089bad75 back to v5 of compression-webpack-plugin 2021-04-10 21:20:10 +02:00
Proddy
8071fe04bc create-react-app uses webpack 4 so compression-webpack-plugin needs to be locked to version 6.1.1. 2021-04-10 16:18:01 +02:00
Proddy
47a401b66e update npm & typescript 2021-04-10 16:07:10 +02:00
Proddy
ddd2684d60 tooltip color, edit icon color, text changes 2021-04-10 16:06:58 +02:00
Proddy
784ba7fc23 lowercase flowtemp commands 2021-04-10 14:57:05 +02:00
Proddy
4bcc23641a lowercase flowtemp commands 2021-04-10 14:56:51 +02:00
Proddy
dabb48fb61 send mqtt when shower starts 2021-04-10 14:56:42 +02:00
Proddy
9aea9aab50 bump version 2021-04-10 14:56:30 +02:00
Proddy
b4aed240a7 Merge pull request #44 from MichaelDvP/ft_webcallcmd
mqtt-HA-config dynamically
2021-04-10 14:31:33 +02:00
MichaelDvP
015ab649af fix publish_ha_config, and add clear 2021-04-10 13:32:09 +02:00
MichaelDvP
4cac16093f mqtt-HA-config dynamically 2021-04-10 12:29:50 +02:00
MichaelDvP
b77d9d4125 combine commands and values, some extra commands 2021-04-08 15:20:07 +02:00
MichaelDvP
ac26d58b97 allow web commands only for admin 2021-04-08 15:02:11 +02:00
proddy
ed7b2ef4ef update with list of enhancements 2021-04-08 12:16:28 +02:00
Proddy
5fe5750130 rename register_mqtt_cmd to register_cmd and rename some device names 2021-04-07 18:53:14 +02:00
proddy
314fff587c formatting 2021-04-06 18:39:43 +02:00
proddy
8318981f4e formatting 2021-04-06 18:39:32 +02:00
proddy
365e2fdb6b added temp and seltemp (same) 2021-04-06 18:39:18 +02:00
proddy
7e196785d8 only write access in API is enabled 2021-04-06 18:39:03 +02:00
proddy
5ef1c7e3bd more error controls 2021-04-06 18:38:41 +02:00
proddy
11bdff9132 show device name in debug 2021-04-06 18:38:31 +02:00
proddy
060802c8f1 added thermostat temp 2021-04-06 18:38:13 +02:00
Proddy
312aeea39d feat: Call commands from the Web UI #18 2021-04-06 17:39:07 +02:00
proddy
9dbc6d4d8f device values table reformatting 2021-04-06 11:18:44 +02:00
proddy
33c3ef64e9 bump version to 3.0.2b1 2021-04-05 14:04:46 +02:00
proddy
8c1a138621 update 2021-04-05 13:59:58 +02:00
proddy
4f239d035e re-enable shower alert 2021-04-04 13:38:49 +02:00
proddy
7fa93a8de0 MQTT Formatting payload (nested vs single) is a pull-down option 2021-04-04 09:33:04 +02:00
proddy
84e76e2bd7 adjusted network icons 2021-04-04 09:13:53 +02:00
proddy
2021a2e52b fix nodemon for realtime changing of mock values 2021-04-04 09:13:42 +02:00
proddy
e1f777e33a updates 2021-04-03 13:08:25 +02:00
proddy
166f8f6c3a update packages and readme 2021-04-03 12:17:44 +02:00
proddy
3ace3e2b63 remove EMSESP_TEST 2021-04-03 10:56:31 +02:00
proddy
8c52145c7b make all default settings configurable at build 2021-04-03 10:56:18 +02:00
proddy
6e3b496f86 update packages 2021-04-02 11:46:53 +02:00
Proddy
88c8cb424b feat: add remaining mock calls #41 2021-04-01 23:43:50 +02:00
Proddy
74179ab6e9 add mocks for get and posts 2021-04-01 19:30:10 +02:00
proddy
f6fefc9a69 update npm 2021-04-01 18:03:06 +02:00
proddy
601f91e5a7 remove 2021-04-01 18:03:00 +02:00
proddy
d553542206 quick proxy test 2021-04-01 17:49:43 +02:00
MichaelDvP
3bacfc3361 value2enum texts from local_EN 2021-04-01 16:31:56 +02:00
proddy
45a6cd3606 fix: typo 2021-03-31 10:19:59 +02:00
proddy
577017bd0c fix: rx sent -> rx recieved 2021-03-31 10:19:47 +02:00
proddy
9787d1686f style: auto formatting 2021-03-30 17:58:49 +02:00
proddy
108f236874 fix: offline standalone compiling 2021-03-30 17:58:22 +02:00
Proddy
d47fcda0fe Merge pull request #40 from MichaelDvP/dev
Move texts and some other changes
2021-03-30 17:10:58 +02:00
Proddy
5d21ba2648 Merge branch 'dev' into dev 2021-03-30 17:09:52 +02:00
proddy
1b730062b7 3.0.2b0 prep 2021-03-30 16:39:01 +02:00
proddy
4841e42286 Merge remote-tracking branch 'origin/dev' into main 2021-03-30 16:35:18 +02:00
proddy
df1c227f2c fix heartbeat on Ethernet 2021-03-30 15:52:27 +02:00
MichaelDvP
6fb8a4bbe9 changelog, missing macro 2021-03-30 12:46:27 +02:00
MichaelDvP
5c605e15dd move topics/texts to local_EN, some thermostat commands read values 2021-03-30 12:20:43 +02:00
MichaelDvP
9983269662 allow info command from mqtt, publish in topic:response 2021-03-30 12:06:51 +02:00
MichaelDvP
d891c7a325 publish command to trigger device-publishes 2021-03-30 12:05:57 +02:00
proddy
06008fcf6c Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-03-30 12:00:16 +02:00
proddy
15c4a3e9a5 don't disable bluetooth or adc for now 2021-03-30 12:00:09 +02:00
proddy
89f1fc8282 remove redundant code 2021-03-30 11:59:43 +02:00
proddy
ca083166a1 show board profile on boot 2021-03-30 11:59:25 +02:00
proddy
ed177396b2 cleaned up pio scripts 2021-03-30 11:59:06 +02:00
proddy
6dd901880e remove redundant code 2021-03-30 11:58:53 +02:00
MichaelDvP
9771ea8f2d allow read commands with length 2021-03-30 11:57:37 +02:00
MichaelDvP
5cf41bdce0 number format for enum-bool 2021-03-30 11:37:31 +02:00
MichaelDvP
f28fafed8d check all commands lower case 2021-03-30 11:31:20 +02:00
MichaelDvP
81e2c31dd3 use native esp32 64bit timer for uptime 2021-03-30 11:27:42 +02:00
MichaelDvP
d9b577d944 fix syslog reboots without eth 2021-03-30 11:12:16 +02:00
proddy
324a6da0d5 add ubadevices, which is 1st telegram sent 2021-03-29 22:26:21 +02:00
proddy
391fecadd0 formatting 2021-03-29 22:26:08 +02:00
proddy
4d0032441f start syslog when network connected 2021-03-29 22:25:44 +02:00
proddy
8e59460845 start sys;log after network to prevent crash on Ethernet 2021-03-29 11:15:07 +02:00
proddy
4b6c676992 text changes 2021-03-29 11:14:49 +02:00
proddy
0237cc1ca4 add comment 2021-03-29 11:14:29 +02:00
proddy
fbef1ca69a remove comment 2021-03-29 11:14:17 +02:00
proddy
3b4bfaa319 remove ESP8266 references 2021-03-28 21:35:30 +02:00
proddy
2b6a986c4a text changes 2021-03-28 21:35:20 +02:00
proddy
494827299c fix MQTT when only on Ethernet 2021-03-28 21:34:40 +02:00
proddy
a920e89ea2 uppercase esp32 2021-03-28 21:33:33 +02:00
proddy
6a4b7a1ac7 if on ethernet, show it's IP and not WiFi 2021-03-28 21:33:16 +02:00
proddy
621c73ab03 move around build defines 2021-03-28 20:09:33 +02:00
proddy
ac7003124e fix ld issue 2021-03-28 20:09:23 +02:00
proddy
4208c3551a fix lint errors in formatting 2021-03-28 16:55:44 +02:00
proddy
1938c93faf rename subscribes to subscribe_format 2021-03-28 16:53:01 +02:00
proddy
2a070ef55f fallback to AP when Ethernet is dropped 2021-03-28 16:28:13 +02:00
proddy
0c17e8deb3 don't process dallas if gpio is 0 2021-03-28 16:27:54 +02:00
proddy
22b4b66cff added logger so external functions can use 2021-03-28 16:27:40 +02:00
proddy
942d062506 formatting, remove wemos from board profile, fix olimex 2021-03-28 16:27:26 +02:00
proddy
7c3b8954fe added info messages for NTP 2021-03-28 16:26:48 +02:00
proddy
bf90056c61 syslog also works when Ethernet connected 2021-03-28 16:26:33 +02:00
proddy
bcd79bc250 formatting 2021-03-28 16:26:18 +02:00
proddy
07c7ef22cf remove wemos mini d1 2021-03-28 16:26:08 +02:00
proddy
6d420662e1 formatting 2021-03-28 16:25:44 +02:00
proddy
fca458687e fix flashing led on ethernet connection 2021-03-27 16:19:12 +01:00
proddy
e34620e1e8 local ip mods 2021-03-27 16:18:41 +01:00
proddy
bcdb49ffff show "quality" next to the line quality % 2021-03-27 16:04:19 +01:00
proddy
ebb71c7724 fix GH URL 2021-03-27 16:03:49 +01:00
proddy
b8dca3db32 mention that blank SSID = disabling wifi 2021-03-27 16:03:39 +01:00
proddy
94d704730f rename network scan to wifi scan 2021-03-27 16:03:13 +01:00
proddy
0c76ed2c4c custom board profile is allowed 2021-03-27 16:02:57 +01:00
proddy
8bac9f687e hide led is default off/false 2021-03-27 16:02:35 +01:00
proddy
56b597d45f update to new version 2021-03-27 16:02:13 +01:00
proddy
96b83e3eb3 fix ems line quality calculation 2021-03-27 16:02:01 +01:00
proddy
e21ad6a6ba re-enable ethernet detection code 2021-03-27 12:48:16 +01:00
proddy
7fe4b99cef fix bug when CUSTOM is chosen as board profile 2021-03-27 12:48:02 +01:00
proddy
0c8dd1d8cf added generic LAN8720 2021-03-27 12:47:46 +01:00
proddy
cafc6103ea board profiles: pre-configured pin layouts #11 2021-03-27 10:30:28 +01:00
proddy
6d3feaf81c MQTT base from std::string to String 2021-03-27 10:30:15 +01:00
proddy
c8d8b50d47 MQTT base from std::string to String 2021-03-27 10:29:47 +01:00
proddy
0c89d90d56 dallas pin fix for mt-et & nodemcu 2021-03-26 22:39:46 +01:00
proddy
d0fc09fc01 snack popups from 5 to 3 seconds 2021-03-26 17:29:54 +01:00
proddy
c8b6d1e69c rename WiFi to Network 2021-03-26 17:29:40 +01:00
proddy
49d719770c remove comment 2021-03-26 17:29:23 +01:00
proddy
c75a1c9e1e added toUpper 2021-03-26 17:29:13 +01:00
proddy
da7b0e9597 feat: board profiles (#11) 2021-03-26 17:29:00 +01:00
proddy
8f1243850f more tests 2021-03-26 17:27:39 +01:00
proddy
b931e282f2 added extra gpio pins to avoid 2021-03-24 07:48:45 +01:00
proddy
66df8031ed use gpio checker. wrong values will cause crash 2021-03-23 22:22:14 +01:00
proddy
966f82e38c gpio checker 2021-03-23 22:21:51 +01:00
proddy
118cbd9224 improve value detection 2021-03-23 22:21:29 +01:00
proddy
def585fa04 bump to b3 2021-03-23 22:21:08 +01:00
proddy
56a3dfd41a on ESP32 no need to use flash strings for MQTT names 2021-03-23 22:20:58 +01:00
proddy
cc0f4c43ae formatting 2021-03-23 22:20:38 +01:00
proddy
c341148009 minor cleanup 2021-03-23 22:19:57 +01:00
MichaelDvP
9089e5d334 mixer IPM add header temperature for unmixed circuits 2021-03-23 15:30:47 +01:00
MichaelDvP
720a82b3da thermostat datetime command only for supported models 2021-03-23 15:30:08 +01:00
MichaelDvP
a83d3a12fb individual subscriptions: resubscribe, show, some system commands 2021-03-23 15:28:50 +01:00
proddy
1dae9f8beb fix LED when connection made 2021-03-22 22:36:31 +01:00
proddy
e25d6e4d0b quit if no valid board profile 2021-03-22 22:33:23 +01:00
proddy
c01c098f7e added more board profiles for ethernet 2021-03-22 22:09:09 +01:00
proddy
fecfe9d791 added text for board profiles 2021-03-22 21:18:22 +01:00
proddy
b996c4dcf6 feat: board profiles (#11) 2021-03-22 21:12:19 +01:00
MichaelDvP
273efbcb65 update changelog 2021-03-22 19:38:17 +01:00
MichaelDvP
7d177ca049 fix compile error standalone? 2021-03-22 19:28:43 +01:00
MichaelDvP
83f46ffd6c add back reset command 2021-03-22 17:28:11 +01:00
MichaelDvP
03e43ba839 add mqtt subscribe settings, thermostat switchtime, boiler heatingsources 2021-03-22 17:17:56 +01:00
MichaelDvP
71dfc0e1eb fix uart out of bounds warning 2021-03-22 17:05:56 +01:00
proddy
355b71cacf minor tidyups 2021-03-22 09:23:57 +01:00
proddy
c660440996 fix: show all devices, except system (#31) 2021-03-22 09:23:34 +01:00
MichaelDvP
70033017fd fix #33, Junkers mode names sorted 2021-03-22 07:37:56 +01:00
proddy
b9c08a58ad fix: only create mqtt subs for Boiler (expose individual commands via MQTT topics #31) 2021-03-21 17:31:52 +01:00
proddy
4db69760c6 fix: rendering of floats 2021-03-21 13:14:05 +01:00
proddy
8ec0731ca2 update upload scripts 2021-03-21 13:02:21 +01:00
proddy
25b1957dbf minor cleanup 2021-03-21 12:48:03 +01:00
proddy
f2dbc26491 feat: expose cmd's via MQTT directly #31 2021-03-21 12:47:47 +01:00
MichaelDvP
fd11a09882 fix negative rounding 2021-03-19 19:49:07 +01:00
143 changed files with 32371 additions and 4768 deletions

2
.gitignore vendored
View File

@@ -25,6 +25,6 @@ emsesp
/data/www
/lib/framework/WWWData.h
/interface/build
/interface/node_modules
node_modules
/interface/.eslintcache

View File

@@ -5,12 +5,92 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.1.0] May 4 2021
- Mock API to simulate an ESP, for testing web
- Able to write values from the Web UI
- check values with `"cmd":<valuename>` and data empty or `?`
- set hc for values and commands by id or prefix `hc<x>`+separator, separator can be any char
## Fixed
- Don't create Home Assistant MQTT discovery entries for device values that don't exists (#756 on EMS-ESP repo)
- Update shower MQTT when a shower start is detected
- S32 board profile
## Changed
- Icon for Network
- MQTT Formatting payload (nested vs single) is a pull-down option
- moved mqtt-topics and texts to local_EN, all topics lower case
- Re-enabled Shower Alert (still experimental)
- lowercased Flow temp in commands
- system console commands to main
## Removed
## [3.0.1] March 30 2021
## Added
- power settings, disabling BLE and turning off Wifi sleep
- Rx and Tx counts to Heartbeat MQTT payload
- ethernet support
- id to info command to show only a heatingcircuit
- add sending devices that are not listed to 0x07
- extra MQTT boolean option for "ON" and "OFF"
- support for chunked MQTT payloads to allow large data sets > 2kb
- external Button support (#708) for resetting to factory defaults and other actions
- new console set command in `system`, `set board_profile <profile>` for quickly enabling cabled ethernet connections without using the captive wifi portal
- added in MQTT nested mode, for thermostat and mixer, like we had back in v2
- cascade MC400 (product-id 210) (3.0.0b6), power values for heating sources (3.0.1b1)
- values for wwMaxPower, wwFlowtempOffset
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
- syslog port selectable (#744)
- individual mqtt commands (#31)
- board Profiles (#11)
## Fixed
- telegrams matched to masterthermostat 0x18
- multiple roomcontrollers
- readback after write with delay (give ems-devices time to set the value)
- thermostat ES72/RC20 device 66 to command-set RC20_2
- MQTT payloads not adding to queue when MQTT is re-connecting (fixes #369)
- fix for HA topics with invalid command formats (#728)
- wrong position of values #723, #732
- OTA Upload via Web on OSX
- Rx and Tx quality % would sometimes show > 100
## Changed
- changed how telegram parameters are rendered for mqtt, console and web (#632)
- split `show values` in smaller packages (edited)
- extended length of IP/hostname from 32 to 48 chars (#676)
- check flowsensor for `tap_water_active`
- mqtt prefixed with `Base`
- count Dallas sensor fails
- switch from SPIFFS to LITTLEFS
- added ID to MQTT payloads which is the Device's product ID and used in HA to identify a unique HA device
- increased MQTT buffer and reduced wait time between publishes
- updated to the latest ArduinoJson library
- some names of mqtt-tags like in v2.2.1
- new ESP32 partition side to allow for smoother OTA and fallback
- network Gateway IP is optional (#682)emsesp/EMS-ESP
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
- invert LED changed to Hide LED. Default is off.
- renamed Scan Network to Scan WiFi Network
- added version to cmd=settings
- Allow both WiFi and Ethernet together, fall back to AP when Ethernet disconnects
## Removed
- Shower Alert (disabled for now)
## [3.0.0] March 18 2021
## **ESP32 version based off ESP-ESP v2.1**
### Added
- Power settings, disabling BLE and turning off Wifi sleep
- Rx and Tx counts to Heartbeat MQTT payload
- Ethernet support
@@ -27,6 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Syslog port selectable (#744)
### Fixed
- telegrams matched to masterthermostat 0x18
- multiple roomcontrollers
- readback after write with delay (give ems-devices time to set the value)
@@ -37,6 +118,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- OTA Upload via Web on OSX
### Changed
- changed how telegram parameters are rendered for mqtt, console and web (#632)
- split `show values` in smaller packages (edited)
- extended length of IP/hostname from 32 to 48 chars (#676)
@@ -51,4 +133,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- new ESP32 partition side to allow for smoother OTA and fallback
- Network Gateway IP is optional (#682)emsesp/EMS-ESP
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32

View File

@@ -1,12 +1,9 @@
# Changelog
### Added
## Added
## Fixed
### Fixed
### Changed
### Removed
## Changed
## Removed

View File

@@ -2,7 +2,13 @@
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
This is the firmware for the ESP32.
This is the firmware for the ESP32. Compared to version 2 on the ESP8266, this version has
- Ethernet Support
- Pre-configured board layouts
- Writing values directly from the Web UI
- Mock API server for faster offline development
- Expose to more commands, via MQTT
- Improvements to Dallas sensors, Shower service
[![version](https://img.shields.io/github/release/emsesp/EMS-ESP32.svg?label=Latest%20Release)](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
[![release-date](https://img.shields.io/github/release-date/emsesp/EMS-ESP32.svg?label=Released)](https://github.com/emsesp/EMS-ESP32/commits/main)

View File

@@ -1,10 +1,5 @@
# Change the IP address to that of your ESP device to enable local development of the UI.
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
# Change the IP address to that of your ESP device to enable local development of the UI
# ESP32 dev
REACT_APP_HTTP_ROOT=http://10.10.10.101
REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.101
# REACT_APP_HTTP_ROOT=http://localhost:3000
# REACT_APP_WEB_SOCKET_ROOT=ws://localhost:3000
# ESP8266 dev
#REACT_APP_HTTP_ROOT=http://10.10.10.140
#REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.140

View File

@@ -9,7 +9,7 @@ const fs = require('fs');
module.exports = function override(config, env) {
if (env === "production") {
// rename the ouput file, we need it's path to be short, for SPIFFS
// rename the output file, we need it's path to be short for LittleFS
config.output.filename = 'js/[id].[chunkhash:4].js';
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';

26443
interface/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,37 +3,40 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.11.3",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@types/lodash": "^4.14.168",
"@types/node": "^12.20.4",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.1",
"@types/node": "^15.0.1",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/react-material-ui-form-validator": "^2.1.0",
"@types/react-router": "^5.1.12",
"@types/react-router-dom": "^5.1.6",
"compression-webpack-plugin": "^4.0.0",
"@types/react-router": "^5.1.13",
"@types/react-router-dom": "^5.1.7",
"compression-webpack-plugin": "^5.0.2",
"express": "^4.17.1",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"mime-types": "^2.1.29",
"notistack": "^1.0.5",
"parse-ms": "^2.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-dropzone": "^11.3.1",
"mime-types": "^2.1.30",
"notistack": "^1.0.6",
"parse-ms": "^3.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-dropzone": "^11.3.2",
"react-form-validator-core": "^1.1.1",
"react-material-ui-form-validator": "^2.1.4",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"react-scripts": "4.0.3",
"sockette": "^2.0.6",
"typescript": "4.0.5",
"typescript": "4.2.4",
"zlib": "^1.0.5"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
"dev": "run-p start mock-api"
},
"eslintConfig": {
"extends": "react-app"
@@ -51,6 +54,10 @@
]
},
"devDependencies": {
"concurrently": "^6.0.1",
"http-proxy-middleware": "^1.1.1",
"nodemon": "^2.0.7",
"npm-run-all": "^4.1.5",
"react-app-rewired": "^2.1.8"
}
}

View File

@@ -28,7 +28,7 @@ class App extends Component {
render() {
return (
<CustomMuiTheme>
<SnackbarProvider maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
ref={this.notistackRef}
action={(key) => (
<IconButton onClick={this.onClickDismiss(key)} size="small">

View File

@@ -30,7 +30,7 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
onChange={handleValueChange('provision_mode')}
margin="normal">
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When Network Disconnected</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
</SelectValidator>
{

View File

@@ -18,5 +18,6 @@ export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";

View File

@@ -5,10 +5,8 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
import * as Authentication from './Authentication';
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
type ChildComponent = React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
component: ChildComponent;
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
@@ -27,7 +25,7 @@ export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps>
);
}
Authentication.storeLoginRedirect(location);
enqueueSnackbar("Please sign in to continue.", { variant: 'info' });
enqueueSnackbar("Please sign in to continue", { variant: 'info' });
return (
<Redirect to='/' />
);

View File

@@ -101,7 +101,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
me: undefined
}
});
this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
this.props.enqueueSnackbar("You have signed out", { variant: 'success', });
history.push('/');
}

View File

@@ -19,7 +19,9 @@ class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
if (authenticationContext.me) {
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
}
return (<Component {...props} />);
if (Component) {
return (<Component {...props} />);
}
}
return (
<Route {...rest} render={renderComponent} />

View File

@@ -8,7 +8,7 @@ import { Card, CardContent, CardActions } from '@material-ui/core';
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
import WifiIcon from '@material-ui/icons/Wifi';
import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
import SettingsIcon from '@material-ui/icons/Settings';
import AccessTimeIcon from '@material-ui/icons/AccessTime';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
@@ -146,7 +146,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
<List>
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
<ListItemIcon>
<WifiIcon />
<SettingsEthernetIcon />
</ListItemIcon>
<ListItemText primary="Network Connection" />
</ListItem>

View File

@@ -1,22 +1,31 @@
import React from 'react';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import React from "react";
import {
TextValidator,
ValidatorForm,
SelectValidator,
} from "react-material-ui-form-validator";
import { Checkbox, TextField, Typography } from '@material-ui/core';
import SaveIcon from '@material-ui/icons/Save';
import MenuItem from '@material-ui/core/MenuItem';
import { Checkbox, TextField, Typography } from "@material-ui/core";
import SaveIcon from "@material-ui/icons/Save";
import MenuItem from "@material-ui/core/MenuItem";
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel, PasswordValidator } from '../components';
import { isIP, isHostname, or, isPath } from '../validators';
import {
RestFormProps,
FormActions,
FormButton,
BlockFormControlLabel,
PasswordValidator,
} from "../components";
import { isIP, isHostname, or, isPath } from "../validators";
import { MqttSettings } from './types';
import { MqttSettings } from "./types";
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
ValidatorForm.addValidationRule('isPath', isPath);
ValidatorForm.addValidationRule("isIPOrHostname", or(isIP, isHostname));
ValidatorForm.addValidationRule("isPath", isPath);
}
render() {
@@ -27,44 +36,57 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.enabled}
onChange={handleValueChange('enabled')}
onChange={handleValueChange("enabled")}
value="enabled"
/>
}
label="Enable MQTT"
/>
<TextValidator
validators={['required', 'isIPOrHostname']}
errorMessages={['Host is required', "Not a valid IP address or hostname"]}
validators={["required", "isIPOrHostname"]}
errorMessages={[
"Host is required",
"Not a valid IP address or hostname",
]}
name="host"
label="Host"
fullWidth
variant="outlined"
value={data.host}
onChange={handleValueChange('host')}
onChange={handleValueChange("host")}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Port is required', "Must be a number", "Must be greater than 0 ", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Port is required",
"Must be a number",
"Must be greater than 0 ",
"Max value is 65535",
]}
name="port"
label="Port"
fullWidth
variant="outlined"
value={data.port}
type="number"
onChange={handleValueChange('port')}
onChange={handleValueChange("port")}
margin="normal"
/>
<TextValidator
validators={['required', 'isPath']}
errorMessages={['Base is required', "Not a valid Path"]}
validators={["required", "isPath"]}
errorMessages={["Base is required", "Not a valid Path"]}
name="base"
label="Base"
fullWidth
variant="outlined"
value={data.base}
onChange={handleValueChange('base')}
onChange={handleValueChange("base")}
margin="normal"
/>
<TextField
@@ -73,7 +95,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.username}
onChange={handleValueChange('username')}
onChange={handleValueChange("username")}
margin="normal"
/>
<PasswordValidator
@@ -82,7 +104,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.password}
onChange={handleValueChange('password')}
onChange={handleValueChange("password")}
margin="normal"
/>
<TextField
@@ -91,28 +113,40 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.client_id}
onChange={handleValueChange('client_id')}
onChange={handleValueChange("client_id")}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
errorMessages={['Keep alive is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:1",
"maxNumber:65535",
]}
errorMessages={[
"Keep alive is required",
"Must be a number",
"Must be greater than 0",
"Max value is 65535",
]}
name="keep_alive"
label="Keep Alive (seconds)"
fullWidth
variant="outlined"
value={data.keep_alive}
type="number"
onChange={handleValueChange('keep_alive')}
onChange={handleValueChange("keep_alive")}
margin="normal"
/>
<SelectValidator name="mqtt_qos"
<SelectValidator
name="mqtt_qos"
label="QoS"
value={data.mqtt_qos}
fullWidth
variant="outlined"
onChange={handleValueChange('mqtt_qos')}
margin="normal">
onChange={handleValueChange("mqtt_qos")}
margin="normal"
>
<MenuItem value={0}>0 (default)</MenuItem>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
@@ -121,7 +155,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.clean_session}
onChange={handleValueChange('clean_session')}
onChange={handleValueChange("clean_session")}
value="clean_session"
/>
}
@@ -131,149 +165,235 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.mqtt_retain}
onChange={handleValueChange('mqtt_retain')}
onChange={handleValueChange("mqtt_retain")}
value="mqtt_retain"
/>
}
label="Retain Flag"
/>
<br></br>
<Typography variant="h6" color="primary" >
<Typography variant="h6" color="primary">
Formatting
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.nested_format}
onChange={handleValueChange('nested_format')}
value="nested_format"
/>
}
label="Nested format (Thermostat & Mixer only)"
/>
<SelectValidator name="dallas_format"
<SelectValidator
name="nested_format"
label="Topic/Payload Format"
value={data.nested_format}
fullWidth
variant="outlined"
onChange={handleValueChange("nested_format")}
margin="normal"
>
<MenuItem value={1}>nested on a single topic</MenuItem>
<MenuItem value={2}>as individual topics</MenuItem>
</SelectValidator>
<SelectValidator
name="dallas_format"
label="Dallas Sensor Payload Grouping"
value={data.dallas_format}
fullWidth
variant="outlined"
onChange={handleValueChange('dallas_format')}
margin="normal">
onChange={handleValueChange("dallas_format")}
margin="normal"
>
<MenuItem value={1}>by Sensor ID</MenuItem>
<MenuItem value={2}>by Number</MenuItem>
</SelectValidator>
<SelectValidator name="bool_format"
<SelectValidator
name="bool_format"
label="Boolean Format"
value={data.bool_format}
fullWidth
variant="outlined"
onChange={handleValueChange('bool_format')}
margin="normal">
onChange={handleValueChange("bool_format")}
margin="normal"
>
<MenuItem value={1}>"on"/"off"</MenuItem>
<MenuItem value={2}>true/false</MenuItem>
<MenuItem value={3}>1/0</MenuItem>
<MenuItem value={4}>"ON"/"OFF"</MenuItem>
</SelectValidator>
<SelectValidator
name="subscribe_format"
label="Subscribe Format"
value={data.subscribe_format}
fullWidth
variant="outlined"
onChange={handleValueChange("subscribe_format")}
margin="normal"
>
<MenuItem value={0}>general device topic</MenuItem>
<MenuItem value={1}>individual topics, main heating circuit</MenuItem>
<MenuItem value={2}>individual topics, all heating circuits</MenuItem>
</SelectValidator>
<BlockFormControlLabel
control={
<Checkbox
checked={data.ha_enabled}
onChange={handleValueChange('ha_enabled')}
onChange={handleValueChange("ha_enabled")}
value="ha_enabled"
/>
}
label="Home Assistant MQTT Discovery"
label="Use Home Assistant MQTT Discovery"
/>
{ data.ha_enabled &&
<SelectValidator name="ha_climate_format"
{data.ha_enabled && (
<SelectValidator
name="ha_climate_format"
label="Thermostat Room Temperature"
value={data.ha_climate_format}
fullWidth
variant="outlined"
onChange={handleValueChange('ha_climate_format')}
margin="normal">
onChange={handleValueChange("ha_climate_format")}
margin="normal"
>
<MenuItem value={1}>use Current temperature (default)</MenuItem>
<MenuItem value={2}>use Setpoint temperature</MenuItem>
<MenuItem value={3}>Fix to 0</MenuItem>
</SelectValidator>
}
)}
<br></br>
<Typography variant="h6" color="primary" >
<Typography variant="h6" color="primary">
Publish Intervals
</Typography>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
]}
name="publish_time_boiler"
label="Boiler Publish Interval (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_boiler}
type="number"
onChange={handleValueChange('publish_time_boiler')}
onChange={handleValueChange("publish_time_boiler")}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
]}
name="publish_time_thermostat"
label="Thermostat Publish Interval (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_thermostat}
type="number"
onChange={handleValueChange('publish_time_thermostat')}
onChange={handleValueChange("publish_time_thermostat")}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
]}
name="publish_time_solar"
label="Solar Publish Interval (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_solar}
type="number"
onChange={handleValueChange('publish_time_solar')}
onChange={handleValueChange("publish_time_solar")}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
]}
name="publish_time_mixer"
label="Mixer Publish Interval (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_mixer}
type="number"
onChange={handleValueChange('publish_time_mixer')}
onChange={handleValueChange("publish_time_mixer")}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
]}
name="publish_time_sensor"
label="Sensors Publish Interval (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_sensor}
type="number"
onChange={handleValueChange('publish_time_sensor')}
onChange={handleValueChange("publish_time_sensor")}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
]}
name="publish_time_other"
label="All other Modules Publish Interval (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_other}
type="number"
onChange={handleValueChange('publish_time_other')}
onChange={handleValueChange("publish_time_other")}
margin="normal"
/>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -40,5 +40,6 @@ export interface MqttSettings {
mqtt_retain: boolean;
ha_enabled: boolean;
ha_climate_format: number;
nested_format: boolean;
nested_format: number;
subscribe_format: number;
}

View File

@@ -45,7 +45,7 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
<MenuAppBar sectionTitle="Network Connection">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tab value="/network/status" label="Network Status" />
<Tab value="/network/scan" label="Scan Networks" disabled={!authenticatedContext.me.admin} />
<Tab value="/network/scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
<Tab value="/network/settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
</Tabs>
<Switch>

View File

@@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import { TextValidator, SelectValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { Checkbox, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction } from '@material-ui/core';
@@ -9,7 +9,6 @@ import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import DeleteIcon from '@material-ui/icons/Delete';
import SaveIcon from '@material-ui/icons/Save';
import MenuItem from '@material-ui/core/MenuItem';
import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
import { isIP, isHostname, optional } from '../validators';
@@ -34,7 +33,6 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
ssid: selectedNetwork.ssid,
password: "",
hostname: props.data.hostname,
ethernet_profile: 0,
static_ip_config: false,
}
props.setData(networkSettings);
@@ -86,7 +84,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
validators={['matchRegexp:^.{0,32}$']}
errorMessages={['SSID must be 32 characters or less']}
name="ssid"
label="SSID"
label="SSID (leave blank to disable WiFi)"
fullWidth
variant="outlined"
value={data.ssid}
@@ -119,17 +117,6 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
onChange={handleValueChange('hostname')}
margin="normal"
/>
<SelectValidator name="ems_bus_id"
label="Ethernet Profile"
value={data.ethernet_profile}
fullWidth
variant="outlined"
onChange={handleValueChange('ethernet_profile')}
margin="normal">
<MenuItem value={0}>None (wifi only)</MenuItem>
<MenuItem value={1}>Profile 1 (LAN8720)</MenuItem>
<MenuItem value={2}>Profile 2 (TLK110)</MenuItem>
</SelectValidator>
<BlockFormControlLabel
control={
<Checkbox

View File

@@ -1,13 +1,22 @@
import { Theme } from '@material-ui/core';
import { NetworkStatus, NetworkConnectionStatus } from './types';
import { Theme } from "@material-ui/core";
import { NetworkStatus, NetworkConnectionStatus } from "./types";
export const isConnected = ({ status }: NetworkStatus) => {
return ((status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED) || (status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED));
}
return (
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
);
};
export const isWiFi = ({ status }: NetworkStatus) => (status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED)
export const isWiFi = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
export const isEthernet = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
export const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
export const networkStatusHighlight = (
{ status }: NetworkStatus,
theme: Theme
) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
@@ -22,7 +31,7 @@ export const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme)
default:
return theme.palette.warning.main;
}
}
};
export const networkStatus = ({ status }: NetworkStatus) => {
switch (status) {
@@ -45,4 +54,4 @@ export const networkStatus = ({ status }: NetworkStatus) => {
default:
return "Unknown";
}
}
};

View File

@@ -1,45 +1,64 @@
import React, { Component, Fragment } from 'react';
import React, { Component, Fragment } from "react";
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import { WithTheme, withTheme } from "@material-ui/core/styles";
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from "@material-ui/core";
import DNSIcon from '@material-ui/icons/Dns';
import WifiIcon from '@material-ui/icons/Wifi';
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import RefreshIcon from '@material-ui/icons/Refresh';
import DNSIcon from "@material-ui/icons/Dns";
import WifiIcon from "@material-ui/icons/Wifi";
import RouterIcon from "@material-ui/icons/Router";
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import SettingsInputAntennaIcon from "@material-ui/icons/SettingsInputAntenna";
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
import RefreshIcon from "@material-ui/icons/Refresh";
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
import { networkStatus, networkStatusHighlight, isConnected, isWiFi } from './NetworkStatus';
import { NetworkStatus } from './types';
import {
RestFormProps,
FormActions,
FormButton,
HighlightAvatar,
} from "../components";
import {
networkStatus,
networkStatusHighlight,
isConnected,
isWiFi,
isEthernet,
} from "./NetworkStatus";
import { NetworkStatus } from "./types";
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
dnsServers(status: NetworkStatus) {
if (!status.dns_ip_1) {
return "none";
}
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
return status.dns_ip_1 + (status.dns_ip_2 ? "," + status.dns_ip_2 : "");
}
createListItems() {
const { data, theme } = this.props
const { data, theme } = this.props;
return (
<Fragment>
<ListItem>
<ListItemAvatar>
<HighlightAvatar color={networkStatusHighlight(data, theme)}>
<WifiIcon />
{isWiFi(data) && <WifiIcon />}
{isEthernet(data) && <RouterIcon />}
</HighlightAvatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={networkStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{
isWiFi(data) &&
{isWiFi(data) && (
<Fragment>
<ListItem>
<ListItemAvatar>
@@ -51,8 +70,8 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
}
{ isConnected(data) &&
)}
{isConnected(data) && (
<Fragment>
<ListItem>
<ListItemAvatar>
@@ -67,14 +86,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="MAC Address" secondary={data.mac_address} />
<ListItemText
primary="MAC Address"
secondary={data.mac_address}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>#</Avatar>
</ListItemAvatar>
<ListItemText primary="Subnet Mask" secondary={data.subnet_mask} />
<ListItemText
primary="Subnet Mask"
secondary={data.subnet_mask}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -83,7 +108,10 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
<SettingsInputComponentIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
<ListItemText
primary="Gateway IP"
secondary={data.gateway_ip || "none"}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -92,11 +120,14 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
<DNSIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="DNS Server IP" secondary={this.dnsServers(data)} />
<ListItemText
primary="DNS Server IP"
secondary={this.dnsServers(data)}
/>
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
}
)}
</Fragment>
);
}
@@ -104,18 +135,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
render() {
return (
<Fragment>
<List>
{this.createListItems()}
</List>
<List>{this.createListItems()}</List>
<FormActions>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
Refresh
</FormButton>
</FormActions>
</Fragment>
);
}
}
export default withTheme(NetworkStatusForm);

View File

@@ -36,7 +36,6 @@ export interface NetworkSettings {
ssid: string;
password: string;
hostname: string;
ethernet_profile: number;
static_ip_config: boolean;
local_ip?: string;
gateway_ip?: string;

View File

@@ -89,6 +89,8 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
<Dialog
open={this.state.settingTime}
onClose={this.closeSetTime}
fullWidth
maxWidth="sm"
>
<DialogTitle>Set Time</DialogTitle>
<DialogContent dividers>

View File

@@ -0,0 +1,23 @@
import React from 'react';
import MenuItem from '@material-ui/core/MenuItem';
type BoardProfiles = {
[name: string]: string
};
export const BOARD_PROFILES: BoardProfiles = {
"S32": "BBQKees Gateway S32",
"E32": "BBQKees Gateway E32",
"NODEMCU": "NodeMCU 32S",
"MH-ET": "MH-ET Live D1 Mini",
"LOLIN": "Lolin D32",
"OLIMEX": "Olimex ESP32-EVB",
"TLK110": "Generic Ethernet (TLK110)",
"LAN8720": "Generic Ethernet (LAN8720)"
}
export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map(code => (
<MenuItem key={code} value={code}>{BOARD_PROFILES[code]}</MenuItem>
));
}

View File

@@ -2,42 +2,27 @@ import React, { Component, Fragment } from "react";
import { withStyles, Theme, createStyles } from "@material-ui/core/styles";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TableContainer,
withWidth,
WithWidthProps,
isWidthDown,
Button,
Tooltip,
DialogTitle,
DialogContent,
DialogActions,
Box,
Dialog,
Typography,
Table, TableBody, TableCell, TableHead, TableRow, TableContainer, withWidth, WithWidthProps, isWidthDown,
Button, Tooltip, DialogTitle, DialogContent, DialogActions, Box, Dialog, Typography
} from "@material-ui/core";
import RefreshIcon from "@material-ui/icons/Refresh";
import ListIcon from "@material-ui/icons/List";
import IconButton from '@material-ui/core/IconButton';
import EditIcon from '@material-ui/icons/Edit';
import {
redirectingAuthorizedFetch,
withAuthenticatedContext,
AuthenticatedContextProps,
} from "../authentication";
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication";
import { RestFormProps, FormButton, extractEventValue } from "../components";
import { RestFormProps, FormButton } from "../components";
import { EMSESPDevices, EMSESPDeviceData, Device, DeviceValue } from "./EMSESPtypes";
import { EMSESPDevices, EMSESPDeviceData, Device } from "./EMSESPtypes";
import ValueForm from './ValueForm';
import { ENDPOINT_ROOT } from "../api";
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + "writeValue";
const StyledTableCell = withStyles((theme: Theme) =>
createStyles({
@@ -51,6 +36,16 @@ const StyledTableCell = withStyles((theme: Theme) =>
})
)(TableCell);
const CustomTooltip = withStyles((theme: Theme) => ({
tooltip: {
backgroundColor: theme.palette.secondary.main,
color: 'white',
boxShadow: theme.shadows[1],
fontSize: 11,
border: '1px solid #dadde9',
},
}))(Tooltip);
function compareDevices(a: Device, b: Device) {
if (a.type < b.type) {
return -1;
@@ -65,35 +60,89 @@ interface EMSESPDevicesFormState {
confirmScanDevices: boolean;
processing: boolean;
deviceData?: EMSESPDeviceData;
selectedDevice?: number;
devicevalue?: DeviceValue;
}
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> &
AuthenticatedContextProps &
WithWidthProps;
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> & AuthenticatedContextProps & WithWidthProps;
function formatTemp(t: string) {
if (t == null) {
return "(not available)";
return "n/a";
}
return t + " °C";
}
function formatUnit(u: string) {
function formatUnit(u: string) {
if (u == null) {
return u;
}
return " " + u;
}
class EMSESPDevicesForm extends Component<
EMSESPDevicesFormProps,
EMSESPDevicesFormState
> {
class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesFormState> {
state: EMSESPDevicesFormState = {
confirmScanDevices: false,
processing: false,
processing: false
};
handleValueChange = (name: keyof DeviceValue) => (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ devicevalue: { ...this.state.devicevalue!, [name]: extractEventValue(event) } });
};
cancelEditingValue = () => {
this.setState({
devicevalue: undefined
});
}
doneEditingValue = () => {
const { devicevalue } = this.state;
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
method: "POST",
body: JSON.stringify({ devicevalue: devicevalue }),
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Write command sent to device", { variant: "success" });
} else if (response.status === 204) {
this.props.enqueueSnackbar("Write command failed", { variant: "error" });
} else if (response.status === 403) {
this.props.enqueueSnackbar("Write access denied", { variant: "error" });
} else {
throw Error("Unexpected response code: " + response.status);
}
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || "Problem writing value", { variant: "error" }
);
});
if (devicevalue) {
this.setState({
devicevalue: undefined
});
}
};
sendCommand = (i: any) => {
this.setState({
devicevalue: {
id: this.state.selectedDevice!,
data: this.state.deviceData?.data[i]!,
uom: this.state.deviceData?.data[i + 1]!,
name: this.state.deviceData?.data[i + 2]!,
cmd: this.state.deviceData?.data[i + 3]!,
}
});
}
noDevices = () => {
return this.props.data.devices.length === 0;
};
@@ -106,7 +155,7 @@ class EMSESPDevicesForm extends Component<
return (this.state.deviceData?.data || []).length === 0;
};
createDeviceItems() {
renderDeviceItems() {
const { width, data } = this.props;
return (
<TableContainer>
@@ -119,50 +168,20 @@ class EMSESPDevicesForm extends Component<
size="small"
padding={isWidthDown("xs", width!) ? "none" : "default"}
>
<TableHead>
<TableRow>
<StyledTableCell>Type</StyledTableCell>
<StyledTableCell align="center">Brand</StyledTableCell>
<StyledTableCell align="center">Model</StyledTableCell>
<StyledTableCell align="center">Device ID</StyledTableCell>
<StyledTableCell align="center">Product ID</StyledTableCell>
<StyledTableCell align="center">Version</StyledTableCell>
<StyledTableCell></StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{data.devices.sort(compareDevices).map((device) => (
<TableRow
hover
key={device.id}
onClick={() => this.handleRowClick(device.id)}
>
<TableCell component="th" scope="row">
<Tooltip
title="click for details..."
arrow
<TableRow hover key={device.id} onClick={() => this.handleRowClick(device)}>
<TableCell>
<CustomTooltip
title={"DeviceID:0x" + ("00" + device.deviceid.toString(16).toUpperCase()).slice(-2) + " ProductID:" + device.productid + " Version:" + device.version}
placement="right-end"
>
<Button
startIcon={<ListIcon />}
size="small"
variant="outlined"
>
<Button startIcon={<ListIcon />} size="small" variant="outlined">
{device.type}
</Button>
</Tooltip>
</CustomTooltip>
</TableCell>
<TableCell align="center">{device.brand}</TableCell>
<TableCell align="center">{device.name}</TableCell>
<TableCell align="center">
0x
{("00" + device.deviceid.toString(16).toUpperCase()).slice(
-2
)}
</TableCell>
<TableCell align="center">{device.productid}</TableCell>
<TableCell align="center">{device.version}</TableCell>
<TableCell></TableCell>
<TableCell align="right">{device.brand + " " + device.name} </TableCell>
</TableRow>
))}
</TableBody>
@@ -172,13 +191,10 @@ class EMSESPDevicesForm extends Component<
<Box
bgcolor="error.main"
color="error.contrastText"
p={2}
mt={2}
mb={2}
p={2} mt={2} mb={2}
>
<Typography variant="body1">
No EMS devices found. Check the connections and for possible Tx
errors.
No EMS devices found. Check the connections and for possible Tx errors.
</Typography>
</Box>
)}
@@ -186,7 +202,7 @@ class EMSESPDevicesForm extends Component<
);
}
createSensorItems() {
renderSensorItems() {
const { data } = this.props;
return (
<TableContainer>
@@ -234,28 +250,19 @@ class EMSESPDevicesForm extends Component<
<Dialog
open={this.state.confirmScanDevices}
onClose={this.onScanDevicesRejected}
fullWidth
maxWidth="sm"
>
<DialogTitle>Confirm Scan Devices</DialogTitle>
<DialogContent dividers>
Are you sure you want to initiate a scan on the EMS bus for all new
devices?
Are you sure you want to initiate a scan on the EMS bus for all new devices?
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={this.onScanDevicesRejected}
color="secondary"
>
<Button variant="contained" onClick={this.onScanDevicesRejected} color="secondary">
Cancel
</Button>
<Button
startIcon={<RefreshIcon />}
variant="contained"
onClick={this.onScanDevicesConfirmed}
disabled={this.state.processing}
color="primary"
autoFocus
>
startIcon={<RefreshIcon />} variant="contained" onClick={this.onScanDevicesConfirmed} disabled={this.state.processing} color="primary" autoFocus>
Start Scan
</Button>
</DialogActions>
@@ -292,11 +299,11 @@ class EMSESPDevicesForm extends Component<
});
};
handleRowClick = (id: any) => {
this.setState({ deviceData: undefined });
handleRowClick = (device: any) => {
this.setState({ selectedDevice: device.id, deviceData: undefined });
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
method: "POST",
body: JSON.stringify({ id: id }),
body: JSON.stringify({ id: device.id }),
headers: {
"Content-Type": "application/json",
},
@@ -304,7 +311,6 @@ class EMSESPDevicesForm extends Component<
.then((response) => {
if (response.status === 200) {
return response.json();
// this.setState({ errorMessage: undefined }, this.props.loadData);
}
throw Error("Unexpected response code: " + response.status);
})
@@ -323,6 +329,7 @@ class EMSESPDevicesForm extends Component<
renderDeviceData() {
const { deviceData } = this.state;
const { width } = this.props;
const me = this.props.authenticatedContext.me;
if (this.noDevices()) {
return;
@@ -346,16 +353,28 @@ class EMSESPDevicesForm extends Component<
size="small"
padding={isWidthDown("xs", width!) ? "none" : "default"}
>
<TableHead></TableHead>
<TableHead>
</TableHead>
<TableBody>
{deviceData.data.map((item, i) => {
if (i % 3) {
if (i % 4) {
return null;
} else {
return (
<TableRow key={i}>
<TableCell component="th" scope="row">{deviceData.data[i + 2]}</TableCell>
<TableCell align="right">{deviceData.data[i]}{formatUnit(deviceData.data[i + 1])}</TableCell>
<TableRow hover key={i}>
<TableCell padding="checkbox" style={{ width: 18 }} >
{deviceData.data[i + 3] && me.admin && (
<CustomTooltip title="change value" placement="left-end"
>
<IconButton edge="start" size="small" aria-label="Edit"
onClick={() => this.sendCommand(i)}>
<EditIcon color="primary" fontSize="small" />
</IconButton>
</CustomTooltip>
)}
</TableCell>
<TableCell padding="none" component="th" scope="row">{deviceData.data[i + 2]}</TableCell>
<TableCell padding="none" align="right">{deviceData.data[i]}{formatUnit(deviceData.data[i + 1])}</TableCell>
</TableRow>
);
}
@@ -365,47 +384,47 @@ class EMSESPDevicesForm extends Component<
</TableContainer>
)}
{this.noDeviceData() && (
<Box color="warning.main" p={0} mt={0} mb={0}>
<Typography variant="body1">
<i>No data available for this device</i>
</Typography>
</Box>
<Box color="warning.main" p={0} mt={0} mb={0}>
<Typography variant="body1">
<i>No data available for this device</i>
</Typography>
</Box>
)}
</Fragment >
);
}
render() {
const { devicevalue } = this.state;
return (
<Fragment>
<br></br>
{this.createDeviceItems()}
{this.renderDeviceItems()}
{this.renderDeviceData()}
{this.createSensorItems()}
{this.renderSensorItems()}
<br></br>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} padding={1}>
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData} >
Refresh
</FormButton>
</Box>
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="primary"
onClick={this.onScanDevices}
>
<FormButton startIcon={<RefreshIcon />} variant="contained" onClick={this.onScanDevices} >
Scan Devices
</FormButton>
</Box>
</Box>
{this.renderScanDevicesDialog()}
{
devicevalue &&
<ValueForm
devicevalue={devicevalue}
onDoneEditing={this.doneEditingValue}
onCancelEditing={this.cancelEditingValue}
handleValueChange={this.handleValueChange}
/>
}
</Fragment>
);
}

View File

@@ -1,14 +1,10 @@
import React, { Component } from 'react';
import { ValidatorForm, TextValidator, SelectValidator } from 'react-material-ui-form-validator';
import { Checkbox, Typography, Box, Link } from '@material-ui/core';
import SaveIcon from '@material-ui/icons/Save';
import MenuItem from '@material-ui/core/MenuItem';
// import { Container } from '@material-ui/core';
import { ENDPOINT_ROOT } from '../api';
import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, BlockFormControlLabel, SectionContent } from '../components';
import EMSESPSettingsForm from './EMSESPSettingsForm';
import { isIP, optional } from '../validators';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import { EMSESPSettings } from './EMSESPtypes';
@@ -19,303 +15,24 @@ type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
this.props.loadData();
}
render() {
return (
<SectionContent title='EMS-ESP Settings' titleGutter>
// <Container maxWidth="md" disableGutters>
<SectionContent title='' titleGutter>
<RestFormLoader
{...this.props}
render={props => (
<EMSESPSettingsControllerForm {...props} />
render={formProps => (
<EMSESPSettingsForm {...formProps} />
)}
/>
</SectionContent>
// </Container>
)
}
}
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
type EMSESPSettingsControllerFormProps = RestFormProps<EMSESPSettings>;
function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps) {
const { data, saveData, handleValueChange } = props;
return (
<ValidatorForm onSubmit={saveData}>
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
<Typography variant="body1">
Change the default settings on this page. For help click <Link target="_blank" href="https://emsesp.github.io/docs/#/Configure-firmware?id=settings" color="primary">{'here'}</Link>.
</Typography>
</Box>
<br></br>
<Typography variant="h6" color="primary" >
EMS Bus
</Typography>
<SelectValidator name="tx_mode"
label="Tx Mode"
value={data.tx_mode}
fullWidth
variant="outlined"
onChange={handleValueChange('tx_mode')}
margin="normal">
<MenuItem value={0}>0 - Off</MenuItem>
<MenuItem value={1}>1 - Default</MenuItem>
<MenuItem value={2}>2 - EMS+</MenuItem>
<MenuItem value={3}>3 - HT3</MenuItem>
<MenuItem value={4}>4 - Hardware</MenuItem>
</SelectValidator>
<SelectValidator name="ems_bus_id"
label="Bus ID"
value={data.ems_bus_id}
fullWidth
variant="outlined"
onChange={handleValueChange('ems_bus_id')}
margin="normal">
<MenuItem value={0x0B}>Service Key (0x0B)</MenuItem>
<MenuItem value={0x0D}>Modem (0x0D)</MenuItem>
<MenuItem value={0x0A}>Terminal (0x0A)</MenuItem>
<MenuItem value={0x0F}>Time Module (0x0F)</MenuItem>
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
</SelectValidator>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['Rx GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
name="rx_gpio"
label="Rx GPIO pin"
fullWidth
variant="outlined"
value={data.rx_gpio}
type="number"
onChange={handleValueChange('rx_gpio')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['Tx GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
name="tx_gpio"
label="Tx GPIO pin"
fullWidth
variant="outlined"
value={data.tx_gpio}
type="number"
onChange={handleValueChange('tx_gpio')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:120']}
errorMessages={['Tx delay is required', "Must be a number", "Must be 0 or higher", "Max value is 120"]}
name="tx_delay"
label="Tx delayed start (seconds)"
fullWidth
variant="outlined"
value={data.tx_delay}
type="number"
onChange={handleValueChange('tx_delay')}
margin="normal"
/>
<br></br>
<Typography variant="h6" color="primary" >
External Button
</Typography>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['Button GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
name="pbutton_gpio"
label="Button GPIO pin"
fullWidth
variant="outlined"
value={data.pbutton_gpio}
type="number"
onChange={handleValueChange('pbutton_gpio')}
margin="normal"
/>
<br></br>
<Typography variant="h6" color="primary" >
Dallas Sensor
</Typography>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['Dallas GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
name="dallas_gpio"
label="Dallas GPIO pin (0=none)"
fullWidth
variant="outlined"
value={data.dallas_gpio}
type="number"
onChange={handleValueChange('dallas_gpio')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.dallas_parasite}
onChange={handleValueChange('dallas_parasite')}
value="dallas_parasite"
/>
}
label="Dallas Parasite Mode"
/>
<br></br>
<Typography variant="h6" color="primary" >
LED
</Typography>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['LED GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
name="led_gpio"
label="LED GPIO pin (0=none)"
fullWidth
variant="outlined"
value={data.led_gpio}
type="number"
onChange={handleValueChange('led_gpio')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.hide_led}
onChange={handleValueChange('hide_led')}
value="hide_led"
/>
}
label="Invert/Hide LED"
/>
<br></br>
<Typography variant="h6" color="primary" >
Shower
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.shower_timer}
onChange={handleValueChange('shower_timer')}
value="shower_timer"
/>
}
label="Shower Timer"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.shower_alert}
onChange={handleValueChange('shower_alert')}
value="shower_alert"
/>
}
label="Shower Alert"
/>
<br></br>
<Typography variant="h6" color="primary" >
API
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.api_enabled}
onChange={handleValueChange('api_enabled')}
value="api_enabled"
/>
}
label="Allow WEB API to write commands"
/>
<br></br>
<Typography variant="h6" color="primary" >
Syslog
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.syslog_enabled}
onChange={handleValueChange('syslog_enabled')}
value="syslog_enabled"
/>
}
label="Enable Syslog"
/>
<TextValidator
validators={['isOptionalIP']}
errorMessages={["Not a valid IP address"]}
name="syslog_host"
label="Syslog IP"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={handleValueChange('syslog_host')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Port is required', "Must be a number", "Must be greater than 0 ", "Max value is 65535"]}
name="syslog_port"
label="Syslog Port (default 514)"
fullWidth
variant="outlined"
value={data.syslog_port}
type="number"
onChange={handleValueChange('syslog_port')}
margin="normal"
/>
<SelectValidator name="syslog_level"
label="Syslog Log Level"
value={data.syslog_level}
fullWidth
variant="outlined"
onChange={handleValueChange('syslog_level')}
margin="normal">
<MenuItem value={-1}>OFF</MenuItem>
<MenuItem value={3}>ERR</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={8}>ALL</MenuItem>
</SelectValidator>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Syslog Mark is required', "Must be a number", "Must be 0 or higher", "Max value is 10"]}
name="syslog_mark_interval"
label="Syslog Mark Interval (seconds, 0=off)"
fullWidth
variant="outlined"
value={data.syslog_mark_interval}
type="number"
onChange={handleValueChange('syslog_mark_interval')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.trace_raw}
onChange={handleValueChange('trace_raw')}
value="trace_raw"
/>
}
label="Trace EMS telegrams in raw format"
/>
<br></br>
<Typography variant="h6" color="primary" >
Analog Input
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.analog_enabled}
onChange={handleValueChange('analog_enabled')}
value="analog_enabled"
/>
}
label="Enable ADC"
/>
<br></br>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
Save
</FormButton>
</FormActions>
</ValidatorForm>
);
}

View File

@@ -0,0 +1,577 @@
import React from "react";
import {
ValidatorForm,
TextValidator,
SelectValidator,
} from "react-material-ui-form-validator";
import {
Checkbox,
Typography,
Box,
Link,
withWidth,
WithWidthProps,
} from "@material-ui/core";
import SaveIcon from "@material-ui/icons/Save";
import MenuItem from "@material-ui/core/MenuItem";
import Grid from "@material-ui/core/Grid";
import {
redirectingAuthorizedFetch,
withAuthenticatedContext,
AuthenticatedContextProps,
} from "../authentication";
import {
RestFormProps,
FormActions,
FormButton,
BlockFormControlLabel,
} from "../components";
import { isIP, optional } from "../validators";
import { EMSESPSettings } from "./EMSESPtypes";
import { boardProfileSelectItems } from "./EMSESPBoardProfiles";
import { ENDPOINT_ROOT } from "../api";
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
AuthenticatedContextProps &
WithWidthProps;
interface EMSESPSettingsFormState {
processing: boolean;
}
class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
state: EMSESPSettingsFormState = {
processing: false,
};
componentDidMount() {
ValidatorForm.addValidationRule("isOptionalIP", optional(isIP));
}
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { data, setData } = this.props;
setData({
...data,
board_profile: event.target.value,
});
if (event.target.value === "CUSTOM") return;
this.setState({ processing: true });
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
method: "POST",
body: JSON.stringify({ code: event.target.value }),
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Unexpected response code: " + response.status);
})
.then((json) => {
this.props.enqueueSnackbar("Profile loaded", { variant: "success" });
setData({
...data,
led_gpio: json.led_gpio,
dallas_gpio: json.dallas_gpio,
rx_gpio: json.rx_gpio,
tx_gpio: json.tx_gpio,
pbutton_gpio: json.pbutton_gpio,
board_profile: event.target.value,
});
this.setState({ processing: false });
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || "Problem fetching board profile",
{ variant: "warning" }
);
this.setState({ processing: false });
});
};
render() {
const { data, saveData, handleValueChange } = this.props;
return (
<ValidatorForm onSubmit={saveData}>
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
<Typography variant="body1">
Adjust any of the EMS-ESP settings here. For help refer to the{" "}
<Link
target="_blank"
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
color="primary"
>
{"online documentation"}
</Link>
.
</Typography>
</Box>
<br></br>
<Typography variant="h6" color="primary">
EMS Bus
</Typography>
<Grid
container
spacing={1}
direction="row"
justify="flex-start"
alignItems="flex-start"
>
<Grid item xs={5}>
<SelectValidator
name="tx_mode"
label="Tx Mode"
value={data.tx_mode}
fullWidth
variant="outlined"
onChange={handleValueChange("tx_mode")}
margin="normal"
>
<MenuItem value={0}>Off</MenuItem>
<MenuItem value={1}>EMS</MenuItem>
<MenuItem value={2}>EMS+</MenuItem>
<MenuItem value={3}>HT3</MenuItem>
<MenuItem value={4}>Hardware</MenuItem>
</SelectValidator>
</Grid>
<Grid item xs={6}>
<SelectValidator
name="ems_bus_id"
label="Bus ID"
value={data.ems_bus_id}
fullWidth
variant="outlined"
onChange={handleValueChange("ems_bus_id")}
margin="normal"
>
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
</SelectValidator>
</Grid>
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:120",
]}
errorMessages={[
"Tx delay is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 120",
]}
name="tx_delay"
label="Tx start delay (seconds)"
fullWidth
variant="outlined"
value={data.tx_delay}
type="number"
onChange={handleValueChange("tx_delay")}
margin="normal"
/>
</Grid>
</Grid>
<br></br>
<Typography variant="h6" color="primary">
Board Profile
</Typography>
<Box color="warning.main" p={0} mt={0} mb={0}>
<Typography variant="body2">
<i>
Select a pre-configured board layout to automatically set the GPIO
pins, or set your own custom configuration
</i>
</Typography>
</Box>
<SelectValidator
name="board_profile"
label="Board Profile"
value={data.board_profile}
fullWidth
variant="outlined"
onChange={this.changeBoardProfile}
margin="normal"
>
{boardProfileSelectItems()}
<MenuItem key={"CUSTOM"} value={"CUSTOM"}>
Custom...
</MenuItem>
</SelectValidator>
{data.board_profile === "CUSTOM" && (
<Grid
container
spacing={1}
direction="row"
justify="flex-start"
alignItems="flex-start"
>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
]}
name="rx_gpio"
label="Rx GPIO"
fullWidth
variant="outlined"
value={data.rx_gpio}
type="number"
onChange={handleValueChange("rx_gpio")}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
]}
name="tx_gpio"
label="Tx GPIO"
fullWidth
variant="outlined"
value={data.tx_gpio}
type="number"
onChange={handleValueChange("tx_gpio")}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
]}
name="pbutton_gpio"
label="Button GPIO"
fullWidth
variant="outlined"
value={data.pbutton_gpio}
type="number"
onChange={handleValueChange("pbutton_gpio")}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
]}
name="dallas_gpio"
label="Dallas GPIO (0=none)"
fullWidth
variant="outlined"
value={data.dallas_gpio}
type="number"
onChange={handleValueChange("dallas_gpio")}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
]}
name="led_gpio"
label="LED GPIO (0=none)"
fullWidth
variant="outlined"
value={data.led_gpio}
type="number"
onChange={handleValueChange("led_gpio")}
margin="normal"
/>
</Grid>
</Grid>
)}
<br></br>
<Typography variant="h6" color="primary">
Options
</Typography>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={
<Checkbox
checked={data.hide_led}
onChange={handleValueChange("hide_led")}
value="hide_led"
/>
}
label="Hide LED"
/>
)}
{data.dallas_gpio !== 0 && (
<BlockFormControlLabel
control={
<Checkbox
checked={data.dallas_parasite}
onChange={handleValueChange("dallas_parasite")}
value="dallas_parasite"
/>
}
label="Enable Dallas parasite mode"
/>
)}
<BlockFormControlLabel
control={
<Checkbox
checked={data.api_enabled}
onChange={handleValueChange("api_enabled")}
value="api_enabled"
/>
}
label="Enable API write commands"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.analog_enabled}
onChange={handleValueChange("analog_enabled")}
value="analog_enabled"
/>
}
label="Enable ADC"
/>
<Grid
container
spacing={0}
direction="row"
justify="flex-start"
alignItems="flex-start"
>
<BlockFormControlLabel
control={
<Checkbox
checked={data.shower_timer}
onChange={handleValueChange("shower_timer")}
value="shower_timer"
/>
}
label="Enable Shower Timer"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.shower_alert}
onChange={handleValueChange("shower_alert")}
value="shower_alert"
/>
}
label="Enable Shower Alert"
/>
</Grid>
<br></br>
<Typography variant="h6" color="primary">
Syslog
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.syslog_enabled}
onChange={handleValueChange("syslog_enabled")}
value="syslog_enabled"
/>
}
label="Enable Syslog"
/>
{data.syslog_enabled && (
<Grid
container
spacing={1}
direction="row"
justify="flex-start"
alignItems="flex-start"
>
<Grid item xs={5}>
<TextValidator
validators={["isOptionalIP"]}
errorMessages={["Not a valid IP address"]}
name="syslog_host"
label="IP"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={handleValueChange("syslog_host")}
margin="normal"
/>
</Grid>
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Port is required",
"Must be a number",
"Must be greater than 0 ",
"Max value is 65535",
]}
name="syslog_port"
label="Port"
fullWidth
variant="outlined"
value={data.syslog_port}
type="number"
onChange={handleValueChange("syslog_port")}
margin="normal"
/>
</Grid>
<Grid item xs={5}>
<SelectValidator
name="syslog_level"
label="Log Level"
value={data.syslog_level}
fullWidth
variant="outlined"
onChange={handleValueChange("syslog_level")}
margin="normal"
>
<MenuItem value={-1}>OFF</MenuItem>
<MenuItem value={3}>ERR</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={8}>ALL</MenuItem>
</SelectValidator>
</Grid>
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
]}
errorMessages={[
"Syslog Mark is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 10",
]}
name="syslog_mark_interval"
label="Mark Interval seconds (0=off)"
fullWidth
variant="outlined"
value={data.syslog_mark_interval}
type="number"
onChange={handleValueChange("syslog_mark_interval")}
margin="normal"
/>
</Grid>
<BlockFormControlLabel
control={
<Checkbox
checked={data.trace_raw}
onChange={handleValueChange("trace_raw")}
value="trace_raw"
/>
}
label="Output EMS telegrams in raw format"
/>
</Grid>
)}
<br></br>
<FormActions>
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>
</ValidatorForm>
);
}
}
export default withAuthenticatedContext(withWidth()(EMSESPSettingsForm));

View File

@@ -62,14 +62,14 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
<TableCell>
# Telegrams Received
</TableCell>
<TableCell align="right">{formatNumber(data.rx_received)}&nbsp;({data.rx_quality}%)
<TableCell align="right">{formatNumber(data.rx_received)}&nbsp;(quality {data.rx_quality}%)
</TableCell>
</TableRow>
<TableRow>
<TableCell >
# Telegrams Sent
</TableCell >
<TableCell align="right">{formatNumber(data.tx_sent)}&nbsp;({data.tx_quality}%)
<TableCell align="right">{formatNumber(data.tx_sent)}&nbsp;(quality {data.tx_quality}%)
</TableCell>
</TableRow>
</TableBody>

View File

@@ -20,6 +20,7 @@ export interface EMSESPSettings {
analog_enabled: boolean;
pbutton_gpio: number;
trace_raw: boolean;
board_profile: string;
}
export enum busConnectionStatus {
@@ -61,3 +62,11 @@ export interface EMSESPDeviceData {
name: string;
data: string[];
}
export interface DeviceValue {
id: number;
data: string,
uom: string,
name: string,
cmd: string
}

View File

@@ -0,0 +1,64 @@
import React, { RefObject } from 'react';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, Typography } from '@material-ui/core';
import { FormButton } from '../components';
import { DeviceValue } from './EMSESPtypes';
interface ValueFormProps {
devicevalue: DeviceValue;
onDoneEditing: () => void;
onCancelEditing: () => void;
handleValueChange: (data: keyof DeviceValue) => (event: React.ChangeEvent<HTMLInputElement>) => void;
}
class ValueForm extends React.Component<ValueFormProps> {
formRef: RefObject<any> = React.createRef();
submit = () => {
this.formRef.current.submit();
}
buildLabel = (devicevalue: DeviceValue) => {
if ((devicevalue.uom === "") || (!devicevalue.uom)) {
return "New value";
}
return "New value (" + devicevalue.uom + ")";
}
render() {
const { devicevalue, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog maxWidth="xs" onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open>
<DialogTitle id="user-form-dialog-title">Change the {devicevalue.name}</DialogTitle>
<DialogContent dividers>
<TextValidator
validators={['required']}
errorMessages={['is required']}
name="data"
label={this.buildLabel(devicevalue)}
fullWidth
variant="outlined"
value={devicevalue.data}
margin="normal"
onChange={handleValueChange('data')}
/>
<Box color="warning.main" p={1} pl={0} pr={0} mt={0} mb={0}>
<Typography variant="body2">
<i>Note: it may take a few seconds before the change is visible. If nothing happens check the logs.</i>
</Typography>
</Box>
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>Cancel</FormButton>
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>Done</FormButton>
</DialogActions>
</Dialog>
</ValidatorForm>
);
}
}
export default ValueForm;

View File

@@ -0,0 +1,77 @@
import React, { Fragment } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextField } from '@material-ui/core';
import { FormButton } from '../components';
import { redirectingAuthorizedFetch } from '../authentication';
import { GENERATE_TOKEN_ENDPOINT } from '../api';
import { withSnackbar, WithSnackbarProps } from 'notistack';
interface GenerateTokenProps extends WithSnackbarProps {
username: string;
onClose: () => void;
}
interface GenerateTokenState {
token?: string;
}
class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenState> {
state: GenerateTokenState = {};
componentDidMount() {
const { username } = this.props;
redirectingAuthorizedFetch(GENERATE_TOKEN_ENDPOINT + "?" + new URLSearchParams({ username }), { method: 'GET' })
.then(response => {
if (response.status === 200) {
return response.json();
} else {
throw Error("Error generating token: " + response.status);
}
}).then(generatedToken => {
console.log(generatedToken);
this.setState({ token: generatedToken.token });
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem generating token", { variant: 'error' });
});
}
render() {
const { onClose, username } = this.props;
const { token } = this.state;
return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="generate-token-dialog-title">Token for: {username}</DialogTitle>
<DialogContent dividers>
{token ?
<Fragment>
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
<Typography variant="body1">
The token below may be used to access the secured APIs. This may be used for bearer authentication with the "Authorization" header or using the "access_token" query paramater.
</Typography>
</Box>
<Box mt={2} mb={2}>
<TextField label="Token" multiline value={token} fullWidth contentEditable={false} />
</Box>
</Fragment>
:
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">
Generating token&hellip;
</Typography>
</Box>
}
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
Close
</FormButton>
</DialogActions>
</Dialog>
);
}
}
export default withSnackbar(GenerateToken);

View File

@@ -11,12 +11,14 @@ import CheckIcon from '@material-ui/icons/Check';
import IconButton from '@material-ui/core/IconButton';
import SaveIcon from '@material-ui/icons/Save';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import VpnKeyIcon from '@material-ui/icons/VpnKey';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
import UserForm from './UserForm';
import { SecuritySettings, User } from './types';
import GenerateToken from './GenerateToken';
function compareUsers(a: User, b: User) {
if (a.username < b.username) {
@@ -33,6 +35,7 @@ type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedConte
type ManageUsersFormState = {
creating: boolean;
user?: User;
generateTokenFor?: string;
}
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
@@ -66,6 +69,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
this.props.setData({ ...data, users });
}
closeGenerateToken = () => {
this.setState({
generateTokenFor: undefined
});
}
generateToken = (user: User) => {
this.setState({
generateTokenFor: user.username
});
}
startEditingUser = (user: User) => {
this.setState({
creating: false,
@@ -103,7 +118,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
render() {
const { width, data } = this.props;
const { user, creating } = this.state;
const { user, creating, generateTokenFor } = this.state;
return (
<Fragment>
<ValidatorForm onSubmit={this.onSubmit}>
@@ -122,11 +137,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
{user.username}
</TableCell>
<TableCell align="center">
{
user.admin ? <CheckIcon /> : <CloseIcon />
}
{user.admin ? <CheckIcon /> : <CloseIcon />}
</TableCell>
<TableCell align="center">
<IconButton size="small" aria-label="Generate Token" onClick={() => this.generateToken(user)}>
<VpnKeyIcon />
</IconButton>
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
<DeleteIcon />
</IconButton>
@@ -164,6 +180,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
</FormButton>
</FormActions>
</ValidatorForm>
{
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
}
{
user &&
<UserForm

View File

@@ -35,7 +35,7 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
/>
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
<Typography variant="body1">
The Super User password is used to sign authentication tokens and also the Console's `su` password. If you modify this all users will be signed out.
The Super User password is used to sign authentication tokens and is also the Console's `su` password. If you modify this all users will be signed out.
</Typography>
</Box>
<FormActions>

View File

@@ -32,7 +32,7 @@ class UserForm extends React.Component<UserFormProps> {
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
<DialogContent dividers>
<TextValidator

View File

@@ -9,3 +9,6 @@ export interface SecuritySettings {
jwt_secret: string;
}
export interface GeneratedToken {
token: string;
}

View File

@@ -0,0 +1,12 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
'/rest/*',
createProxyMiddleware({
target: 'http://localhost:3080',
secure: false,
changeOrigin: true
})
);
};

View File

@@ -111,6 +111,8 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
<Dialog
open={this.state.confirmRestart}
onClose={this.onRestartRejected}
fullWidth
maxWidth="sm"
>
<DialogTitle>Confirm Restart</DialogTitle>
<DialogContent dividers>
@@ -158,6 +160,8 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
<Dialog
open={this.state.confirmFactoryReset}
onClose={this.onFactoryResetRejected}
fullWidth
maxWidth="sm"
>
<DialogTitle>Confirm Factory Reset</DialogTitle>
<DialogContent dividers>

View File

@@ -22,10 +22,8 @@ class UploadFirmwareForm extends React.Component<UploadFirmwareFormProps> {
return (
<Fragment>
<Box py={2}>
Upload a new firmware file (.bin or .bin.gz) below to replace the
existing firmware.
<p></p>This can take up to a minute. Wait until you see "Activating
new Firmware" and EMS-ESP will automatically restart.
Upload a new firmware file (.bin or .bin.gz) below to replace the existing firmware.
<p></p>This can take up to a minute. Wait until you see "Activating new Firmware" and EMS-ESP will then automatically restart.
</Box>
<SingleUpload
onDrop={this.handleDrop}

View File

@@ -17,7 +17,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"jsx": "react-jsx",
"noFallthroughCasesInSwitch": true
},
"include": [

View File

@@ -39,12 +39,12 @@
#include <Print.h>
#if ARDUINOJSON_VERSION_MAJOR == 5
#define ARDUINOJSON_5_COMPATIBILITY
#define ARDUINOJSON_5_COMPATIBILITY
#else
#define DYNAMIC_JSON_DOCUMENT_SIZE 1024
#define DYNAMIC_JSON_DOCUMENT_SIZE 1024
#endif
constexpr const char* JSON_MIMETYPE = "application/json";
constexpr const char * JSON_MIMETYPE = "application/json";
/*
* Json Response
@@ -52,34 +52,38 @@ constexpr const char* JSON_MIMETYPE = "application/json";
class ChunkPrint : public Print {
private:
uint8_t* _destination;
size_t _to_skip;
size_t _to_write;
size_t _pos;
uint8_t * _destination;
size_t _to_skip;
size_t _to_write;
size_t _pos;
public:
ChunkPrint(uint8_t* destination, size_t from, size_t len)
: _destination(destination), _to_skip(from), _to_write(len), _pos{0} {}
virtual ~ChunkPrint(){}
size_t write(uint8_t c){
if (_to_skip > 0) {
_to_skip--;
return 1;
} else if (_to_write > 0) {
_to_write--;
_destination[_pos++] = c;
return 1;
}
return 0;
ChunkPrint(uint8_t * destination, size_t from, size_t len)
: _destination(destination)
, _to_skip(from)
, _to_write(len)
, _pos{0} {
}
size_t write(const uint8_t *buffer, size_t size)
{
return this->Print::write(buffer, size);
virtual ~ChunkPrint() {
}
size_t write(uint8_t c) {
if (_to_skip > 0) {
_to_skip--;
return 1;
} else if (_to_write > 0) {
_to_write--;
_destination[_pos++] = c;
return 1;
}
return 0;
}
size_t write(const uint8_t * buffer, size_t size) {
return this->Print::write(buffer, size);
}
};
class AsyncJsonResponse: public AsyncAbstractResponse {
class AsyncJsonResponse : public AsyncAbstractResponse {
protected:
#ifdef ARDUINOJSON_5_COMPATIBILITY
DynamicJsonBuffer _jsonBuffer;
#else
@@ -87,166 +91,201 @@ class AsyncJsonResponse: public AsyncAbstractResponse {
#endif
JsonVariant _root;
bool _isValid;
bool _isValid;
public:
#ifdef ARDUINOJSON_5_COMPATIBILITY
AsyncJsonResponse(bool isArray=false): _isValid{false} {
_code = 200;
_contentType = JSON_MIMETYPE;
if(isArray)
_root = _jsonBuffer.createArray();
else
_root = _jsonBuffer.createObject();
AsyncJsonResponse(bool isArray = false)
: _isValid{false} {
_code = 200;
_contentType = JSON_MIMETYPE;
if (isArray)
_root = _jsonBuffer.createArray();
else
_root = _jsonBuffer.createObject();
}
#else
AsyncJsonResponse(bool isArray=false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : _jsonBuffer(maxJsonBufferSize), _isValid{false} {
_code = 200;
_contentType = JSON_MIMETYPE;
if(isArray)
_root = _jsonBuffer.createNestedArray();
else
_root = _jsonBuffer.createNestedObject();
AsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE)
: _jsonBuffer(maxJsonBufferSize)
, _isValid{false} {
_code = 200;
_contentType = JSON_MIMETYPE;
if (isArray)
_root = _jsonBuffer.createNestedArray();
else
_root = _jsonBuffer.createNestedObject();
}
#endif
~AsyncJsonResponse() {}
JsonVariant & getRoot() { return _root; }
bool _sourceValid() const { return _isValid; }
~AsyncJsonResponse() {
}
JsonVariant & getRoot() {
return _root;
}
bool _sourceValid() const {
return _isValid;
}
size_t setLength() {
#ifdef ARDUINOJSON_5_COMPATIBILITY
_contentLength = _root.measureLength();
_contentLength = _root.measureLength();
#else
_contentLength = measureJson(_root);
_contentLength = measureJson(_root);
#endif
if (_contentLength) { _isValid = true; }
return _contentLength;
}
size_t getSize() { return _jsonBuffer.size(); }
size_t _fillBuffer(uint8_t *data, size_t len){
ChunkPrint dest(data, _sentLength, len);
#ifdef ARDUINOJSON_5_COMPATIBILITY
_root.printTo( dest ) ;
#else
serializeJson(_root, dest);
#endif
return len;
}
};
class PrettyAsyncJsonResponse: public AsyncJsonResponse {
public:
#ifdef ARDUINOJSON_5_COMPATIBILITY
PrettyAsyncJsonResponse (bool isArray=false) : AsyncJsonResponse{isArray} {}
#else
PrettyAsyncJsonResponse (bool isArray=false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : AsyncJsonResponse{isArray, maxJsonBufferSize} {}
#endif
size_t setLength () {
#ifdef ARDUINOJSON_5_COMPATIBILITY
_contentLength = _root.measurePrettyLength ();
#else
_contentLength = measureJsonPretty(_root);
#endif
if (_contentLength) {_isValid = true;}
return _contentLength;
}
size_t _fillBuffer (uint8_t *data, size_t len) {
ChunkPrint dest (data, _sentLength, len);
#ifdef ARDUINOJSON_5_COMPATIBILITY
_root.prettyPrintTo (dest);
#else
serializeJsonPretty(_root, dest);
#endif
return len;
}
};
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArJsonRequestHandlerFunction;
class AsyncCallbackJsonWebHandler: public AsyncWebHandler {
private:
protected:
const String _uri;
WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest;
size_t _contentLength;
#ifndef ARDUINOJSON_5_COMPATIBILITY
const size_t maxJsonBufferSize;
#endif
size_t _maxContentLength;
public:
#ifdef ARDUINOJSON_5_COMPATIBILITY
AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest)
: _uri(uri), _method(HTTP_POST|HTTP_PUT|HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
#else
AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize=DYNAMIC_JSON_DOCUMENT_SIZE)
: _uri(uri), _method(HTTP_POST|HTTP_PUT|HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
#endif
void setMethod(WebRequestMethodComposite method){ _method = method; }
void setMaxContentLength(int maxContentLength){ _maxContentLength = maxContentLength; }
void onRequest(ArJsonRequestHandlerFunction fn){ _onRequest = fn; }
virtual bool canHandle(AsyncWebServerRequest *request) override final{
if(!_onRequest)
return false;
if(!(_method & request->method()))
return false;
if(_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri+"/")))
return false;
if ( !request->contentType().equalsIgnoreCase(JSON_MIMETYPE) )
return false;
request->addInterestingHeader("ANY");
return true;
}
virtual void handleRequest(AsyncWebServerRequest *request) override final {
if(_onRequest) {
if (request->_tempObject != NULL) {
#ifdef ARDUINOJSON_5_COMPATIBILITY
DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((uint8_t*)(request->_tempObject));
if (json.success()) {
#else
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject));
if(!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif
_onRequest(request, json);
return;
if (_contentLength) {
_isValid = true;
}
}
request->send(_contentLength > _maxContentLength ? 413 : 400);
} else {
request->send(500);
return _contentLength;
}
}
virtual void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) override final {
}
virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final {
if (_onRequest) {
_contentLength = total;
if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) {
request->_tempObject = malloc(total);
}
if (request->_tempObject != NULL) {
memcpy((uint8_t*)(request->_tempObject) + index, data, len);
}
size_t getSize() {
return _jsonBuffer.size();
}
size_t _fillBuffer(uint8_t * data, size_t len) {
ChunkPrint dest(data, _sentLength, len);
#ifdef ARDUINOJSON_5_COMPATIBILITY
_root.printTo(dest);
#else
serializeJson(_root, dest);
#endif
return len;
}
};
class PrettyAsyncJsonResponse : public AsyncJsonResponse {
public:
#ifdef ARDUINOJSON_5_COMPATIBILITY
PrettyAsyncJsonResponse(bool isArray = false)
: AsyncJsonResponse{isArray} {
}
#else
PrettyAsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE)
: AsyncJsonResponse{isArray, maxJsonBufferSize} {
}
#endif
size_t setLength() {
#ifdef ARDUINOJSON_5_COMPATIBILITY
_contentLength = _root.measurePrettyLength();
#else
_contentLength = measureJsonPretty(_root);
#endif
if (_contentLength) {
_isValid = true;
}
return _contentLength;
}
size_t _fillBuffer(uint8_t * data, size_t len) {
ChunkPrint dest(data, _sentLength, len);
#ifdef ARDUINOJSON_5_COMPATIBILITY
_root.prettyPrintTo(dest);
#else
serializeJsonPretty(_root, dest);
// serializeJson(_root, Serial); // for testing
// Serial.println();
#endif
return len;
}
};
typedef std::function<void(AsyncWebServerRequest * request, JsonVariant & json)> ArJsonRequestHandlerFunction;
class AsyncCallbackJsonWebHandler : public AsyncWebHandler {
private:
protected:
const String _uri;
WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest;
size_t _contentLength;
#ifndef ARDUINOJSON_5_COMPATIBILITY
const size_t maxJsonBufferSize;
#endif
size_t _maxContentLength;
public:
#ifdef ARDUINOJSON_5_COMPATIBILITY
AsyncCallbackJsonWebHandler(const String & uri, ArJsonRequestHandlerFunction onRequest)
: _uri(uri)
, _method(HTTP_POST | HTTP_PUT | HTTP_PATCH)
, _onRequest(onRequest)
, _maxContentLength(16384) {
}
#else
AsyncCallbackJsonWebHandler(const String & uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE)
: _uri(uri)
, _method(HTTP_POST | HTTP_PUT | HTTP_PATCH)
, _onRequest(onRequest)
, maxJsonBufferSize(maxJsonBufferSize)
, _maxContentLength(16384) {
}
#endif
void setMethod(WebRequestMethodComposite method) {
_method = method;
}
void setMaxContentLength(int maxContentLength) {
_maxContentLength = maxContentLength;
}
void onRequest(ArJsonRequestHandlerFunction fn) {
_onRequest = fn;
}
virtual bool canHandle(AsyncWebServerRequest * request) override final {
if (!_onRequest)
return false;
if (!(_method & request->method()))
return false;
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/")))
return false;
if (!request->contentType().equalsIgnoreCase(JSON_MIMETYPE))
return false;
request->addInterestingHeader("ANY");
return true;
}
virtual void handleRequest(AsyncWebServerRequest * request) override final {
if (_onRequest) {
if (request->_tempObject != NULL) {
#ifdef ARDUINOJSON_5_COMPATIBILITY
DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((uint8_t *)(request->_tempObject));
if (json.success()) {
#else
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif
_onRequest(request, json);
return;
}
}
request->send(_contentLength > _maxContentLength ? 413 : 400);
} else {
request->send(500);
}
}
virtual void handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final) override final {
}
virtual void handleBody(AsyncWebServerRequest * request, uint8_t * data, size_t len, size_t index, size_t total) override final {
if (_onRequest) {
_contentLength = total;
if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) {
request->_tempObject = malloc(total);
}
if (request->_tempObject != NULL) {
memcpy((uint8_t *)(request->_tempObject) + index, data, len);
}
}
}
virtual bool isRequestHandlerTrivial() override final {
return _onRequest ? false : true;
}
}
virtual bool isRequestHandlerTrivial() override final {return _onRequest ? false : true;}
};
#endif

View File

@@ -28,9 +28,9 @@ PButton::PButton() {
LongPressDelay_ = 750; // Hold period for a long press event (in ms)
VLongPressDelay_ = 3000; // Hold period for a very long press event (in ms)
cb_onClick = nullptr;
cb_onDblClick = nullptr;
cb_onLongPress = nullptr;
cb_onClick = nullptr;
cb_onDblClick = nullptr;
cb_onLongPress = nullptr;
cb_onVLongPress = nullptr;
// Initialization of variables
@@ -144,7 +144,7 @@ bool PButton::check(void) {
// added code: raise OnVLongPress event when the button is released, only for pin 0
if (state_ == pullMode_ && vLongPressHappened_) {
resultEvent = 4;
resultEvent = 4;
vLongPressHappened_ = false;
longPressHappened_ = false;
}

View File

@@ -22,22 +22,20 @@ void APSettingsService::reconfigureAP() {
}
void APSettingsService::loop() {
// if we have an ETH connection, quit
if (emsesp::EMSESP::system_.ethernet_connected()) {
return;
}
unsigned long currentMillis = uuid::get_uptime();
unsigned long manageElapsed = (uint32_t)(currentMillis - _lastManaged);
if (manageElapsed >= MANAGE_NETWORK_DELAY) {
_lastManaged = currentMillis;
manageAP();
}
handleDNS();
}
void APSettingsService::manageAP() {
WiFiMode_t currentWiFiMode = WiFi.getMode();
if (_state.provisionMode == AP_MODE_ALWAYS || (_state.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) {
WiFiMode_t currentWiFiMode = WiFi.getMode();
bool network_connected = (emsesp::EMSESP::system_.ethernet_connected() || (WiFi.status() == WL_CONNECTED));
if (_state.provisionMode == AP_MODE_ALWAYS || (_state.provisionMode == AP_MODE_DISCONNECTED && !network_connected)) {
if (_reconfigureAp || currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA) {
startAP();
}
@@ -48,13 +46,11 @@ void APSettingsService::manageAP() {
}
void APSettingsService::startAP() {
// Serial.println(F("Starting software access point"));
WiFi.softAPConfig(_state.localIP, _state.gatewayIP, _state.subnetMask);
WiFi.softAP(_state.ssid.c_str(), _state.password.c_str());
if (!_dnsServer) {
IPAddress apIp = WiFi.softAPIP();
// Serial.print(F("Starting captive portal on "));
// Serial.println(apIp);
emsesp::EMSESP::logger().info(F("Starting Access Point with captive portal on %s"), apIp.toString().c_str());
_dnsServer = new DNSServer;
_dnsServer->start(DNS_PORT, "*", apIp);
}
@@ -62,12 +58,11 @@ void APSettingsService::startAP() {
void APSettingsService::stopAP() {
if (_dnsServer) {
// Serial.println(F("Stopping captive portal"));
emsesp::EMSESP::logger().info(F("Stopping Access Point"));
_dnsServer->stop();
delete _dnsServer;
_dnsServer = nullptr;
}
// Serial.println(F("Stopping software access point"));
WiFi.softAPdisconnect(true);
}

View File

@@ -1,10 +1,10 @@
#include <APStatus.h>
using namespace std::placeholders; // for `_1` etc
APStatus::APStatus(AsyncWebServer * server, SecurityManager * securityManager, APSettingsService * apSettingsService)
: _apSettingsService(apSettingsService) {
server->on(AP_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(AP_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
}
void APStatus::apStatus(AsyncWebServerRequest * request) {

View File

@@ -1,13 +1,8 @@
#ifndef APStatus_h
#define APStatus_h
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ArduinoJson.h>
#include <AsyncJson.h>

View File

@@ -15,14 +15,13 @@ String ArduinoJsonJWT::getSecret() {
/*
* ESP32 uses mbedtls, ESP2866 uses bearssl.
*
* Both come with decent HMAC implmentations supporting sha256, as well as others.
* Both come with decent HMAC implementations supporting sha256, as well as others.
*
* No need to pull in additional crypto libraries - lets use what we already have.
*/
String ArduinoJsonJWT::sign(String & payload) {
unsigned char hmacResult[32];
{
#ifdef ESP32
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
@@ -31,14 +30,6 @@ String ArduinoJsonJWT::sign(String & payload) {
mbedtls_md_hmac_update(&ctx, (unsigned char *)payload.c_str(), payload.length());
mbedtls_md_hmac_finish(&ctx, hmacResult);
mbedtls_md_free(&ctx);
#elif defined(ESP8266)
br_hmac_key_context keyCtx;
br_hmac_key_init(&keyCtx, &br_sha256_vtable, _secret.c_str(), _secret.length());
br_hmac_context hmacCtx;
br_hmac_init(&hmacCtx, &keyCtx, 0);
br_hmac_update(&hmacCtx, payload.c_str(), payload.length());
br_hmac_out(&hmacCtx, hmacResult);
#endif
}
return encode((char *)hmacResult, 32);
}
@@ -94,13 +85,8 @@ void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument & jsonDocument) {
String ArduinoJsonJWT::encode(const char * cstr, int inputLen) {
// prepare encoder
base64_encodestate _state;
#ifdef ESP32
base64_init_encodestate(&_state);
size_t encodedLength = base64_encode_expected_len(inputLen) + 1;
#elif defined(ESP8266)
base64_init_encodestate_nonewlines(&_state);
size_t encodedLength = base64_encode_expected_len_nonewlines(inputLen) + 1;
#endif
// prepare buffer of correct length, returning an empty string on failure
char * buffer = (char *)malloc(encodedLength * sizeof(char));
if (buffer == nullptr) {

View File

@@ -5,12 +5,7 @@
#include <ArduinoJson.h>
#include <libb64/cdecode.h>
#include <libb64/cencode.h>
#ifdef ESP32
#include <mbedtls/md.h>
#elif defined(ESP8266)
#include <bearssl/bearssl_hmac.h>
#endif
class ArduinoJsonJWT {
private:

View File

@@ -1,11 +1,13 @@
#include <AuthenticationService.h>
using namespace std::placeholders; // for `_1` etc
#if FT_ENABLED(FT_SECURITY)
AuthenticationService::AuthenticationService(AsyncWebServer * server, SecurityManager * securityManager)
: _securityManager(securityManager)
, _signInHandler(SIGN_IN_PATH, std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2)) {
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1));
, _signInHandler(SIGN_IN_PATH, std::bind(&AuthenticationService::signIn, this, _1, _2)) {
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, _1));
_signInHandler.setMethod(HTTP_POST);
_signInHandler.setMaxContentLength(MAX_AUTHENTICATION_SIZE);
server->addHandler(&_signInHandler);

View File

@@ -40,13 +40,6 @@ ESP8266React::ESP8266React(AsyncWebServer * server, FS * fs)
});
}
});
// Disable CORS if required
#if defined(ENABLE_CORS)
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
}
void ESP8266React::begin() {

View File

@@ -6,11 +6,7 @@
class ESPUtils {
public:
static String defaultDeviceValue(String prefix = "") {
#ifdef ESP32
return prefix + String((uint32_t)ESP.getEfuseMac(), HEX);
#elif defined(ESP8266)
return prefix + String(ESP.getChipId(), HEX);
#endif
}
};

View File

@@ -30,7 +30,7 @@ class FSPersistence {
// debug added by Proddy
#if defined(EMSESP_DEBUG)
Serial.printf("Read File: %s: ", _filePath);
Serial.printf("Reading file: %s: ", _filePath);
serializeJson(jsonDocument, Serial);
Serial.println();
#endif
@@ -63,7 +63,7 @@ class FSPersistence {
// debug added by Proddy
#if defined(EMSESP_DEBUG)
Serial.printf("Write File: %s: ", _filePath);
Serial.printf("Writing to file: %s: ", _filePath);
serializeJson(jsonDocument, Serial);
Serial.println();
#endif

View File

@@ -16,7 +16,6 @@ void FactoryResetService::handleRequest(AsyncWebServerRequest * request) {
* Delete function assumes that all files are stored flat, within the config directory.
*/
void FactoryResetService::factoryReset() {
#ifdef ESP32
/*
* Based on LITTLEFS. Modified by proddy
* Could be replaced with fs.rmdir(FS_CONFIG_DIRECTORY) in IDF 4.2
@@ -28,14 +27,5 @@ void FactoryResetService::factoryReset() {
file.close();
fs->remove(pathStr);
}
#elif defined(ESP8266)
Dir configDirectory = fs->openDir(FS_CONFIG_DIRECTORY);
while (configDirectory.next()) {
String path = FS_CONFIG_DIRECTORY;
path.concat("/");
path.concat(configDirectory.fileName());
fs->remove(path);
}
#endif
RestartService::restartNow();
}

View File

@@ -1,14 +1,8 @@
#ifndef FactoryResetService_h
#define FactoryResetService_h
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <RestartService.h>

View File

@@ -1,11 +1,13 @@
#ifndef Features_h
#define Features_h
// modified by Proddy
#define FT_ENABLED(feature) feature
// project feature off by default
// project feature on by default
#ifndef FT_PROJECT
#define FT_PROJECT 0
#define FT_PROJECT 1
#endif
// security feature on by default
@@ -23,14 +25,14 @@
#define FT_NTP 1
#endif
// mqtt feature on by default
// ota feature on by default
#ifndef FT_OTA
#define FT_OTA 1
#endif
// upload firmware feature off by default
// upload firmware feature on by default
#ifndef FT_UPLOAD_FIRMWARE
#define FT_UPLOAD_FIRMWARE 0
#define FT_UPLOAD_FIRMWARE 1
#endif

View File

@@ -1,7 +1,9 @@
#include <FeaturesService.h>
using namespace std::placeholders; // for `_1` etc
FeaturesService::FeaturesService(AsyncWebServer * server) {
server->on(FEATURES_SERVICE_PATH, HTTP_GET, std::bind(&FeaturesService::features, this, std::placeholders::_1));
server->on(FEATURES_SERVICE_PATH, HTTP_GET, std::bind(&FeaturesService::features, this, _1));
}
void FeaturesService::features(AsyncWebServerRequest * request) {

View File

@@ -3,14 +3,8 @@
#include <Features.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>

View File

@@ -11,6 +11,8 @@
#define HTTP_ENDPOINT_ORIGIN_ID "http"
using namespace std::placeholders; // for `_1` etc
template <class T>
class HttpGetEndpoint {
public:
@@ -24,20 +26,14 @@ class HttpGetEndpoint {
: _stateReader(stateReader)
, _statefulService(statefulService)
, _bufferSize(bufferSize) {
server->on(servicePath.c_str(),
HTTP_GET,
securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1), authenticationPredicate));
server->on(servicePath.c_str(), HTTP_GET, securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, _1), authenticationPredicate));
}
HttpGetEndpoint(JsonStateReader<T> stateReader,
StatefulService<T> * statefulService,
AsyncWebServer * server,
const String & servicePath,
size_t bufferSize = DEFAULT_BUFFER_SIZE)
HttpGetEndpoint(JsonStateReader<T> stateReader, StatefulService<T> * statefulService, AsyncWebServer * server, const String & servicePath, size_t bufferSize = DEFAULT_BUFFER_SIZE)
: _stateReader(stateReader)
, _statefulService(statefulService)
, _bufferSize(bufferSize) {
server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1));
server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, _1));
}
protected:
@@ -69,25 +65,17 @@ class HttpPostEndpoint {
: _stateReader(stateReader)
, _stateUpdater(stateUpdater)
, _statefulService(statefulService)
, _updateHandler(servicePath,
securityManager->wrapCallback(std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2),
authenticationPredicate),
bufferSize)
, _updateHandler(servicePath, securityManager->wrapCallback(std::bind(&HttpPostEndpoint::updateSettings, this, _1, _2), authenticationPredicate), bufferSize)
, _bufferSize(bufferSize) {
_updateHandler.setMethod(HTTP_POST);
server->addHandler(&_updateHandler);
}
HttpPostEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T> * statefulService,
AsyncWebServer * server,
const String & servicePath,
size_t bufferSize = DEFAULT_BUFFER_SIZE)
HttpPostEndpoint(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater, StatefulService<T> * statefulService, AsyncWebServer * server, const String & servicePath, size_t bufferSize = DEFAULT_BUFFER_SIZE)
: _stateReader(stateReader)
, _stateUpdater(stateUpdater)
, _statefulService(statefulService)
, _updateHandler(servicePath, std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), bufferSize)
, _updateHandler(servicePath, std::bind(&HttpPostEndpoint::updateSettings, this, _1, _2), bufferSize)
, _bufferSize(bufferSize) {
_updateHandler.setMethod(HTTP_POST);
server->addHandler(&_updateHandler);
@@ -137,12 +125,7 @@ class HttpEndpoint : public HttpGetEndpoint<T>, public HttpPostEndpoint<T> {
, HttpPostEndpoint<T>(stateReader, stateUpdater, statefulService, server, servicePath, securityManager, authenticationPredicate, bufferSize) {
}
HttpEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T> * statefulService,
AsyncWebServer * server,
const String & servicePath,
size_t bufferSize = DEFAULT_BUFFER_SIZE)
HttpEndpoint(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater, StatefulService<T> * statefulService, AsyncWebServer * server, const String & servicePath, size_t bufferSize = DEFAULT_BUFFER_SIZE)
: HttpGetEndpoint<T>(stateReader, statefulService, server, servicePath, bufferSize)
, HttpPostEndpoint<T>(stateReader, stateUpdater, statefulService, server, servicePath, bufferSize) {
}

View File

@@ -4,6 +4,8 @@
#include <StatefulService.h>
#include <AsyncMqttClient.h>
using namespace std::placeholders; // for `_1` etc
#define MQTT_ORIGIN_ID "mqtt"
template <class T>
@@ -31,11 +33,7 @@ class MqttConnector {
template <class T>
class MqttPub : virtual public MqttConnector<T> {
public:
MqttPub(JsonStateReader<T> stateReader,
StatefulService<T> * statefulService,
AsyncMqttClient * mqttClient,
const String & pubTopic = "",
size_t bufferSize = DEFAULT_BUFFER_SIZE)
MqttPub(JsonStateReader<T> stateReader, StatefulService<T> * statefulService, AsyncMqttClient * mqttClient, const String & pubTopic = "", size_t bufferSize = DEFAULT_BUFFER_SIZE)
: MqttConnector<T>(statefulService, mqttClient, bufferSize)
, _stateReader(stateReader)
, _pubTopic(pubTopic) {
@@ -76,22 +74,11 @@ class MqttPub : virtual public MqttConnector<T> {
template <class T>
class MqttSub : virtual public MqttConnector<T> {
public:
MqttSub(JsonStateUpdater<T> stateUpdater,
StatefulService<T> * statefulService,
AsyncMqttClient * mqttClient,
const String & subTopic = "",
size_t bufferSize = DEFAULT_BUFFER_SIZE)
MqttSub(JsonStateUpdater<T> stateUpdater, StatefulService<T> * statefulService, AsyncMqttClient * mqttClient, const String & subTopic = "", size_t bufferSize = DEFAULT_BUFFER_SIZE)
: MqttConnector<T>(statefulService, mqttClient, bufferSize)
, _stateUpdater(stateUpdater)
, _subTopic(subTopic) {
MqttConnector<T>::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
std::placeholders::_5,
std::placeholders::_6));
MqttConnector<T>::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
}
void setSubTopic(const String & subTopic) {

View File

@@ -2,6 +2,8 @@
#include "../../src/emsesp_stub.hpp" // proddy added
using namespace std::placeholders; // for `_1` etc
/**
* Retains a copy of the cstr provided in the pointer provided using dynamic allocation.
*
@@ -33,10 +35,9 @@ MqttSettingsService::MqttSettingsService(AsyncWebServer * server, FS * fs, Secur
, _disconnectedAt(0)
, _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED)
, _mqttClient() {
WiFi.onEvent(std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED);
WiFi.onEvent(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP);
_mqttClient.onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, std::placeholders::_1));
_mqttClient.onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, std::placeholders::_1));
WiFi.onEvent(std::bind(&MqttSettingsService::WiFiEvent, this, _1, _2));
_mqttClient.onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, _1));
_mqttClient.onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, _1));
addUpdateHandler([&](const String & originId) { onConfigUpdated(); }, false);
}
@@ -79,17 +80,11 @@ AsyncMqttClient * MqttSettingsService::getMqttClient() {
}
void MqttSettingsService::onMqttConnect(bool sessionPresent) {
// Serial.print(F("Connected to MQTT, "));
// if (sessionPresent) {
// Serial.println(F("with persistent session"));
// } else {
// Serial.println(F("without persistent session"));
// }
// emsesp::EMSESP::logger().info(F("Connected to MQTT, %s"), (sessionPresent) ? F("with persistent session") : F("without persistent session"));
}
void MqttSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
// Serial.print(F("Disconnected from MQTT reason: "));
// Serial.println((uint8_t)reason);
// emsesp::EMSESP::logger().info(F("Disconnected from MQTT reason: %s"), (uint8_t)reason);
_disconnectReason = reason;
_disconnectedAt = uuid::get_uptime();
}
@@ -99,47 +94,38 @@ void MqttSettingsService::onConfigUpdated() {
_disconnectedAt = 0;
// added by proddy
// reload EMS-ESP MQTT settings
emsesp::EMSESP::mqtt_.start();
emsesp::EMSESP::mqtt_.start(); // reload EMS-ESP MQTT settings
}
#ifdef ESP32
void MqttSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
if (_state.enabled) {
// Serial.println(F("WiFi connection dropped, starting MQTT client."));
onConfigUpdated();
}
}
void MqttSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case SYSTEM_EVENT_STA_GOT_IP:
case SYSTEM_EVENT_ETH_GOT_IP:
if (_state.enabled) {
// emsesp::EMSESP::logger().info(F("Network connection found, starting MQTT client"));
onConfigUpdated();
}
break;
void MqttSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
if (_state.enabled) {
// Serial.println(F("WiFi connection dropped, stopping MQTT client."));
onConfigUpdated();
}
}
#elif defined(ESP8266)
void MqttSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP & event) {
if (_state.enabled) {
// Serial.println(F("WiFi connection dropped, starting MQTT client."));
onConfigUpdated();
}
}
case SYSTEM_EVENT_STA_DISCONNECTED:
case SYSTEM_EVENT_ETH_DISCONNECTED:
if (_state.enabled) {
// emsesp::EMSESP::logger().info(F("Network connection dropped, stopping MQTT client"));
onConfigUpdated();
}
break;
void MqttSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected & event) {
if (_state.enabled) {
// Serial.println(F("WiFi connection dropped, stopping MQTT client."));
onConfigUpdated();
default:
break;
}
}
#endif
void MqttSettingsService::configureMqtt() {
// disconnect if currently connected
_mqttClient.disconnect();
// only connect if WiFi is connected and MQTT is enabled
if (_state.enabled && WiFi.isConnected()) {
// Serial.println(F("Connecting to MQTT..."));
if (_state.enabled && emsesp::EMSESP::system_.network_connected()) {
_mqttClient.setServer(retainCstr(_state.host.c_str(), &_retainedHost), _state.port);
if (_state.username.length() > 0) {
_mqttClient.setCredentials(retainCstr(_state.username.c_str(), &_retainedUsername), retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword));
@@ -153,7 +139,7 @@ void MqttSettingsService::configureMqtt() {
_mqttClient.connect();
}
emsesp::EMSESP::dallassensor_.reload();
emsesp::EMSESP::dallassensor_.reload(); // added by Proddy for EMS-ESP
}
void MqttSettings::read(MqttSettings & settings, JsonObject & root) {
@@ -182,6 +168,7 @@ void MqttSettings::read(MqttSettings & settings, JsonObject & root) {
root["ha_climate_format"] = settings.ha_climate_format;
root["ha_enabled"] = settings.ha_enabled;
root["nested_format"] = settings.nested_format;
root["subscribe_format"] = settings.subscribe_format;
}
StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & settings) {
@@ -213,6 +200,7 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
newSettings.ha_climate_format = root["ha_climate_format"] | EMSESP_DEFAULT_HA_CLIMATE_FORMAT;
newSettings.ha_enabled = root["ha_enabled"] | EMSESP_DEFAULT_HA_ENABLED;
newSettings.nested_format = root["nested_format"] | EMSESP_DEFAULT_NESTED_FORMAT;
newSettings.subscribe_format = root["subscribe_format"] | EMSESP_DEFAULT_SUBSCRIBE_FORMAT;
if (newSettings.mqtt_qos != settings.mqtt_qos) {
emsesp::EMSESP::mqtt_.set_qos(newSettings.mqtt_qos);
@@ -228,6 +216,10 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
changed = true;
}
if (newSettings.subscribe_format != settings.subscribe_format) {
changed = true;
}
if (newSettings.ha_climate_format != settings.ha_climate_format) {
emsesp::EMSESP::mqtt_.ha_climate_format(newSettings.ha_climate_format);
changed = true;

View File

@@ -41,11 +41,7 @@
#ifndef FACTORY_MQTT_CLIENT_ID
#define FACTORY_MQTT_CLIENT_ID generateClientId()
static String generateClientId() {
#ifdef ESP32
return ESPUtils::defaultDeviceValue("esp32-");
#elif defined(ESP8266)
return ESPUtils::defaultDeviceValue("esp8266-");
#endif
}
#endif
@@ -61,22 +57,12 @@ static String generateClientId() {
#define FACTORY_MQTT_MAX_TOPIC_LENGTH 128
#endif
#define EMSESP_DEFAULT_BOOL_FORMAT 1 // on/off
#define EMSESP_DEFAULT_DALLAS_FORMAT 1 // sensorid
#define EMSESP_DEFAULT_HA_CLIMATE_FORMAT 1 // current temp
#define EMSESP_DEFAULT_MQTT_QOS 0
#define EMSESP_DEFAULT_MQTT_RETAIN false
#define EMSESP_DEFAULT_HA_ENABLED false
#define EMSESP_DEFAULT_PUBLISH_TIME 10
#define EMSESP_DEFAULT_NESTED_FORMAT true
class MqttSettings {
public:
// host and port - if enabled
bool enabled;
String host;
uint16_t port;
String base;
// username and password
String username;
@@ -91,6 +77,7 @@ class MqttSettings {
uint16_t maxTopicLength;
// proddy EMS-ESP specific
String base;
uint16_t publish_time_boiler;
uint16_t publish_time_thermostat;
uint16_t publish_time_solar;
@@ -103,7 +90,8 @@ class MqttSettings {
uint8_t bool_format;
uint8_t ha_climate_format;
bool ha_enabled;
bool nested_format;
uint8_t nested_format;
uint8_t subscribe_format;
static void read(MqttSettings & settings, JsonObject & root);
static StateUpdateResult update(JsonObject & root, MqttSettings & settings);
@@ -146,8 +134,7 @@ class MqttSettingsService : public StatefulService<MqttSettings> {
// the MQTT client instance
AsyncMqttClient _mqttClient;
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
void onMqttConnect(bool sessionPresent);
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason);
void configureMqtt();

View File

@@ -2,11 +2,13 @@
#include "../../src/emsesp_stub.hpp" // proddy added
using namespace std::placeholders; // for `_1` etc
MqttStatus::MqttStatus(AsyncWebServer * server, MqttSettingsService * mqttSettingsService, SecurityManager * securityManager)
: _mqttSettingsService(mqttSettingsService) {
server->on(MQTT_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED));
securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
}
void MqttStatus::mqttStatus(AsyncWebServerRequest * request) {

View File

@@ -1,14 +1,8 @@
#ifndef MqttStatus_h
#define MqttStatus_h
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <MqttSettingsService.h>
#include <ArduinoJson.h>
#include <AsyncJson.h>

View File

@@ -1,14 +1,18 @@
#include <NTPSettingsService.h>
#include "../../src/emsesp_stub.hpp" // proddy added
using namespace std::placeholders; // for `_1` etc
NTPSettingsService::NTPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE)
, _timeHandler(TIME_PATH, securityManager->wrapCallback(std::bind(&NTPSettingsService::configureTime, this, std::placeholders::_1, std::placeholders::_2), AuthenticationPredicates::IS_ADMIN)) {
, _timeHandler(TIME_PATH, securityManager->wrapCallback(std::bind(&NTPSettingsService::configureTime, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) {
_timeHandler.setMethod(HTTP_POST);
_timeHandler.setMaxContentLength(MAX_TIME_SIZE);
server->addHandler(&_timeHandler);
WiFi.onEvent(std::bind(&NTPSettingsService::WiFiEvent, this, std::placeholders::_1));
WiFi.onEvent(std::bind(&NTPSettingsService::WiFiEvent, this, _1));
addUpdateHandler([&](const String & originId) { configureNTP(); }, false);
}
@@ -22,14 +26,15 @@ void NTPSettingsService::begin() {
void NTPSettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case SYSTEM_EVENT_STA_DISCONNECTED:
// Serial.println(F("WiFi connection dropped, stopping NTP."));
case SYSTEM_EVENT_ETH_DISCONNECTED:
emsesp::EMSESP::logger().info(F("WiFi connection dropped, stopping NTP"));
connected_ = false;
configureNTP();
break;
case SYSTEM_EVENT_STA_GOT_IP:
case SYSTEM_EVENT_ETH_GOT_IP:
// Serial.println(F("Got IP address, starting NTP Synchronization"));
// emsesp::EMSESP::logger().info(F("Got IP address, starting NTP synchronization"));
connected_ = true;
configureNTP();
break;
@@ -41,7 +46,7 @@ void NTPSettingsService::WiFiEvent(WiFiEvent_t event) {
void NTPSettingsService::configureNTP() {
if (connected_ && _state.enabled) {
// Serial.println(F("Starting NTP..."));
emsesp::EMSESP::logger().info(F("Starting NTP"));
configTzTime(_state.tzFormat.c_str(), _state.server.c_str());
} else {
setenv("TZ", _state.tzFormat.c_str(), 1);

View File

@@ -1,7 +1,9 @@
#include <NTPStatus.h>
using namespace std::placeholders; // for `_1` etc
NTPStatus::NTPStatus(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(NTP_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(NTP_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
}
/*

View File

@@ -2,15 +2,10 @@
#define NTPStatus_h
#include <time.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#include <lwip/apps/sntp.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <sntp.h>
#endif
#include <ArduinoJson.h>
#include <AsyncJson.h>

View File

@@ -1,5 +1,7 @@
#include <NetworkSettingsService.h>
using namespace std::placeholders; // for `_1` etc
NetworkSettingsService::NetworkSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(NetworkSettings::read, NetworkSettings::update, this, server, NETWORK_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(NetworkSettings::read, NetworkSettings::update, this, fs, NETWORK_SETTINGS_FILE)
@@ -17,7 +19,7 @@ NetworkSettingsService::NetworkSettingsService(AsyncWebServer * server, FS * fs,
WiFi.mode(WIFI_MODE_MAX);
WiFi.mode(WIFI_MODE_NULL);
WiFi.onEvent(std::bind(&NetworkSettingsService::WiFiEvent, this, std::placeholders::_1));
WiFi.onEvent(std::bind(&NetworkSettingsService::WiFiEvent, this, _1));
addUpdateHandler([&](const String & originId) { reconfigureWiFiConnection(); }, false);
}

View File

@@ -31,7 +31,6 @@ class NetworkSettings {
String password;
String hostname;
bool staticIPConfig;
uint8_t ethernet_profile;
// optional configuration for static IP address
IPAddress localIP;
@@ -46,7 +45,6 @@ class NetworkSettings {
root["password"] = settings.password;
root["hostname"] = settings.hostname;
root["static_ip_config"] = settings.staticIPConfig;
root["ethernet_profile"] = settings.ethernet_profile;
// extended settings
JsonUtils::writeIP(root, "local_ip", settings.localIP);
@@ -61,7 +59,6 @@ class NetworkSettings {
settings.password = root["password"] | FACTORY_WIFI_PASSWORD;
settings.hostname = root["hostname"] | FACTORY_WIFI_HOSTNAME;
settings.staticIPConfig = root["static_ip_config"] | false;
settings.ethernet_profile = root["ethernet_profile"] | 0; // no ethernet
// extended settings
JsonUtils::readIP(root, "local_ip", settings.localIP);

View File

@@ -2,8 +2,10 @@
#include "../../src/emsesp_stub.hpp" // proddy added
using namespace std::placeholders; // for `_1` etc
NetworkStatus::NetworkStatus(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(NETWORK_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&NetworkStatus::networkStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(NETWORK_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&NetworkStatus::networkStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
}
void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {

View File

@@ -2,15 +2,13 @@
#include "../../src/emsesp_stub.hpp" // proddy added
using namespace std::placeholders; // for `_1` etc
OTASettingsService::OTASettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(OTASettings::read, OTASettings::update, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(OTASettings::read, OTASettings::update, this, fs, OTA_SETTINGS_FILE)
, _arduinoOTA(nullptr) {
#ifdef ESP32
WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP);
#elif defined(ESP8266)
_onStationModeGotIPHandler = WiFi.onStationModeGotIP(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1));
#endif
WiFi.onEvent(std::bind(&OTASettingsService::WiFiEvent, this, _1, _2));
addUpdateHandler([&](const String & originId) { configureArduinoOTA(); }, false);
}
@@ -27,54 +25,51 @@ void OTASettingsService::loop() {
void OTASettingsService::configureArduinoOTA() {
if (_arduinoOTA) {
#ifdef ESP32
_arduinoOTA->end();
#endif
delete _arduinoOTA;
_arduinoOTA = nullptr;
}
if (_state.enabled) {
// Serial.println(F("Starting OTA Update Service..."));
_arduinoOTA = new ArduinoOTAClass;
_arduinoOTA->setPort(_state.port);
_arduinoOTA->setPassword(_state.password.c_str());
_arduinoOTA->onStart([]() {
// Serial.println(F("Starting"));
Serial.println(F("Starting"));
emsesp::EMSESP::system_.upload_status(true);
});
_arduinoOTA->onEnd([]() {
// Serial.println(F("\r\nEnd"));
Serial.println(F("\r\nEnd"));
emsesp::EMSESP::system_.upload_status(false);
});
// _arduinoOTA->onProgress([](unsigned int progress, unsigned int total) {
// Serial.printf_P(PSTR("Progress: %u%%\r\n"), (progress / (total / 100)));
// });
// _arduinoOTA->onError([](ota_error_t error) {
// Serial.printf("Error[%u]: ", error);
// if (error == OTA_AUTH_ERROR)
// Serial.println(F("Auth Failed"));
// else if (error == OTA_BEGIN_ERROR)
// Serial.println(F("Begin Failed"));
// else if (error == OTA_CONNECT_ERROR)
// Serial.println(F("Connect Failed"));
// else if (error == OTA_RECEIVE_ERROR)
// Serial.println(F("Receive Failed"));
// else if (error == OTA_END_ERROR)
// Serial.println(F("End Failed"));
// });
_arduinoOTA->onProgress([](unsigned int progress, unsigned int total) { Serial.printf_P(PSTR("Progress: %u%%\r\n"), (progress / (total / 100))); });
_arduinoOTA->onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR)
Serial.println(F("Auth Failed"));
else if (error == OTA_BEGIN_ERROR)
Serial.println(F("Begin Failed"));
else if (error == OTA_CONNECT_ERROR)
Serial.println(F("Connect Failed"));
else if (error == OTA_RECEIVE_ERROR)
Serial.println(F("Receive Failed"));
else if (error == OTA_END_ERROR)
Serial.println(F("End Failed"));
});
_arduinoOTA->begin();
}
}
#ifdef ESP32
void OTASettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
configureArduinoOTA();
void OTASettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case SYSTEM_EVENT_STA_GOT_IP:
case SYSTEM_EVENT_ETH_GOT_IP:
configureArduinoOTA();
break;
default:
break;
}
}
#elif defined(ESP8266)
void OTASettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP & event) {
configureArduinoOTA();
}
#endif

View File

@@ -4,11 +4,7 @@
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#ifdef ESP32
#include <ESPmDNS.h>
#elif defined(ESP8266)
#include <ESP8266mDNS.h>
#endif
#include <ArduinoOTA.h>
#include <WiFiUdp.h>
@@ -61,12 +57,7 @@ class OTASettingsService : public StatefulService<OTASettings> {
ArduinoOTAClass * _arduinoOTA;
void configureArduinoOTA();
#ifdef ESP32
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
#elif defined(ESP8266)
WiFiEventHandler _onStationModeGotIPHandler;
void onStationModeGotIP(const WiFiEventStationModeGotIP & event);
#endif
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
};
#endif // end OTASettingsService_h

View File

@@ -1,9 +1,9 @@
#include <RestartService.h>
using namespace std::placeholders; // for `_1` etc
RestartService::RestartService(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(RESTART_SERVICE_PATH,
HTTP_POST,
securityManager->wrapRequest(std::bind(&RestartService::restart, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
server->on(RESTART_SERVICE_PATH, HTTP_POST, securityManager->wrapRequest(std::bind(&RestartService::restart, this, _1), AuthenticationPredicates::IS_ADMIN));
}
void RestartService::restart(AsyncWebServerRequest * request) {

View File

@@ -1,13 +1,8 @@
#ifndef RestartService_h
#define RestartService_h
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>

View File

@@ -9,6 +9,7 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer * server, FS * f
, _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE)
, _jwtHandler(FACTORY_JWT_SECRET) {
addUpdateHandler([&](const String & originId) { configureJWTHandler(); }, false);
server->on(GENERATE_TOKEN_PATH, HTTP_GET, wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
}
void SecuritySettingsService::begin() {
@@ -109,6 +110,21 @@ ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequest
};
}
void SecuritySettingsService::generateToken(AsyncWebServerRequest* request) {
AsyncWebParameter* usernameParam = request->getParam("username");
for (User _user : _state.users) {
if (_user.username == usernameParam->value()) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, GENERATE_TOKEN_SIZE);
JsonObject root = response->getRoot();
root["token"] = generateJWT(&_user);
response->setLength();
request->send(response);
return;
}
}
request->send(401);
}
#else
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);

View File

@@ -25,6 +25,9 @@
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
#define GENERATE_TOKEN_SIZE 512
#define GENERATE_TOKEN_PATH "/rest/generateToken"
#if FT_ENABLED(FT_SECURITY)
class SecuritySettings {
@@ -83,6 +86,8 @@ class SecuritySettingsService : public StatefulService<SecuritySettings>, public
FSPersistence<SecuritySettings> _fsPersistence;
ArduinoJsonJWT _jwtHandler;
void generateToken(AsyncWebServerRequest * request);
void configureJWTHandler();
/*

View File

@@ -6,10 +6,8 @@
#include <list>
#include <functional>
#ifdef ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#endif
#ifndef DEFAULT_BUFFER_SIZE
#define DEFAULT_BUFFER_SIZE 1024
@@ -45,16 +43,10 @@ template <class T>
class StatefulService {
public:
template <typename... Args>
#ifdef ESP32
StatefulService(Args &&... args)
: _state(std::forward<Args>(args)...)
, _accessMutex(xSemaphoreCreateRecursiveMutex()) {
}
#else
StatefulService(Args &&... args)
: _state(std::forward<Args>(args)...) {
}
#endif
update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true) {
if (!cb) {
@@ -131,21 +123,15 @@ class StatefulService {
T _state;
inline void beginTransaction() {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
}
inline void endTransaction() {
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
private:
#ifdef ESP32
SemaphoreHandle_t _accessMutex;
#endif
SemaphoreHandle_t _accessMutex;
std::list<StateUpdateHandlerInfo_t> _updateHandlers;
};

View File

@@ -1,13 +1,15 @@
#include <SystemStatus.h>
using namespace std::placeholders; // for `_1` etc
SystemStatus::SystemStatus(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
}
void SystemStatus::systemStatus(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_ESP_STATUS_SIZE);
JsonObject root = response->getRoot();
root["esp_platform"] = "esp32";
root["esp_platform"] = "ESP32";
root["max_alloc_heap"] = ESP.getMaxAllocHeap();
root["psram_size"] = ESP.getPsramSize();
root["free_psram"] = ESP.getFreePsram();

View File

@@ -1,21 +1,10 @@
#include <UploadFirmwareService.h>
using namespace std::placeholders; // for `_1` etc
UploadFirmwareService::UploadFirmwareService(AsyncWebServer * server, SecurityManager * securityManager)
: _securityManager(securityManager) {
server->on(UPLOAD_FIRMWARE_PATH,
HTTP_POST,
std::bind(&UploadFirmwareService::uploadComplete, this, std::placeholders::_1),
std::bind(&UploadFirmwareService::handleUpload,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
std::placeholders::_5,
std::placeholders::_6));
#ifdef ESP8266
Update.runAsync(true);
#endif
server->on(UPLOAD_FIRMWARE_PATH, HTTP_POST, std::bind(&UploadFirmwareService::uploadComplete, this, _1), std::bind(&UploadFirmwareService::handleUpload, this, _1, _2, _3, _4, _5, _6));
}
void UploadFirmwareService::handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final) {
@@ -72,9 +61,5 @@ void UploadFirmwareService::handleError(AsyncWebServerRequest * request, int cod
}
void UploadFirmwareService::handleEarlyDisconnect() {
#ifdef ESP32
Update.abort();
#elif defined(ESP8266)
Update.end();
#endif
}

View File

@@ -3,14 +3,9 @@
#include <Arduino.h>
#ifdef ESP32
#include <Update.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>

View File

@@ -10,6 +10,8 @@
#define WEB_SOCKET_ORIGIN "websocket"
#define WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX "websocket:"
using namespace std::placeholders; // for `_1` etc
template <class T>
class WebSocketConnector {
protected:
@@ -18,27 +20,15 @@ class WebSocketConnector {
AsyncWebSocket _webSocket;
size_t _bufferSize;
WebSocketConnector(StatefulService<T> * statefulService,
AsyncWebServer * server,
const char * webSocketPath,
SecurityManager * securityManager,
AuthenticationPredicate authenticationPredicate,
size_t bufferSize)
WebSocketConnector(StatefulService<T> * statefulService, AsyncWebServer * server, const char * webSocketPath, SecurityManager * securityManager, AuthenticationPredicate authenticationPredicate, size_t bufferSize)
: _statefulService(statefulService)
, _server(server)
, _webSocket(webSocketPath)
, _bufferSize(bufferSize) {
_webSocket.setFilter(securityManager->filterRequest(authenticationPredicate));
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
std::placeholders::_5,
std::placeholders::_6));
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, this, _1, _2, _3, _4, _5, _6));
_server->addHandler(&_webSocket);
_server->on(webSocketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, std::placeholders::_1));
_server->on(webSocketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, _1));
}
WebSocketConnector(StatefulService<T> * statefulService, AsyncWebServer * server, const char * webSocketPath, size_t bufferSize)
@@ -46,14 +36,7 @@ class WebSocketConnector {
, _server(server)
, _webSocket(webSocketPath)
, _bufferSize(bufferSize) {
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
std::placeholders::_5,
std::placeholders::_6));
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, this, _1, _2, _3, _4, _5, _6));
_server->addHandler(&_webSocket);
}
@@ -84,11 +67,7 @@ class WebSocketTx : virtual public WebSocketConnector<T> {
WebSocketConnector<T>::_statefulService->addUpdateHandler([&](const String & originId) { transmitData(nullptr, originId); }, false);
}
WebSocketTx(JsonStateReader<T> stateReader,
StatefulService<T> * statefulService,
AsyncWebServer * server,
const char * webSocketPath,
size_t bufferSize = DEFAULT_BUFFER_SIZE)
WebSocketTx(JsonStateReader<T> stateReader, StatefulService<T> * statefulService, AsyncWebServer * server, const char * webSocketPath, size_t bufferSize = DEFAULT_BUFFER_SIZE)
: WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize)
, _stateReader(stateReader) {
WebSocketConnector<T>::_statefulService->addUpdateHandler([&](const String & originId) { transmitData(nullptr, originId); }, false);
@@ -161,11 +140,7 @@ class WebSocketRx : virtual public WebSocketConnector<T> {
, _stateUpdater(stateUpdater) {
}
WebSocketRx(JsonStateUpdater<T> stateUpdater,
StatefulService<T> * statefulService,
AsyncWebServer * server,
const char * webSocketPath,
size_t bufferSize = DEFAULT_BUFFER_SIZE)
WebSocketRx(JsonStateUpdater<T> stateUpdater, StatefulService<T> * statefulService, AsyncWebServer * server, const char * webSocketPath, size_t bufferSize = DEFAULT_BUFFER_SIZE)
: WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize)
, _stateUpdater(stateUpdater) {
}
@@ -207,12 +182,7 @@ class WebSocketTxRx : public WebSocketTx<T>, public WebSocketRx<T> {
, WebSocketRx<T>(stateUpdater, statefulService, server, webSocketPath, securityManager, authenticationPredicate, bufferSize) {
}
WebSocketTxRx(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T> * statefulService,
AsyncWebServer * server,
const char * webSocketPath,
size_t bufferSize = DEFAULT_BUFFER_SIZE)
WebSocketTxRx(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater, StatefulService<T> * statefulService, AsyncWebServer * server, const char * webSocketPath, size_t bufferSize = DEFAULT_BUFFER_SIZE)
: WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize)
, WebSocketTx<T>(stateReader, statefulService, server, webSocketPath, bufferSize)
, WebSocketRx<T>(stateUpdater, statefulService, server, webSocketPath, bufferSize) {

View File

@@ -1,8 +1,10 @@
#include <WiFiScanner.h>
using namespace std::placeholders; // for `_1` etc
WiFiScanner::WiFiScanner(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, _1), AuthenticationPredicates::IS_ADMIN));
server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, _1), AuthenticationPredicates::IS_ADMIN));
};
void WiFiScanner::scanNetworks(AsyncWebServerRequest * request) {

View File

@@ -1,13 +1,8 @@
#ifndef WiFiScanner_h
#define WiFiScanner_h
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ArduinoJson.h>
#include <AsyncJson.h>
@@ -26,10 +21,6 @@ class WiFiScanner {
private:
void scanNetworks(AsyncWebServerRequest * request);
void listNetworks(AsyncWebServerRequest * request);
#ifdef ESP8266
uint8_t convertEncryptionType(uint8_t encryptionType);
#endif
};
#endif // end WiFiScanner_h

View File

@@ -22,44 +22,24 @@
namespace uuid {
#define UPTIME_OVERFLOW 4294967295 // Uptime overflow value
// added by proddy, modified
static uint64_t now_millis = 0;
// returns system uptime in seconds
uint32_t get_uptime_sec() {
static uint32_t last_uptime = 0;
static uint8_t uptime_overflows = 0;
if (millis() < last_uptime) {
++uptime_overflows;
}
last_uptime = millis();
uint32_t uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000);
return uptime_seconds;
return (uint32_t)(now_millis / 1000ULL);
}
uint64_t get_uptime_ms() {
static uint32_t high_millis = 0;
static uint32_t low_millis = 0;
if (get_uptime() < low_millis) {
high_millis++;
}
low_millis = get_uptime();
return ((uint64_t)high_millis << 32) | low_millis;
}
// added by proddy
static uint32_t now_millis;
void set_uptime() {
now_millis = ::millis();
}
uint32_t get_uptime() {
return now_millis;
}
void set_uptime() {
now_millis = esp_timer_get_time() / 1000ULL;
}
uint32_t get_uptime() {
return (uint32_t)now_millis;
}
} // namespace uuid

View File

@@ -22,7 +22,6 @@ namespace uuid {
void loop() {
set_uptime(); // added by proddy
get_uptime_ms();
}
} // namespace uuid

View File

@@ -50,10 +50,7 @@ void Commands::add_command(const flash_string_vector & name, const flash_string_
add_command(0, 0, name, arguments, function, nullptr);
}
void Commands::add_command(const flash_string_vector & name,
const flash_string_vector & arguments,
command_function function,
argument_completion_function arg_function) {
void Commands::add_command(const flash_string_vector & name, const flash_string_vector & arguments, command_function function, argument_completion_function arg_function) {
add_command(0, 0, name, arguments, function, arg_function);
}
@@ -61,20 +58,11 @@ void Commands::add_command(unsigned int context, unsigned int flags, const flash
add_command(context, flags, name, flash_string_vector{}, function, nullptr);
}
void Commands::add_command(unsigned int context,
unsigned int flags,
const flash_string_vector & name,
const flash_string_vector & arguments,
command_function function) {
void Commands::add_command(unsigned int context, unsigned int flags, const flash_string_vector & name, const flash_string_vector & arguments, command_function function) {
add_command(context, flags, name, arguments, function, nullptr);
}
void Commands::add_command(unsigned int context,
unsigned int flags,
const flash_string_vector & name,
const flash_string_vector & arguments,
command_function function,
argument_completion_function arg_function) {
void Commands::add_command(unsigned int context, unsigned int flags, const flash_string_vector & name, const flash_string_vector & arguments, command_function function, argument_completion_function arg_function) {
commands_.emplace(std::piecewise_construct, std::forward_as_tuple(context), std::forward_as_tuple(flags, name, arguments, function, arg_function));
}
@@ -179,8 +167,7 @@ bool Commands::find_longest_common_prefix(const std::multimap<size_t, const Comm
for (auto command_it = std::next(commands.begin()); command_it != commands.end(); command_it++) {
// This relies on the null terminator character limiting the
// length before it becomes longer than any of the strings
if (pgm_read_byte(reinterpret_cast<PGM_P>(first) + length)
!= pgm_read_byte(reinterpret_cast<PGM_P>(*std::next(command_it->second->name_.begin(), component_prefix)) + length)) {
if (pgm_read_byte(reinterpret_cast<PGM_P>(first) + length) != pgm_read_byte(reinterpret_cast<PGM_P>(*std::next(command_it->second->name_.begin(), component_prefix)) + length)) {
all_match = false;
break;
}
@@ -275,8 +262,7 @@ Commands::Completion Commands::complete_command(Shell & shell, const CommandLine
result.replacement->push_back(std::move(read_flash_string(name)));
}
if (command_line.total_size() > result.replacement->size()
&& command_line.total_size() <= matching_command->name_.size() + matching_command->maximum_arguments()) {
if (command_line.total_size() > result.replacement->size() && command_line.total_size() <= matching_command->name_.size() + matching_command->maximum_arguments()) {
// Try to auto-complete arguments
std::vector<std::string> arguments{std::next(command_line->cbegin(), result.replacement->size()), command_line->cend()};
@@ -526,11 +512,7 @@ void Commands::for_each_available_command(Shell & shell, apply_function f) const
}
}
Commands::Command::Command(unsigned int flags,
const flash_string_vector name,
const flash_string_vector arguments,
command_function function,
argument_completion_function arg_function)
Commands::Command::Command(unsigned int flags, const flash_string_vector name, const flash_string_vector arguments, command_function function, argument_completion_function arg_function)
: flags_(flags)
, name_(name)
, arguments_(arguments)

View File

@@ -65,8 +65,10 @@ void Shell::start() {
#endif
line_buffer_.reserve(maximum_command_line_length_);
line_old_.reserve(maximum_command_line_length_);
line_old_.clear();
for (uint8_t i = 0; i < MAX_LINES; i++) {
line_old_[i].reserve(maximum_command_line_length_);
line_old_[i].clear();
}
display_banner();
display_prompt();
shells_.insert(shared_from_this());
@@ -156,7 +158,8 @@ void Shell::loop_normal() {
// Interrupt (^C)
line_buffer_.clear();
println();
cursor_ = 0;
cursor_ = 0;
line_no_ = 0;
break;
case '\x04':
@@ -172,13 +175,15 @@ void Shell::loop_normal() {
// Del/Backspace (^?)
if (line_buffer_.length() > cursor_) {
line_buffer_.erase(line_buffer_.length() - cursor_ - 1, 1);
line_no_ = 0;
}
break;
case '\x09':
// Tab (^I)
process_completion();
cursor_ = 0;
cursor_ = 0;
line_no_ = 0;
break;
case '\x0A':
@@ -198,12 +203,14 @@ void Shell::loop_normal() {
case '\x15':
// Delete line (^U)
line_buffer_.clear();
cursor_ = 0;
cursor_ = 0;
line_no_ = 0;
break;
case '\x17':
// Delete word (^W)
delete_buffer_word(true);
line_no_ = 0;
break;
case '\033':
@@ -214,10 +221,20 @@ void Shell::loop_normal() {
default:
if (esc_) {
if (c == 'A') { // cursor up
line_buffer_ = line_old_;
cursor_ = 0;
line_buffer_ = line_old_[line_no_];
if (line_no_ < MAX_LINES - 1) {
line_no_++;
}
cursor_ = 0;
} else if (c == 'B') { // cursor down
line_buffer_.clear();
if (line_no_) {
line_no_--;
}
if (line_no_) {
line_buffer_ = line_old_[line_no_ - 1];
} else {
line_buffer_.clear();
}
cursor_ = 0;
} else if (c == 'C') { // cursor right
if (cursor_) {
@@ -239,6 +256,7 @@ void Shell::loop_normal() {
if ((esc_ == 3) && cursor_) { // del
cursor_--;
line_buffer_.erase(line_buffer_.length() - cursor_ - 1, 1);
line_no_ = 0;
} else if (esc_ == 4) { // end
cursor_ = 0;
} else if (esc_ == 1) { // pos1
@@ -279,6 +297,7 @@ void Shell::loop_normal() {
} else if (c >= '\x20' && c <= '\x7E') {
if (line_buffer_.length() < maximum_command_line_length_) {
line_buffer_.insert(line_buffer_.length() - cursor_, 1, c);
line_no_ = 0;
}
}
break;
@@ -498,7 +517,12 @@ void Shell::process_command() {
println();
return;
}
line_old_ = line_buffer_;
uint8_t no = line_no_ ? line_no_ : MAX_LINES;
while (--no) {
line_old_[no] = line_old_[no - 1];
}
line_no_ = 0;
line_old_[0] = line_buffer_;
while (!line_buffer_.empty()) {
size_t pos = line_buffer_.find(';');
std::string line1;

View File

@@ -96,7 +96,6 @@ void Shell::output_logs() {
}
::yield();
}
display_prompt();
}

View File

@@ -61,8 +61,9 @@ class Commands;
*/
class Shell : public std::enable_shared_from_this<Shell>, public uuid::log::Handler, public ::Stream {
public:
static constexpr size_t MAX_COMMAND_LINE_LENGTH = 80; /*!< Maximum length of a command line. @since 0.1.0 */
static constexpr size_t MAX_LOG_MESSAGES = 20; /*!< Maximum number of log messages to buffer before they are output. @since 0.1.0 */
static constexpr size_t MAX_COMMAND_LINE_LENGTH = 80; /*!< Maximum length of a command line. @since 0.1.0 */
static constexpr size_t MAX_LOG_MESSAGES = 20; /*!< Maximum number of log messages to buffer before they are output. @since 0.1.0 */
static constexpr uint8_t MAX_LINES = 5; /*!< Maximum lines in buffer */
/**
* Function to handle the response to a password entry prompt.
@@ -903,8 +904,9 @@ class Shell : public std::enable_shared_from_this<Shell>, public uuid::log::Hand
unsigned long log_message_id_ = 0; /*!< The next identifier to use for queued log messages. @since 0.1.0 */
std::list<QueuedLogMessage> log_messages_; /*!< Queued log messages, in the order they were received. @since 0.1.0 */
size_t maximum_log_messages_ = MAX_LOG_MESSAGES; /*!< Maximum command line length in bytes. @since 0.6.0 */
std::string line_buffer_; /*!< Command line buffer. Limited to maximum_command_line_length() bytes. @since 0.1.0 */
std::string line_old_; /*!< old Command line buffer.*/
std::string line_buffer_; /*!< Command line buffer. Limited to maximum_command_line_length() bytes. @since 0.1.0 */
std::string line_old_[MAX_LINES]; /*!< old Command line buffer.*/
uint8_t line_no_ = 0;
size_t maximum_command_line_length_ = MAX_COMMAND_LINE_LENGTH; /*!< Maximum command line length in bytes. @since 0.6.0 */
unsigned char previous_ = 0; /*!< Previous character that was entered on the command line. Used to detect CRLF line endings. @since 0.1.0 */
uint8_t cursor_ = 0; /*!< cursor position from end of line */

View File

@@ -19,13 +19,11 @@
#include "uuid/syslog.h"
#include <Arduino.h>
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WiFi.h>
#else
#include <WiFi.h>
#endif
#include <WiFiUdp.h>
#include "../../../src/emsesp.h"
#ifndef UUID_SYSLOG_HAVE_GETTIMEOFDAY
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
// time() does not return UTC on the ESP8266: https://github.com/esp8266/Arduino/issues/4637
@@ -191,7 +189,8 @@ void SyslogService::mark_interval(unsigned long interval) {
SyslogService::QueuedLogMessage::QueuedLogMessage(unsigned long id, std::shared_ptr<uuid::log::Message> && content)
: id_(id)
, content_(std::move(content)) {
if (time_good_ || WiFi.status() == WL_CONNECTED) {
// Added by proddy - check for Ethernet too. This assumes the network has already started.
if (time_good_ || emsesp::EMSESP::system_.network_connected()) {
#if UUID_SYSLOG_HAVE_GETTIMEOFDAY
if (gettimeofday(&time_, nullptr) != 0) {
time_.tv_sec = (time_t)-1;
@@ -200,7 +199,6 @@ SyslogService::QueuedLogMessage::QueuedLogMessage(unsigned long id, std::shared_
time_.tv_sec = time(nullptr);
time_.tv_usec = 0;
#endif
if (time_.tv_sec >= 0 && time_.tv_sec < 18140 * 86400) {
time_.tv_sec = (time_t)-1;
}
@@ -269,8 +267,8 @@ bool SyslogService::can_transmit() {
}
#endif
if (WiFi.status() != WL_CONNECTED) {
return false;
if (!emsesp::EMSESP::system_.network_connected()) {
return false; // added by proddy. Check Ethernet
}
const uint64_t now = uuid::get_uptime_ms();
@@ -387,15 +385,20 @@ bool SyslogService::can_transmit() {
}
bool SyslogService::transmit(const QueuedLogMessage & message) {
/*
// modifications by Proddy. From https://github.com/emsesp/EMS-ESP/issues/395#issuecomment-640053528
struct tm tm;
int8_t tzh = 0;
int8_t tzm = 0;
tm.tm_year = 0;
if (message.time_.tv_sec != (time_t)-1) {
gmtime_r(&message.time_.tv_sec, &tm);
struct tm utc;
gmtime_r(&message.time_.tv_sec, &utc);
localtime_r(&message.time_.tv_sec, &tm);
int16_t diff = 60 * (tm.tm_hour - utc.tm_hour) + tm.tm_min - utc.tm_min;
diff = diff > 720 ? diff - 1440 : diff < -720 ? diff + 1440 : diff;
tzh = diff / 60;
tzm = diff < 0 ? (0 - diff) % 60 : diff % 60;
}
*/
if (udp_.beginPacket(host_, port_) != 1) {
last_transmit_ = uuid::get_uptime_ms();
@@ -403,30 +406,16 @@ bool SyslogService::transmit(const QueuedLogMessage & message) {
}
udp_.printf_P(PSTR("<%u>1 "), ((unsigned int)message.content_->facility * 8) + std::min(7U, (unsigned int)message.content_->level));
/*
if (tm.tm_year != 0) {
udp_.printf_P(PSTR("%04u-%02u-%02uT%02u:%02u:%02u.%06luZ"),
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
tm.tm_sec,
(uint32_t)message.time_.tv_usec);
udp_.printf_P(PSTR("%04u-%02u-%02uT%02u:%02u:%02u.%06u%+02d:%02d"), tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, (uint32_t)message.time_.tv_usec, tzh, tzm);
} else {
udp_.print('-');
}
*/
udp_.print('-');
udp_.printf_P(PSTR(" %s - - - - \xEF\xBB\xBF"), hostname_.c_str());
udp_.printf_P(PSTR(" %s %s - - - \xEF\xBB\xBF"), hostname_.c_str(), uuid::read_flash_string(message.content_->name).c_str());
udp_.print(uuid::log::format_timestamp_ms(message.content_->uptime_ms, 3).c_str());
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat"
udp_.printf_P(PSTR(" %c %lu: [%S] "), uuid::log::format_level_char(message.content_->level), message.id_, message.content_->name);
#pragma GCC diagnostic pop
udp_.printf_P(PSTR(" %c %lu: "), uuid::log::format_level_char(message.content_->level), message.id_);
udp_.print(message.content_->text.c_str());
bool ok = (udp_.endPacket() == 1);

View File

@@ -61,6 +61,10 @@ unsigned long millis() {
return __millis;
}
int64_t esp_timer_get_time() {
return __millis;
}
void delay(unsigned long millis) {
// __millis += millis;
}

View File

@@ -53,11 +53,7 @@ int digitalRead(uint8_t pin);
#define PROGMEM
#define PGM_P const char *
#define PSTR(s) \
(__extension__({ \
static const char __c[] = (s); \
&__c[0]; \
}))
#define PSTR(s) s
class __FlashStringHelper;
#define FPSTR(string_literal) (reinterpret_cast<const __FlashStringHelper *>(string_literal))
@@ -196,11 +192,13 @@ class NativeConsole : public Stream {
#include <Network.h>
extern NativeConsole Serial;
extern ETHClass ETH;
extern WiFiClass WiFi;
extern ETHClass ETH;
extern WiFiClass WiFi;
unsigned long millis();
int64_t esp_timer_get_time();
void delay(unsigned long millis);
void yield(void);

View File

@@ -28,15 +28,16 @@ class DummySettings {
bool api_enabled = true;
// MQTT
uint16_t publish_time = 10; // seconds
uint8_t mqtt_qos = 0;
bool mqtt_retain = false;
bool enabled = true;
uint8_t dallas_format = 1;
bool nested_format = true;
uint8_t ha_climate_format = 1;
bool ha_enabled = true;
std::string base = "ems-esp";
uint16_t publish_time = 10; // seconds
uint8_t mqtt_qos = 0;
bool mqtt_retain = false;
bool enabled = true;
uint8_t dallas_format = 1;
uint8_t nested_format = 1;
uint8_t ha_climate_format = 1;
bool ha_enabled = true;
String base = "ems-esp";
uint8_t subscribe_format = 0;
String hostname = "ems-esp";
String jwtSecret = "ems-esp";
@@ -48,7 +49,7 @@ class DummySettings {
String staticIPConfig = "";
String dnsIP1 = "";
String dnsIP2 = "";
uint8_t ethernet_profile = 0;
String board_profile = "CUSTOM";
uint16_t publish_time_boiler = 10;
uint16_t publish_time_thermostat = 10;
uint16_t publish_time_solar = 10;

View File

@@ -33,7 +33,7 @@ CXX_STANDARD := -std=c++11
#----------------------------------------------------------------------
# Defined Symbols
#----------------------------------------------------------------------
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_DEFAULT_BOARD_PROFILE=\"LOLIN\"
#----------------------------------------------------------------------
# Sources & Files

29
mock-api/README.md Normal file
View File

@@ -0,0 +1,29 @@
(https://github.com/emsesp/EMS-ESP32/issues/41)
When developing and testing the web interface, it's handy not to bother with re-flashing an ESP32 each time. The idea is to mimic the ESP using a mock/stub server that responds to the REST (HTTP POST & GET) and WebSocket calls.
To set it up it do
```sh
% cd mock-api
% npm install
% cd interface
% npm install
```
and to run it
```sh
% cd interface
% npm run dev
```
## Notes
- It's for local development only
- `src/.env.development` is no longer required
- CORS is removed, also the build flag
- new file `interface/src/setupProxy.js`
- new files `mock-api/server.js` with the hardcoded data. Requires its own npm packages for express
## ToDo
- add filter rule to prevent from exposing yourself to malicious attacks when running the dev server(https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a)

2818
mock-api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
mock-api/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "api",
"version": "1.0.0",
"private": "true",
"description": "mock api for EMS-ESP",
"main": "server.js",
"scripts": {
"dev": "nodemon ./server.js localhost 3080",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "proddy",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"nodemon": "^2.0.7"
}
}

344
mock-api/server.js Normal file
View File

@@ -0,0 +1,344 @@
const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 3080;
app.use(express.static(path.join(__dirname, '../interface/build')));
app.use(express.json());
const ENDPOINT_ROOT = "/rest/";
// NTP
const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
const ntp_settings = {
"enabled": true, "server": "time.google.com", "tz_label": "Europe/Amsterdam", "tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
};
const ntp_status = {
"status": 1, "utc_time": "2021-04-01T14:25:42Z", "local_time": "2021-04-01T16:25:42", "server": "time.google.com", "uptime": 856
}
// AP
const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
const ap_settings = {
"provision_mode": 1, "ssid": "ems-esp", "password": "ems-esp-neo", "local_ip": "192.168.4.1",
"gateway_ip": "192.168.4.1", "subnet_mask": "255.255.255.0"
};
const ap_status = {
"status": 1, "ip_address": "192.168.4.1", "mac_address": "3C:61:05:03:AB:2D", "station_num": 0
};
// NETWORK
const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "networkSettings";
const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + "networkStatus";
const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
const network_settings = {
ssid: "myWifi", password: 'myPassword', hostname: 'ems-esp', static_ip_config: false
};
const network_status =
{
"status": 3, "local_ip": "10.10.10.101", "mac_address": "3C:61:05:03:AB:2C", "rssi": -41, "ssid": "home",
"bssid": "06:ED:DA:FE:B4:68", "channel": 11, "subnet_mask": "255.255.255.0", "gateway_ip": "10.10.10.1",
"dns_ip_1": "10.10.10.1", "dns_ip_2": "0.0.0.0"
};
const list_networks = {
"networks": [
{ "rssi": -40, "ssid": "", "bssid": "FC:EC:DA:FD:B4:68", "channel": 11, "encryption_type": 3 },
{ "rssi": -41, "ssid": "home", "bssid": "02:EC:DA:FD:B4:68", "channel": 11, "encryption_type": 3 },
{ "rssi": -42, "ssid": "", "bssid": "06:EC:DA:FD:B4:68", "channel": 11, "encryption_type": 3 },
{ "rssi": -73, "ssid": "", "bssid": "FC:EC:DA:17:D4:7E", "channel": 1, "encryption_type": 3 },
{ "rssi": -73, "ssid": "office", "bssid": "02:EC:DA:17:D4:7E", "channel": 1, "encryption_type": 3 },
{ "rssi": -75, "ssid": "Erica", "bssid": "C8:D7:19:9A:88:BD", "channel": 2, "encryption_type": 3 },
{ "rssi": -75, "ssid": "", "bssid": "C6:C9:E3:FF:A5:DE", "channel": 2, "encryption_type": 3 },
{ "rssi": -76, "ssid": "Bruin", "bssid": "C0:C9:E3:FF:A5:DE", "channel": 2, "encryption_type": 3 },
]
};
// OTA
const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
const ota_settings = {
"enabled": true, "port": 8266, "password": "ems-esp-neo"
};
// MQTT
const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
const mqtt_settings = {
"enabled": true, "host": "192.168.1.4", "port": 1883, "base": "ems-esp32", "username": "", "password": "",
"client_id": "ems-esp32", "keep_alive": 60, "clean_session": true, "max_topic_length": 128,
"publish_time_boiler": 10, "publish_time_thermostat": 10, "publish_time_solar": 10, "publish_time_mixer": 10,
"publish_time_other": 10, "publish_time_sensor": 10, "mqtt_qos": 0, "mqtt_retain": false, "dallas_format": 1,
"bool_format": 1, "ha_climate_format": 1, "ha_enabled": true, "nested_format": 1, "subscribe_format": 0
};
const mqtt_status = {
"enabled": true, "connected": true, "client_id": "ems-esp32", "disconnect_reason": 0, "mqtt_fails": 0
};
// SYSTEM
const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
const system_status = {
"esp_platform": "ESP32", "max_alloc_heap": 113792, "psram_size": 0, "free_psram": 0, "cpu_freq_mhz": 240,
"free_heap": 193340, "sdk_version": "v3.3.5-1-g85c43024c", "flash_chip_size": 4194304, "flash_chip_speed": 40000000,
"fs_total": 65536, "fs_used": 16384, "uptime": "000+00:15:42.707"
};
const security_settings = {
"jwt_secret": "naughty!", "users": [{ "username": "admin", "password": "admin", "admin": true }, { "username": "guest", "password": "guest", "admin": false }]
};
const features = {
"project": true, "security": true, "mqtt": true, "ntp": true, "ota": true, "upload_firmware": true
};
const verify_authentication = { access_token: '1234' };
const signin = {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInZlcnNpb24iOiIzLjAuMmIwIn0.MsHSgoJKI1lyYz77EiT5ZN3ECMrb4mPv9FNy3udq0TU"
};
const generate_token = { token: '1234' };
// EMS-ESP Project specific
const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
const EMSESP_ALLDEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices";
const EMSESP_SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
const EMSESP_DEVICEDATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + "emsespStatus";
const EMSESP_BOARDPROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + "writeValue";
const emsesp_settings = {
"tx_mode": 1, "tx_delay": 0, "ems_bus_id": 11, "syslog_enabled": false, "syslog_level": 3,
"trace_raw": false, "syslog_mark_interval": 0, "syslog_host": "192.168.1.4", "syslog_port": 514,
"master_thermostat": 0, "shower_timer": true, "shower_alert": false, "rx_gpio": 23, "tx_gpio": 5,
"dallas_gpio": 3, "dallas_parasite": false, "led_gpio": 2, "hide_led": false, "api_enabled": true,
"analog_enabled": false, "pbutton_gpio": 0, "board_profile": "S32"
};
const emsesp_alldevices = {
"devices": [{
"id": 1, "type": "Thermostat", "brand": "", "name": "RC20/Moduline 300",
"deviceid": 23, "productid": 77, "version": "03.03"
}, {
"id": 2, "type": "Boiler", "brand": "Nefit", "name": "GBx72/Trendline/Cerapur/Greenstar Si/27i",
"deviceid": 8, "productid": 123, "version": "06.01"
}, {
"id": 3, "type": "Controller", "brand": "", "name": "BC10",
"deviceid": 9, "productid": 190, "version": "01.03"
}],
"sensors": []
}
const emsesp_status = {
"status": 0, "rx_received": 344, "tx_sent": 104, "rx_quality": 100, "tx_quality": 100
};
const emsesp_devicedata_1 = {
"name": "Thermostat: RC20/Moduline 300",
"data": [
"16:28:21 01/04/2021", "", "date/time", "datetime",
"(0)", "", "error code", "",
15, "°C", "(hc1) setpoint room temperature", "temp",
20.5, "°C", "(hc1) current room temperature", "",
"auto", "", "(hc1) mode", "mode"
]
};
const emsesp_devicedata_2 = {
"name": "Boiler: Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i",
"data": [
"off", "", "heating active", "",
"off", "", "warm water active", "",
5, "°C", "selected flow temperature", "selflowtemp",
0, "%", "burner selected max power", "",
0, "%", "heating pump modulation", "",
42.7, "°C", "current flow temperature", "",
39, "°C", "return temperature", "",
1.2, "bar", "system pressure", "",
45.3, "°C", "max boiler temperature", "",
"off", "", "gas", "",
0, "uA", "flame current", "",
"off", "", "heating pump", "",
"off", "", "fan", "",
"off", "", "ignition", "",
"on", "", "heating activated", "",
75, "°C", "heating temperature", "",
90, "%", "burner pump max power", "",
55, "%", "burner pump min power", "",
1, null, "pump delay", "",
10, null, "burner min period", "",
0, "%", "burner min power", "",
75, "%", "burner max power", "",
-6, "°C", "hysteresis on temperature", "",
6, "°C", "hysteresis off temperature", "",
0, "%", "burner current power", "",
295740, "", "burner # starts", "",
"344 days 2 hours 8 minutes", null, "total burner operating time", "",
"279 days 11 hours 55 minutes", null, "total heat operating time", "",
"2946 days 19 hours 8 minutes", null, "total UBA operating time", "",
"1C(210) 06.06.2020 12:07", "", "last error code", "",
"0H", "", "service code", "",
203, "", "service code number", "",
"01.01.2012", "", "maintenance set date", "",
"off", "", "maintenance scheduled", "",
6000, "hours", "maintenance set time", "",
60, "°C", "(warm water) selected temperature", "",
62, "°C", "(warm water) set temperature", "",
"flow", "", "(warm water) type", "",
"hot", "", "(warm water) comfort", "",
40, "", "(warm water) flow temperature offset", "",
100, "%", "(warm water) max power", "",
"off", "", "(warm water) circulation pump available", "",
"3-way valve", "", "(warm water) charging type", "",
70, "°C", "(warm water) disinfection temperature", "",
"off", "", "(warm water) circulation pump freq", "",
"off", "", "(warm water) circulation active", "",
34.7, "°C", "(warm water) current intern temperature", "",
0, "l/min", "(warm water) current tap water flow", "",
34.6, "°C", "(warm water) storage intern temperature", "",
"on", "", "(warm water) activated", "",
"off", "", "(warm water) one time charging", "",
"off", "", "(warm water) disinfecting", "",
"off", "", "(warm water) charging", "",
"off", "", "(warm water) recharging", "",
"on", "", "(warm water) temperature ok", "",
"off", "", "(warm water) active", "",
"on", "", "(warm water) heating", "",
262387, "", "(warm water) # starts", "",
"64 days 14 hours 13 minutes", null, "(warm water) active time", ""
]
};
// NETWORK
app.get(NETWORK_STATUS_ENDPOINT, (req, res) => { res.json(network_status); });
app.get(NETWORK_SETTINGS_ENDPOINT, (req, res) => { res.json(network_settings); });
app.post(NETWORK_SETTINGS_ENDPOINT, (req, res) => { res.json(network_settings); });
app.get(LIST_NETWORKS_ENDPOINT, (req, res) => { res.json(list_networks); });
app.get(SCAN_NETWORKS_ENDPOINT, (req, res) => { res.sendStatus(202); });
// AP
app.get(AP_SETTINGS_ENDPOINT, (req, res) => { res.json(ap_settings); });
app.get(AP_STATUS_ENDPOINT, (req, res) => { res.json(ap_status); });
app.post(AP_SETTINGS_ENDPOINT, (req, res) => { res.json(ap_settings); });
// OTA
app.get(OTA_SETTINGS_ENDPOINT, (req, res) => { res.json(ota_settings); });
app.post(OTA_SETTINGS_ENDPOINT, (req, res) => { res.json(ota_settings); });
// MQTT
app.get(MQTT_SETTINGS_ENDPOINT, (req, res) => { res.json(mqtt_settings); });
app.post(MQTT_SETTINGS_ENDPOINT, (req, res) => { res.json(mqtt_settings); });
app.get(MQTT_STATUS_ENDPOINT, (req, res) => { res.json(mqtt_status); });
// NTP
app.get(NTP_SETTINGS_ENDPOINT, (req, res) => { res.json(ntp_settings); });
app.post(NTP_SETTINGS_ENDPOINT, (req, res) => { res.json(ntp_settings); });
app.get(NTP_STATUS_ENDPOINT, (req, res) => { res.json(ntp_status); });
app.post(TIME_ENDPOINT, (req, res) => { res.sendStatus(200); });
// SYSTEM
app.get(SYSTEM_STATUS_ENDPOINT, (req, res) => { res.json(system_status); });
app.get(SECURITY_SETTINGS_ENDPOINT, (req, res) => { res.json(security_settings); });
app.post(SECURITY_SETTINGS_ENDPOINT, (req, res) => { res.json(security_settings); });
app.get(FEATURES_ENDPOINT, (req, res) => { res.json(features); });
app.get(VERIFY_AUTHORIZATION_ENDPOINT, (req, res) => { res.json(verify_authentication); });
app.post(RESTART_ENDPOINT, (req, res) => { res.sendStatus(200); });
app.post(FACTORY_RESET_ENDPOINT, (req, res) => { res.sendStatus(200); });
app.post(UPLOAD_FIRMWARE_ENDPOINT, (req, res) => { res.sendStatus(200); });
app.post(SIGN_IN_ENDPOINT, (req, res) => { res.json(signin); });
app.get(GENERATE_TOKEN_ENDPOINT, (req, res) => { res.json(generate_token); });
// EMS-ESP Project stuff
app.get(EMSESP_SETTINGS_ENDPOINT, (req, res) => { res.json(emsesp_settings); });
app.post(EMSESP_SETTINGS_ENDPOINT, (req, res) => { res.json(emsesp_settings); });
app.get(EMSESP_ALLDEVICES_ENDPOINT, (req, res) => { res.json(emsesp_alldevices); });
app.post(EMSESP_SCANDEVICES_ENDPOINT, (req, res) => { res.sendStatus(200); });
app.get(EMSESP_STATUS_ENDPOINT, (req, res) => { res.json(emsesp_status); });
app.post(EMSESP_DEVICEDATA_ENDPOINT, (req, res) => {
const id = req.body.id;
if (id == 1) {
res.json(emsesp_devicedata_1);
}
if (id == 2) {
res.json(emsesp_devicedata_2);
}
});
app.post(WRITE_VALUE_ENDPOINT, (req, res) => {
const devicevalue = req.body.devicevalue;
console.log(devicevalue);
res.sendStatus(200);
});
app.post(EMSESP_BOARDPROFILE_ENDPOINT, (req, res) => {
const board_profile = req.body.code;
const data = {
led_gpio: 1,
dallas_gpio: 2,
rx_gpio: 3,
tx_gpio: 4,
pbutton_gpio: 5
};
if (board_profile == "S32") { // BBQKees Gateway S32
data.led_gpio = 2;
data.dallas_gpio = 18;
data.rx_gpio = 23;
data.tx_gpio = 5;
data.pbutton_gpio = 0;
} else if (board_profile == "E32") { // BBQKees Gateway E32
data.led_gpio = 2;
data.dallas_gpio = 4;
data.rx_gpio = 5;
data.tx_gpio = 17;
data.pbutton_gpio = 33;
} else if (board_profile == "MH-ET") { // MH-ET Live D1 Mini
data.led_gpio = 2;
data.dallas_gpio = 18;
data.rx_gpio = 23;
data.tx_gpio = 5;
data.pbutton_gpio = 0;
} else if (board_profile == "NODEMCU") { // NodeMCU 32S
data.led_gpio = 2;
data.dallas_gpio = 18;
data.rx_gpio = 23;
data.tx_gpio = 5;
data.pbutton_gpio = 0;
} else if (board_profile == "LOLIN") {// Lolin D32
data.led_gpio = 2;
data.dallas_gpio = 18;
data.rx_gpio = 17;
data.tx_gpio = 16;
data.pbutton_gpio = 0;
} else if (board_profile == "OLIMEX") {// Olimex ESP32-EVB (uses U1TXD/U1RXD/BUTTON, no LED or Dallas)
data.led_gpio = 0;
data.dallas_gpio = 0;
data.rx_gpio = 36;
data.tx_gpio = 4;
data.pbutton_gpio = 34;
// data = { 0, 0, 36, 4, 34};
} else if (board_profile == "TLK110") {// Generic Ethernet (TLK110)
data.led_gpio = 2;
data.dallas_gpio = 4;
data.rx_gpio = 5;
data.tx_gpio = 17;
data.pbutton_gpio = 33;
} else if (board_profile == "LAN8720") {// Generic Ethernet (LAN8720)
data.led_gpio = 2;
data.dallas_gpio = 4;
data.rx_gpio = 5;
data.tx_gpio = 17;
data.pbutton_gpio = 33;
}
res.json(data);
});
app.listen(port);
console.log(`Mock API Server is up and running at: http://localhost:${port}`);

View File

@@ -1,3 +1,5 @@
; example custom platformio.ini file for EMS-ESP
[env]
upload_protocol = espota
upload_flags =
@@ -6,14 +8,15 @@ upload_flags =
upload_port = 10.10.10.101
[common]
; debug_flags = -DENABLE_CORS -DEMSESP_TEST
; debug_flags = -DEMSESP_DEBUG -DEMSESP_TEST
debug_flags = -DEMSESP_TEST
; options are EMSESP_DEBUG EMSESP_UART_DEBUG EMSESP_DEBUG_SENSOR
; plus all the settings in default_settings.h, e.g. -DEMSESP_DEFAULT_BOARD_PROFILE=\"NODEMCU\"
; debug_flags = -DEMSESP_DEBUG
[env:esp32]
monitor_filters = esp32_exception_decoder
debug_tool = esp-prog
debug_init_break = tbreak setup
extra_scripts =
; to prevent the web UI from building each time, comment out this next line
; pre:scripts/build_interface.py
; scripts/rename_fw.py

View File

@@ -1,50 +1,37 @@
; PlatformIO Project Configuration File for EMS-ESP
; override any settings with your own local ones in pio_local.ini
[platformio]
default_envs = esp32
# override any settings with your own local ones in pio_local.ini
extra_configs =
factory_settings.ini
pio_local.ini
[common]
; default platformio compile flags are: -fno-rtti -std=c++11 -Os -mlongcalls -mtext-section-literals -falign-functions=4 -ffunction-sections -fdata-sections -fno-exceptions -Wall
core_build_flags = -Wno-deprecated-declarations
-Wreturn-type
-DCORE_DEBUG_LEVEL=0
-DNDEBUG
core_build_flags =
-Wall
-D CORE_DEBUG_LEVEL=0
-D NDEBUG
-D ARDUINO_ARCH_ESP32=1
-D ESP32=1
; -std=c++17 -std=gnu++17
esp32_build_flags = -DARDUINO_ARCH_ESP32=1
-DESP32=1
-DBOARD_HAS_PSRAM
; -std=c17 -std=c++17 -std=gnu++17
core_unbuild_flags =
; -std=gnu++11
build_flags =
${common.core_build_flags}
${factory_settings.build_flags}
-D FT_PROJECT=1
-D FT_SECURITY=1
-D FT_MQTT=1
-D FT_OTA=1
-D FT_NTP=1
-D FT_UPLOAD_FIRMWARE=1
-D ONEWIRE_CRC16=0
-D NO_GLOBAL_ARDUINOOTA
-D ARDUINOJSON_ENABLE_STD_STRING=1
-D CORS_ORIGIN=\"http://localhost:3000\"
build_unflags = -Wall
-Wdeprecated-declarations
esp32_build_unflags =
; -std=gnu++11
unbuild_flags =
${common.core_unbuild_flags}
; these are set in your pio_local.ini
debug_flags =
; -D EMSESP_DEBUG
; -D EMSESP_UART_DEBUG
; -D EMSESP_TEST
; -D ENABLE_CORS
[env]
framework = arduino
@@ -52,23 +39,21 @@ monitor_speed = 115200
upload_speed = 921600
build_type = release
lib_ldf_mode = chain+
; lib_compat_mode = strict
check_tool = cppcheck, clangtidy
check_severity = high, medium
check_flags =
cppcheck: --std=c++11 -v
clangtidy: --checks=-*,clang-analyzer-*,performance-*
cppcheck: --std=c++11 -v
clangtidy: --checks=-*,clang-analyzer-*,performance-*
; build for GitHub Actions CI
[env:ci]
extra_scripts =
scripts/rename_fw.py
extra_scripts = scripts/rename_fw.py
board = esp32dev
platform = espressif32
board_build.partitions = esp32_partition_app1984k_spiffs64k.csv
build_flags = ${common.build_flags} ${common.esp32_build_flags}
build_unflags = ${common.build_unflags} ${common.esp32_build_unflags}
build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags}
[env:esp32]
extra_scripts =
@@ -76,11 +61,11 @@ extra_scripts =
scripts/rename_fw.py
board = esp32dev
platform = espressif32
;platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#idf-release/v4.2
; ; toolchain-xtensa32 @ 2.80200.200226
; ; toolchain-xtensa32 @ 5.100200.201223
; toolchain-xtensa32 @ 2.80400.2020 ; c70ec8a-toolchain-xtensa32-linux_x86_64-2.80400.2020.tar.gz
; platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#idf-release/v4.2
; toolchain-xtensa32 @ 2.80200.200226
; toolchain-xtensa32 @ 5.100200.201223
; toolchain-xtensa32 @ 2.80400.2020
; platform = https://github.com/platformio/platform-espressif32.git
board_build.partitions = esp32_partition_app1984k_spiffs64k.csv ; https://github.com/espressif/arduino-esp32/blob/master/tools/partitions/
build_flags = ${common.build_flags} ${common.esp32_build_flags} ${common.debug_flags}
build_unflags = ${common.build_unflags} ${common.esp32_build_unflags}
board_build.partitions = esp32_partition_app1984k_spiffs64k.csv
build_flags = ${common.build_flags} ${common.debug_flags}
build_unflags = ${common.unbuild_flags}

View File

@@ -10,9 +10,9 @@ def upload(source, target, env):
platform = "esp" + env['PIOPLATFORM'].strip("espressif")
if platform == 'esp8266':
call(["cmd.exe", "/c", "C:\\Users\\Paul\\OneDrive\\Desktop\\com8266.bat"])
call(["cmd.exe", "/c", "C:\\Users\\Paul\\Desktop\\ems-esp8266.bat"])
if platform == 'esp32':
call(["cmd.exe", "/c", "C:\\Users\\Paul\\OneDrive\\Desktop\\com32.bat"])
call(["cmd.exe", "/c", "C:\\Users\\Paul\\Desktop\\ems-esp32.bat"])
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [upload])

View File

@@ -18,18 +18,16 @@
#include "emsesp.h"
using namespace std::placeholders; // for `_1` etc
namespace emsesp {
WebAPIService::WebAPIService(AsyncWebServer * server) {
server->on(EMSESP_API_SERVICE_PATH, HTTP_GET, std::bind(&WebAPIService::webAPIService, this, std::placeholders::_1));
server->on(EMSESP_API_SERVICE_PATH, HTTP_GET, std::bind(&WebAPIService::webAPIService, this, _1));
}
// e.g. http://ems-esp/api?device=boiler&cmd=wwtemp&data=20&id=1
void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
// see if the API is enabled
bool api_enabled;
EMSESP::webSettingsService.read([&](WebSettings & settings) { api_enabled = settings.api_enabled; });
// must have device and cmd parameters
if ((!request->hasParam(F_(device))) || (!request->hasParam(F_(cmd)))) {
request->send(400, "text/plain", F("Invalid syntax"));
@@ -47,12 +45,6 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
// get cmd, we know we have one
String cmd = request->getParam(F_(cmd))->value();
// look up command in our list
if (Command::find_command(device_type, cmd.c_str()) == nullptr) {
request->send(400, "text/plain", F("Invalid cmd"));
return;
}
String data;
if (request->hasParam(F_(data))) {
data = request->getParam(F_(data))->value();
@@ -68,15 +60,17 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
}
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject json = doc.to<JsonObject>();
bool ok = false;
JsonObject json = doc.to<JsonObject>();
bool ok = false;
// execute the command
if (data.isEmpty()) {
ok = Command::call(device_type, cmd.c_str(), nullptr, id.toInt(), json); // command only
} else {
// we only allow commands with parameters if the API is enabled
bool api_enabled;
EMSESP::webSettingsService.read([&](WebSettings & settings) { api_enabled = settings.api_enabled; });
if (api_enabled) {
// we only allow commands with parameters if the API is enabled
ok = Command::call(device_type, cmd.c_str(), data.c_str(), id.toInt(), json); // has cmd, data and id
} else {
request->send(401, "text/plain", F("Unauthorized"));
@@ -88,7 +82,7 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
doc.shrinkToFit();
std::string buffer;
serializeJsonPretty(doc, buffer);
request->send(200, "text/plain", buffer.c_str());
request->send(200, "text/plain;charset=utf-8", buffer.c_str());
return;
}
request->send(200, "text/plain", ok ? F("OK") : F("Invalid"));

View File

@@ -23,19 +23,18 @@ namespace emsesp {
using namespace std::placeholders; // for `_1` etc
WebDevicesService::WebDevicesService(AsyncWebServer * server, SecurityManager * securityManager)
: _device_dataHandler(DEVICE_DATA_SERVICE_PATH,
securityManager->wrapCallback(std::bind(&WebDevicesService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED)) {
server->on(EMSESP_DEVICES_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebDevicesService::all_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(SCAN_DEVICES_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebDevicesService::scan_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
: _device_dataHandler(DEVICE_DATA_SERVICE_PATH, securityManager->wrapCallback(std::bind(&WebDevicesService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED))
, _writevalue_dataHandler(WRITE_VALUE_SERVICE_PATH, securityManager->wrapCallback(std::bind(&WebDevicesService::write_value, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED)) {
server->on(EMSESP_DEVICES_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebDevicesService::all_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(SCAN_DEVICES_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebDevicesService::scan_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
_device_dataHandler.setMethod(HTTP_POST);
_device_dataHandler.setMaxContentLength(256);
server->addHandler(&_device_dataHandler);
_writevalue_dataHandler.setMethod(HTTP_POST);
_writevalue_dataHandler.setMaxContentLength(256);
server->addHandler(&_writevalue_dataHandler);
}
void WebDevicesService::scan_devices(AsyncWebServerRequest * request) {
@@ -101,4 +100,49 @@ void WebDevicesService::device_data(AsyncWebServerRequest * request, JsonVariant
request->send(response);
}
// takes a command and its data value from a specific Device, from the Web
void WebDevicesService::write_value(AsyncWebServerRequest * request, JsonVariant & json) {
// only issue commands if the API is enabled
EMSESP::webSettingsService.read([&](WebSettings & settings) {
if (!settings.api_enabled) {
request->send(403); // forbidden error
return;
}
});
if (json.is<JsonObject>()) {
JsonObject dv = json["devicevalue"];
// using the unique ID from the web find the real device type
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
if (emsdevice->unique_id() == dv["id"].as<int>()) {
const char * cmd = dv["cmd"];
uint8_t device_type = emsdevice->device_type();
bool ok = false;
char s[10];
// the data could be in any format, but we need string
JsonVariant data = dv["data"];
if (data.is<char *>()) {
ok = Command::call(device_type, cmd, data.as<const char *>());
} else if (data.is<int>()) {
ok = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int16_t>(), 0));
} else if (data.is<float>()) {
ok = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as<float>(), 1));
} else if (data.is<bool>()) {
ok = Command::call(device_type, cmd, data.as<bool>() ? "true" : "false");
}
if (ok) {
request->send(200);
}
return; // found device, quit
}
}
}
request->send(204); // no content error
}
}
} // namespace emsesp

Some files were not shown because too many files have changed in this diff Show More