260 Commits

Author SHA1 Message Date
proddy
7eb1f061b7 Merge remote-tracking branch 'origin/dev' for 3.2.0 release 2021-08-06 12:06:08 +02:00
proddy
5e4f5916f2 prep for 3.2.0 release 2021-08-06 12:01:42 +02:00
Proddy
1450737d94 make boiler id first in device registry, so it's first in MQTT payload 2021-08-03 13:28:04 +02:00
Proddy
bfd20e559e minor changes, make cppcheck happy 2021-08-03 13:23:33 +02:00
Proddy
98a7932dee optimizations 2021-08-03 13:23:09 +02:00
Proddy
19e26d0d64 don't show commands in web or console 2021-08-03 13:22:45 +02:00
Proddy
1715218864 update to b3 2021-08-03 13:22:13 +02:00
Proddy
e503c6cd79 remove msg moves 2021-08-03 13:22:02 +02:00
Proddy
9515e3d00b text changes 2021-08-03 13:21:43 +02:00
proddy
53e25ae213 lint warning changes 2021-08-01 23:44:40 +02:00
proddy
4863ecc329 add KB195i 2021-08-01 11:55:12 +02:00
proddy
b5892f5b5e text changes 2021-08-01 11:34:55 +02:00
proddy
d16502c872 auto-formatting 2021-08-01 11:34:48 +02:00
proddy
e29fb9ba8a updated 3.2.0b2 2021-07-29 16:55:36 +02:00
proddy
2ee0411582 text changes 2021-07-29 16:54:22 +02:00
proddy
f210466cb1 download buttons for settings 2021-07-29 16:54:08 +02:00
proddy
6af28b1c29 download button for log 2021-07-29 16:53:42 +02:00
proddy
049be2484e don't show dallas in system/info as its not an ems device 2021-07-29 16:53:06 +02:00
proddy
8f438e8045 rename add_json() 2021-07-29 16:52:33 +02:00
proddy
a8382dd6ce bump version 2021-07-29 16:52:06 +02:00
proddy
c55385d6d8 update mock data 2021-07-28 18:00:26 +02:00
proddy
87e6691433 remove comment 2021-07-28 18:00:14 +02:00
proddy
aa9ba65f70 comment changes 2021-07-28 10:40:17 +02:00
proddy
39fef48915 Update ArduinoJson to 6.18.3 2021-07-28 10:06:51 +02:00
proddy
362fead7e8 make standalone compile 2021-07-27 21:48:42 +02:00
proddy
e809ed3743 add max messages and make web log dynamic - #71 2021-07-27 21:44:12 +02:00
proddy
dc8c322b42 auto-formatting 2021-07-27 21:43:36 +02:00
proddy
c5688ab632 manually merge in official 0.9.0 version 2021-07-27 21:43:17 +02:00
proddy
d5d75eee63 remove comment 2021-07-27 21:42:37 +02:00
MichaelDvP
6ec16733c3 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-07-26 11:55:06 +02:00
proddy
ce2b2658ad mention wwactivated fix 2021-07-26 11:49:42 +02:00
proddy
3a866b1aea fix browser warning 2021-07-26 11:46:26 +02:00
MichaelDvP
1cf938e16a Syslog host setting as ipv4 or hostname 2021-07-26 11:41:23 +02:00
MichaelDvP
1df427366f syslog add hostname, rename host_ to ip_, query status 2021-07-26 11:06:23 +02:00
MichaelDvP
f97cdcb4d6 fix #89
Also Norbert have 0xFF on his list.
2021-07-25 11:52:42 +02:00
proddy
15c682cd1e don't show CRC in log (irrelevant to most people) 2021-07-24 15:02:58 +02:00
proddy
008983be26 comment update 2021-07-24 15:02:12 +02:00
proddy
2f01000665 minor debug text changed 2021-07-24 15:01:53 +02:00
proddy
616955daef use system gpio adc 2021-07-24 12:02:45 +02:00
proddy
e22b191a48 comments, fix typos, prep for v3.2 2021-07-24 11:56:39 +02:00
MichaelDvP
8930c52ada analog input use calibrated mV 2021-07-23 12:09:41 +02:00
MichaelDvP
6d94335079 Fix crash on unknown commands (cf==nullptr) 2021-07-22 19:35:27 +02:00
proddy
f2e0b193af Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-07-22 13:52:09 +02:00
proddy
5b55902cd9 sync up with HA MQTT Discovery when dallas sensor changed #84 2021-07-22 13:51:59 +02:00
MichaelDvP
694f647a2c Command returns as enum 2021-07-22 13:00:16 +02:00
MichaelDvP
fc1cb00523 reload values after write and update 2021-07-22 12:54:02 +02:00
proddy
2fda59d7db latest asyncmqttclient dev changes - to safeguard against stuck connections 2021-07-21 16:25:42 +02:00
proddy
a95837404a valiadtion in sensor fields, auto refresh after change #84 2021-07-21 16:00:39 +02:00
proddy
c6db2a1adf label change 2021-07-21 15:59:44 +02:00
proddy
f6d22732a0 formatting 2021-07-21 15:59:29 +02:00
MichaelDvP
220a69938f fix mqtt individual subscriptions 2021-07-21 09:37:46 +02:00
proddy
d438866864 updated with log changes 2021-07-20 23:37:56 +02:00
proddy
2e0ed9ce9f fix error where eventsource wasn't flushed correctly & included the log ID 2021-07-20 23:36:49 +02:00
proddy
5de3b69e2c refresh sync changed from 200 to 300ms 2021-07-20 23:36:16 +02:00
proddy
dfd6798377 added log id and changed format layout 2021-07-20 23:35:52 +02:00
proddy
074ae2a5a1 some refactoring 2021-07-20 21:45:59 +02:00
proddy
77f6a18075 commands take a set of flags, like NEED_ADMIN or HIDDEN 2021-07-20 21:45:29 +02:00
proddy
0762d9e124 bump to b6 2021-07-20 21:43:03 +02:00
proddy
37dae04715 clean up ts 2021-07-20 13:01:56 +02:00
proddy
f299a7ad14 update calls to dallas library 2021-07-20 13:01:45 +02:00
proddy
ba295385ab rename allDevices endpoint to data. improve dallas sensor form 2021-07-20 13:01:22 +02:00
proddy
33adf518ae update 2021-07-20 13:00:01 +02:00
proddy
93885d0dd5 update 2021-07-20 12:59:48 +02:00
proddy
747cda79db rename add_name() to update(). show offset in log msg 2021-07-20 12:59:37 +02:00
proddy
f3cfc38adc update regex for ipv6 #83 2021-07-19 19:21:03 +02:00
proddy
dd3a0a706d auto-formatting 2021-07-19 16:55:41 +02:00
proddy
37d001e7b5 formatting 2021-07-19 16:44:53 +02:00
proddy
add09e5a1c update mock api to work with latest changes 2021-07-19 16:44:46 +02:00
Proddy
561d1c0e55 fix typo 2021-07-18 21:48:18 +02:00
proddy
239ba335b1 changes to make it compile standalone 2021-07-18 21:44:24 +02:00
proddy
ec83123090 rename Makefile 2021-07-18 21:43:25 +02:00
MichaelDvP
4f6d5164a4 Set sensornames from web 2021-07-18 20:42:02 +02:00
MichaelDvP
ae1e2eccd2 minExtTemp for RC300 2021-07-18 20:40:51 +02:00
MichaelDvP
d7bc821bbe calculate dallassensor offset 2021-07-17 06:50:13 +02:00
MichaelDvP
f8579f7c96 formatting 2021-07-16 16:45:38 +02:00
MichaelDvP
046a9ef6f2 show dallas-temperatures always with digit on web 2021-07-16 16:26:58 +02:00
MichaelDvP
64e15542a2 show api type of commands as command instead of unknown 2021-07-16 16:25:20 +02:00
MichaelDvP
7dec674452 moving dallas-setting, cleanup 2021-07-16 16:24:03 +02:00
MichaelDvP
0f48d3e72c add sensorname console command 2021-07-16 10:57:43 +02:00
MichaelDvP
1f793c49ae Move dallas/bool/enum formats to Settings #76 2021-07-16 10:16:32 +02:00
MichaelDvP
e581539cf9 Fix #78 2021-07-15 11:09:59 +02:00
MichaelDvP
736eee79df fix change-check 2021-07-14 18:10:04 +02:00
MichaelDvP
7a0fe3819b reset Settingsflags after update 2021-07-14 17:02:40 +02:00
MichaelDvP
65c9bf7e52 check bufferlength, add formatstrings 2021-07-14 17:00:14 +02:00
MichaelDvP
1e61b5670e delay in emsesp-loop as mentioned in #78 2021-07-11 15:50:15 +02:00
MichaelDvP
6c2bae6296 remove SM10 collector min/max wrong values 2021-07-11 15:19:51 +02:00
MichaelDvP
358d6010b0 Web selectbox for enum commands (like bool) 2021-07-11 15:02:24 +02:00
MichaelDvP
82978a25c5 show wwtapactivated as value 2021-07-11 14:42:12 +02:00
MichaelDvP
3519696bae add boiler ww-hysteresis, maintenance settings 2021-07-11 14:34:18 +02:00
MichaelDvP
bd33df2cc7 Junkers mode/set_mode synchronized 2021-07-11 14:15:39 +02:00
MichaelDvP
5f44eb14ad allow numbers for set_mode 2021-07-11 14:01:24 +02:00
MichaelDvP
fb94cf953a no enum numbers for "hamode" 2021-07-11 13:52:16 +02:00
MichaelDvP
d7486218bc enum start at zero 2021-07-11 13:50:56 +02:00
MichaelDvP
59913cdc4b disable bluetooth, show IPv6 in web, mqtt and console 2021-07-08 18:56:24 +02:00
MichaelDvP
2d7449aeba add network options, IPv6 for mqtt 2021-07-08 10:17:50 +02:00
proddy
3ea53a8012 updated doc and version 2021-07-07 09:19:01 +02:00
MichaelDvP
48cd12ec3d NTP time for weblog 2021-07-06 07:35:39 +02:00
MichaelDvP
7c71ed2dc6 fix mem leak 2021-07-06 07:35:15 +02:00
proddy
24d8ccc52f fix crash with IPv6 and ETH - #83 2021-07-05 22:55:39 +02:00
proddy
05cd96f2be IPv6 support by MichaelDvP - #83 2021-07-05 22:43:08 +02:00
proddy
75795ab1e9 fix shower on/off when using non-default MQTT booleans 2021-07-05 21:44:38 +02:00
proddy
bb1602f179 typo 2021-07-05 21:44:12 +02:00
proddy
ac268f0f73 lower WiFi tx when using -DEMSESP_WIFI_TWEAK 2021-07-05 21:44:04 +02:00
Proddy
d924567e5f include debug target 2021-07-05 16:55:23 +02:00
Proddy
109d8df782 don't show mem updates on EMSESP_DEBUG 2021-07-05 16:54:41 +02:00
Proddy
0aaa35098d remove obsolete scripts 2021-07-05 16:54:15 +02:00
Proddy
6f57beab28 cleanup build 2021-07-05 16:53:57 +02:00
Proddy
5b26e27834 Update to 16.8.1 2021-07-05 16:53:27 +02:00
Proddy
8429f650aa fix lint warnings 2021-07-03 20:32:46 +02:00
proddy
3eb2202117 add test to crash EMS-ESP 2021-07-03 20:20:56 +02:00
proddy
c217a40710 remove CRC in warning message 2021-07-03 20:20:43 +02:00
proddy
4a7308c5bb add linux commands 2021-07-03 20:20:27 +02:00
proddy
5fba51103e cleanup 2021-07-03 20:20:07 +02:00
proddy
95a9808f35 add debug target 2021-07-03 20:19:57 +02:00
proddy
c09e180c48 add linters 2021-07-03 20:19:43 +02:00
proddy
b09b650c1d remove # from dallas in api/system/info 2021-07-03 10:44:15 +02:00
proddy
44a41b963d add upload_sec to api/system/info and removed # from some names to keep consistent with MQTT heartbeat - #80 2021-07-02 16:24:50 +02:00
proddy
0510189f54 slow down MQTT reconnect to 2 seconds 2021-07-01 11:19:00 +02:00
MichaelDvP
16b3cf764d don't count wrong tx-echos as rx-errors 2021-07-01 09:34:34 +02:00
MichaelDvP
c634c39874 3. fix for mqtt enable (#79) 2021-06-30 14:26:46 +02:00
MichaelDvP
ae0846e877 mqtt connect on enable-change (#79) 2021-06-30 13:49:52 +02:00
MichaelDvP
40e7e1b418 always register subscriptions (#79) 2021-06-30 13:13:18 +02:00
proddy
e2a5853dde rename WIFI in System settings to Network 2021-06-29 22:57:21 +02:00
MichaelDvP
e419e67cb0 fix memory leak (#78) 2021-06-29 09:37:05 +02:00
proddy
3356a4ce14 fix v16 for nodejs 2021-06-26 11:14:45 +02:00
proddy
50459a23fe force v16 of nodejs 2021-06-26 11:13:07 +02:00
proddy
26a4347155 prep for 3.1.2 2021-06-26 11:05:04 +02:00
proddy
5bf53c3389 3.1.1 2021-06-26 11:03:03 +02:00
proddy
4b7aa95be3 Merge remote-tracking branch 'origin/dev' 2021-06-26 11:02:55 +02:00
proddy
b5921d15ac show TRACE messages correctly 2021-06-26 10:28:44 +02:00
proddy
82a4f1499a added more screenshots 2021-06-26 10:28:30 +02:00
Proddy
e99c9208ad Update pre_release.yml
on demand
2021-06-19 14:12:32 +02:00
proddy
65dae7af42 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-06-19 14:04:17 +02:00
proddy
050c75944a fixes #76 2021-06-19 14:04:09 +02:00
Proddy
2d96aa1736 Update pre_release.yml
typo
2021-06-19 13:32:46 +02:00
proddy
85161ec09a node to v16 2021-06-19 13:32:14 +02:00
proddy
967eee67c4 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-06-19 13:32:07 +02:00
proddy
469f78a329 update node to v16 2021-06-19 13:30:40 +02:00
proddy
1e4eb52c90 auto formatting 2021-06-19 13:30:32 +02:00
Proddy
fdbbfe8ddb Update pre_release.yml
node to v16
2021-06-19 13:27:47 +02:00
proddy
e79d4603fc replace TRACE with ALL 2021-06-18 21:42:16 +02:00
proddy
9ef2e62955 update for 3.1.1b7 2021-06-18 20:40:47 +02:00
proddy
c234503a9c Merge remote-tracking branch 'origin/ft_webui_log' into dev 2021-06-18 20:38:28 +02:00
proddy
2056d3ff19 UI updates #71 2021-06-18 17:08:57 +02:00
proddy
270298eb8a text changes 2021-06-17 11:40:19 +02:00
proddy
7e7bd29c9a show # messages, use msgpack to compress json 2021-06-16 20:31:35 +02:00
proddy
19b37d9e0e Show realtime debug log in WebUI #71 2021-06-16 14:54:36 +02:00
proddy
fc2bcd50ca log to webui - initial version 2021-06-14 21:28:20 +02:00
MichaelDvP
37da9d3755 fix #73 RC300 summersetmode 2021-06-14 16:47:44 +02:00
proddy
fffed3b411 comment 2021-06-12 12:41:12 +02:00
proddy
9738c0848d update package 2021-06-12 12:41:06 +02:00
proddy
fc11db03f0 make 'call system' commands work again 2021-06-10 23:11:05 +02:00
proddy
17a28d246d remove old code 2021-06-10 23:10:48 +02:00
proddy
3143ed1060 fix comment 2021-06-10 23:10:40 +02:00
proddy
50540f1f82 add API to test suite 2021-06-10 23:10:26 +02:00
proddy
fad1b09e19 bump to 3.1.1b6 2021-06-08 18:25:00 +02:00
proddy
a1912405c7 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-06-08 18:24:16 +02:00
proddy
cc30e09e4b TAG_DEVICE_DATA_WW is only for BOILER 2021-06-08 18:24:06 +02:00
proddy
07a943eedf rename warm water to ww 2021-06-08 18:23:54 +02:00
proddy
adf4584717 remove TAG_DEVICE_DATA_WW 2021-06-08 18:23:37 +02:00
proddy
b3d647850d add ww to those ww values 2021-06-08 18:22:58 +02:00
proddy
ee6a09c9df do not default to info if no command specificied 2021-06-08 18:22:18 +02:00
proddy
a84ae9e7cc fix console commands 2021-06-08 18:21:53 +02:00
MichaelDvP
40206a27ac fix '0 hours' display 2021-06-08 16:07:20 +02:00
MichaelDvP
5c282b7a7e fix charging type 2021-06-08 16:04:41 +02:00
proddy
8dd18aa24d Render values in Web natively #70 2021-06-07 21:19:52 +02:00
proddy
db43f2d711 remove debug log to console 2021-06-07 21:18:48 +02:00
proddy
e9741ea4f8 improve layout, booleans show as a menu 2021-06-07 21:18:29 +02:00
proddy
cf416ee080 fix spelling 2021-06-07 21:17:54 +02:00
proddy
af41f352ba change spelling 2021-06-07 21:17:39 +02:00
proddy
feed65bea6 don't create HA sensor for wifi rssi if only ethernet 2021-06-07 21:15:46 +02:00
proddy
66f14fff82 fix prettier on win10 2021-05-17 09:50:48 +02:00
Proddy
70943f5758 Update pre_release.yml 2021-05-16 15:52:09 +02:00
Proddy
3bc280b817 Delete check_code.yml 2021-05-16 15:51:56 +02:00
proddy
9e432efcd1 remove code check 2021-05-16 15:51:44 +02:00
proddy
8417c715c1 remove local make 2021-05-16 15:50:10 +02:00
proddy
ddb3633fdb test build 2021-05-16 15:41:49 +02:00
Proddy
62b15a5319 Update pre_release.yml 2021-05-16 15:35:06 +02:00
Proddy
8dd18802d6 Update tagged_release.yml 2021-05-16 15:34:45 +02:00
proddy
8530520a62 updated changelog 2021-05-16 15:33:24 +02:00
proddy
b077d867ba fixes prefix ww to the HA entity names #67 2021-05-16 15:27:18 +02:00
proddy
8c48639572 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-05-16 13:49:16 +02:00
proddy
4a6ca636e5 add product id to HA, and top of entity list 2021-05-16 13:49:05 +02:00
proddy
4bce819bd3 fix Incorrect Thermostat mode mapping in Home Assistant (RC310) #66 2021-05-16 13:48:48 +02:00
proddy
de63d10e3d add default HA icon for each device 2021-05-16 13:48:23 +02:00
proddy
c62183f886 comment change 2021-05-16 13:47:55 +02:00
proddy
6a73ee4a0b more HA icons 2021-05-16 12:43:33 +02:00
proddy
964db8e7d7 more HA icons 2021-05-16 12:43:24 +02:00
proddy
5b66528c0b rssi shows real rssi and wifistrength shows % in heartbeat 2021-05-16 12:43:07 +02:00
proddy
01fd90f3ed added more HA icons 2021-05-16 12:42:36 +02:00
MichaelDvP
5e7bed1063 fix #68, add hc as prefix to web cmd 2021-05-15 18:27:09 +02:00
proddy
4d69846932 eslint 2021-05-14 12:46:06 +02:00
proddy
fec5ff3132 eslint 2021-05-14 12:45:57 +02:00
proddy
15df0c0552 added lint & standalone as targets 2021-05-14 12:45:49 +02:00
proddy
42a362196e updated doc 2021-05-14 12:45:19 +02:00
proddy
ab28013ec6 removed EMSESP_PLATFORM 2021-05-14 12:45:00 +02:00
proddy
75f3a6f82a added eslint to prettier 2021-05-14 12:44:40 +02:00
proddy
e467e73755 updated tabs and comma 2021-05-14 12:44:18 +02:00
proddy
505e846dd8 added 2021-05-14 12:44:02 +02:00
proddy
7808959d67 added log.h 2021-05-14 12:43:44 +02:00
proddy
1ecee740d3 fix: move showing version to settings. fixes JWT resetting after each version change 2021-05-14 12:43:11 +02:00
proddy
47eaeba373 remove ESP32 2021-05-14 12:41:04 +02:00
proddy
e7dbccabec bump version 2021-05-14 12:40:49 +02:00
MichaelDvP
6ff3d243bd add missing names 2021-05-11 11:03:40 +02:00
MichaelDvP
4027003729 add SM100 values and commands 2021-05-11 08:51:26 +02:00
MichaelDvP
70fd0ad658 fix empty commands '<device>/hcx' 2021-05-11 08:50:32 +02:00
Proddy
94127ad3eb Merge pull request #58 from MichaelDvP/dev_cmd
Command checks and prefixes
2021-05-10 18:26:58 +02:00
MichaelDvP
44734713f1 Merge pull request #57 from MichaelDvP/dev
Solar SM10 module
2021-05-10 17:32:55 +02:00
MichaelDvP
2f0f45f3ec fix build error with debug_flag 2021-05-10 16:16:34 +02:00
MichaelDvP
8641e9d9cb Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2021-05-10 15:48:41 +02:00
MichaelDvP
bc78dd3f50 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev_cmd 2021-05-10 15:40:14 +02:00
MichaelDvP
a9fca73f2d command default id=-1, command_check, add "warm water" to long names 2021-05-10 15:34:11 +02:00
MichaelDvP
5cccfacbc4 show mqtt-count and sensor-reads in info 2021-05-10 15:28:53 +02:00
proddy
7425d0e095 updated with latest changes 2021-05-10 14:13:41 +02:00
MichaelDvP
28068bdb98 add SM10 solar values and commands 2021-05-10 14:13:37 +02:00
proddy
bc69ca0a9b show freemem in KB, add more HA icons 2021-05-10 14:10:16 +02:00
MichaelDvP
fd9ac28254 do not publish empty messages, free mem 2021-05-10 13:50:18 +02:00
MichaelDvP
312fd85469 prevent multiple reading of long messages with no offset 2021-05-10 13:40:11 +02:00
proddy
57a516a83a updated README and images 2021-05-09 15:13:16 +02:00
proddy
37bee39cea remove 2021-05-09 15:10:55 +02:00
proddy
ed843ba58d updated images 2021-05-09 15:06:57 +02:00
proddy
f8c7da6e0c remove ESP8266 AsyncTCP lib 2021-05-09 15:03:36 +02:00
proddy
e0b1ff1353 add hosted ems-esp.derbyshire.nl 2021-05-09 13:52:46 +02:00
proddy
0f78df517f fix formatting 2021-05-09 11:01:13 +02:00
proddy
e7dae28922 bump version 2021-05-09 11:00:58 +02:00
proddy
dbd3c04d1b remove comment 2021-05-09 10:59:44 +02:00
proddy
e19566ecb8 comment change 2021-05-09 10:59:28 +02:00
proddy
370af11200 include description in the command 2021-05-09 10:59:16 +02:00
proddy
0e67e8311e show commands has a verbose mode 2021-05-09 10:59:03 +02:00
proddy
81f4724d71 added new command called commands 2021-05-09 10:58:44 +02:00
proddy
4a06d328d6 minor comment change 2021-05-09 10:57:48 +02:00
proddy
4dab735dad moved to wiki doc 2021-05-09 10:57:32 +02:00
proddy
ce2fa15554 remove TODO 2021-05-09 10:57:17 +02:00
proddy
c0cb121660 API defauls to hidden command info_short 2021-05-08 18:42:23 +02:00
proddy
efac66835a hidden command info_short 2021-05-08 18:42:04 +02:00
proddy
4a269fd508 added hidden commands, sort commands 2021-05-08 18:41:46 +02:00
proddy
bcef360252 hide ssid from system settings 2021-05-08 16:01:35 +02:00
proddy
bb262ed0df so 'serve' command works for local builds 2021-05-08 15:36:56 +02:00
proddy
039d60abfb use new API path 2021-05-08 15:33:17 +02:00
proddy
c6a40d2125 auto formatting 2021-05-07 10:15:29 +02:00
proddy
d15aa79d18 add prettier to tidy up typescript 2021-05-07 10:15:19 +02:00
proddy
c0d5bd1f05 add examples 2021-05-06 10:58:05 +02:00
proddy
54c2a73d68 auto formatting 2021-05-06 10:17:33 +02:00
proddy
461aa1fd58 updated text about bearer token 2021-05-06 10:17:25 +02:00
proddy
9211d29e17 upgrade to 6.18 2021-05-06 10:10:37 +02:00
proddy
3c1b30a5e4 auto formatting 2021-05-06 10:10:20 +02:00
MichaelDvP
7f52ef8bd8 info shows shortnames, only valid hcs 2021-05-05 18:48:07 +02:00
MichaelDvP
32f477726b allow not only commands in api 2021-05-05 08:04:06 +02:00
MichaelDvP
e9068e702e fix mixer HA-id nested 2021-05-05 08:03:13 +02:00
proddy
e97f6c09e5 Merge remote-tracking branch 'origin/ft_https' into dev 2021-05-04 12:41:47 +02:00
proddy
f0e7ede499 merge with latest dev 2021-05-02 22:17:16 +02:00
proddy
a595bde1b8 Merge remote-tracking branch 'origin/dev' into ft_https 2021-05-02 22:17:06 +02:00
proddy
e113ebd298 fetch command also per device type 2021-05-02 22:09:09 +02:00
proddy
5339e0876e feat: Adopt the OpenAPI 3.0 standard for the REST API #50 2021-05-02 22:08:53 +02:00
proddy
101978f713 include fullname in info command 2021-05-02 22:07:49 +02:00
proddy
23218bca7d some minor formatting 2021-05-02 22:07:22 +02:00
proddy
7d0ed2246a auto formatting 2021-05-02 08:32:52 +02:00
proddy
c43fe4f9ae add doc on API spec 2021-05-01 13:19:57 +02:00
proddy
4a4e5f1890 Merge branch 'dev' into ft_https 2021-04-27 14:41:52 +02:00
proddy
6c41c49866 remove asyncjson.h 2021-04-06 11:21:01 +02:00
413 changed files with 13540 additions and 12479 deletions

View File

@@ -2,7 +2,7 @@ Language: Cpp
BasedOnStyle: LLVM
UseTab: Never
IndentWidth: 4
ColumnLimit: 220
ColumnLimit: 160
TabWidth: 4
#BreakBeforeBraces: Custom
BraceWrapping:

View File

@@ -1,37 +0,0 @@
name: Code Check
on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['cpp']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- run: |
make
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,6 +1,7 @@
name: "pre-release"
on:
workflow_dispatch:
push:
branches:
- "dev"
@@ -13,46 +14,41 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v2
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Get build variables
- name: Get EMS-ESP source code and version
id: build_info
run: |
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
echo "::set-output name=version::$version"
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
echo "::set-output name=platform::$platform"
- name: Compile locally
run: make
- name: Setup Python
uses: actions/setup-python@v2
- name: Install pio
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install -U platformio
platformio upgrade
platformio update
- name: Build web
- name: Build WebUI
run: |
cd interface
npm install
npm ci
npm run build
- name: Build firmware
run: |
platformio run -e ci
- name: Release
- name: Create a GH Release
id: "automatic_releases"
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
title: ${{steps.build_info.outputs.platform}} Development Build v${{steps.build_info.outputs.version}}
title: ESP32 Development Build v${{steps.build_info.outputs.version}}
automatic_release_tag: "latest"
prerelease: true
files: |

View File

@@ -12,34 +12,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Get build variables
id: build_info
run: |
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
echo "::set-output name=version::$version"
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
echo "::set-output name=platform::$platform"
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Compile locally
run: make
- name: Setup Python
uses: actions/setup-python@v2
- name: Install pio
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install -U platformio
platformio upgrade
platformio update
- name: Build web
- name: Build WebUI
run: |
cd interface
npm install
npm ci
npm run build
- name: Build firmware

2
.gitignore vendored
View File

@@ -27,4 +27,4 @@ emsesp
/interface/build
node_modules
/interface/.eslintcache
test.sh

View File

@@ -5,7 +5,67 @@ 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
# [3.2.0] August 6 2021
## Added
- support for IPv6 (web/api/mqtt, not syslog yet) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
- System Log in Web UI will show current time if the NTP Service is enabled [#82](https://github.com/emsesp/EMS-ESP32/issues/82)
- Network settings for Tx-power, WiFi-bandwidth, WiFi-sleepmode [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
- optional low CPU clockrate (160 MHz) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
- select format for enumerated values in web
- settings for water hysteresis on/off
- dallas sensor name editable. `sensorname` console-command, replace sensorid with a unique name [#84](https://github.com/emsesp/EMS-ESP32/issues/84)
- 'restart' system command. Can be invoked via API with authentication. [#87](https://github.com/emsesp/EMS-ESP32/issues/87)
- add Download button in Web UI for log
## Fixed
- set mode allow numbers
- Junkers thermostat shows mode as selected by set_mode
- HA thermostat mode if bool-format: numbers is selected
- Web UI System Log sometimes skipped a few log messages when watching real-time
- fix wwactivated [#89](https://github.com/emsesp/EMS-ESP32/issues/89)
- don't show commands (like reset) as Device values in the Web or Console
## Changed
- removed Rx echo failures counting as incomplete telegrams. Bad telegrams show as Warning and not Errors. [#80](https://github.com/emsesp/EMS-ESP32/issues/80)
- add upload_sec to `api/system/info` and removed # from some names to keep consistent with MQTT heartbeat
- added debug target to PlatformIO build to help hunt down system crashes
- enumerated values always start at zero
- maintenance settings for time/date as extra setting
- move api/mqtt formats to `settings`, add `enum format`
- UI improvements for editing Dallas Sensor details
- RESTful GET commands can also require authentication (via bearer access token) for better security
- Updated AsyncMqttClient to 0.9.0 and ArduinoJson to 6.18.3
- Download buttons for settings and info under the Help tab
# [3.1.1] June 26 2021
## Changed
- new command called `commands` which lists all available commands. `ems-esp/api/{device}/commands`
- More Home Assistant icons to match the UOMs
- new API. Using secure access tokens and OpenAPI standard. See `doc/EMS-ESP32 API.md` and [#50](https://github.com/emsesp/EMS-ESP32/issues/50)
- show log messages in Web UI [#71](https://github.com/emsesp/EMS-ESP32/issues/71)
## Fixed
- HA thermostat mode was not in sync with actual mode [#66](https://github.com/emsesp/EMS-ESP32/issues/66)
- Don't publish rssi if Wifi is disabled and ethernet is being used
- Booleans are shown as true/false in API GETs
## Changed
- `info` command always shows full names in API. For short names query the device or name directly, e.g. `http://ems-esp/api/boiler`
- free memory is shown in kilobytes
- boiler's warm water entities have ww added to the Home Assistant entity name [#67](https://github.com/emsesp/EMS-ESP32/issues/67)
- improved layout and rendering of device values in the WebUI, also the edit value screen
# [3.1.0] May 4 2021
## Changed
- Mock API to simulate an ESP, for testing web
- Able to write values from the Web UI
@@ -27,9 +87,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- lowercased Flow temp in commands
- system console commands to main
## Removed
## [3.0.1] March 30 2021
# [3.0.1] March 30 2021
## Added
@@ -87,9 +145,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Shower Alert (disabled for now)
## [3.0.0] March 18 2021
# [3.0.0] March 18 2021
### Added
## Added
- Power settings, disabling BLE and turning off Wifi sleep
- Rx and Tx counts to Heartbeat MQTT payload
@@ -106,7 +164,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
- Syslog port selectable (#744)
### Fixed
## Fixed
- telegrams matched to masterthermostat 0x18
- multiple roomcontrollers
@@ -117,7 +175,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- wrong position of values #723, #732
- OTA Upload via Web on OSX
### Changed
## Changed
- changed how telegram parameters are rendered for mqtt, console and web (#632)
- split `show values` in smaller packages (edited)

View File

@@ -5,5 +5,3 @@
## Fixed
## Changed
## Removed

View File

@@ -32,8 +32,8 @@ This document describes rules that are in effect for this repository, meant for
6. Issues with feature requests should be discussed for viability/desirability.
7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified.
8. Feature requests that are not accompanied by a PR:
* could be closed immediately (denied).
* could be closed after some predetermined period of time (left as candidate for somebody to pick up).
- could be closed immediately (denied).
- could be closed after some predetermined period of time (left as candidate for somebody to pick up).
9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot.
## Pull requests
@@ -42,24 +42,24 @@ A Pull Request (PR) is the process where code modifications are managed in GitHu
The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
1. All pull requests must be done against the dev branch.
2. Make sure code is formatting per the `.clang-format`
3. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
4. Only one feature/fix should be added per PR.
5. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
6. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
7. All pull requests should consider updates to the documentation.
8. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
9. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
10. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
11. Pull requests that don't meet the above will be denied and closed.
2. Make sure code is formatting per the `.clang-format`.
3. Make sure any new code is clearly commented explaining what the function/logic does.
4. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
5. Only one feature/fix should be added per PR.
6. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
7. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
8. All pull requests should consider updates to the documentation.
9. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
10. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
11. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
12. Pull requests that don't meet the above will be denied and closed.
## Semantic Commit Messages
@@ -92,7 +92,7 @@ More Examples:
References:
- https://www.conventionalcommits.org/
- <https://www.conventionalcommits.org/>
--------------------------------------

View File

@@ -17,8 +17,8 @@ MAKEFLAGS+="j "
#TARGET := $(notdir $(CURDIR))
TARGET := emsesp
BUILD := build
SOURCES := src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton src/test
INCLUDES := lib/ArduinoJson/src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/PButton src/devices lib src
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton
INCLUDES := src lib_standalone lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/* src/devices
LIBRARIES :=
CPPCHECK = cppcheck
@@ -72,9 +72,9 @@ CPPFLAGS += -g3
CPPFLAGS += -Os
CFLAGS += $(CPPFLAGS)
# CFLAGS += -Wall
# CFLAGS += -Wno-unused -Wno-restrict
# CFLAGS += -Wextra
CFLAGS += -Wall
CFLAGS += -Wno-unused -Wno-restrict
CFLAGS += -Wextra
CXXFLAGS += $(CFLAGS) -MMD

117
README.md
View File

@@ -2,22 +2,21 @@
**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. 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
This project is the specifically for the ESP32. Compared with the previous ESP8266 (version 2) release it has the following enhancements:
- Ethernet Support
- Pre-configured circuit board layouts
- Supports writing EMS values directly from within Web UI
- Mock API server for faster offline development and testing
- Improved API and MQTT commands
- Improvements to Dallas temperature sensors
- Embedded log tracing in the Web UI
[![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)
[![license](https://img.shields.io/github/license/emsesp/EMS-ESP32.svg)](LICENSE)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9441142f49424ef891e8f5251866ee6b)](https://www.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=emsesp/EMS-ESP32&amp;utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9441142f49424ef891e8f5251866ee6b)](https://www.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=github.com&utm_medium=referral&utm_content=emsesp/EMS-ESP32&utm_campaign=Badge_Grade)
[![downloads](https://img.shields.io/github/downloads/emsesp/EMS-ESP32/total.svg)](https://github.com/emsesp/EMS-ESP32/releases)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/emsesp/EMS-ESP32.svg)](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Average time to resolve an issue")
[![Percentage of issues still open](http://isitmaintained.com/badge/open/emsesp/EMS-ESP32.svg)](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Percentage of issues still open")
<br/>
[![chat](https://img.shields.io/discord/816637840644505620.svg?style=flat-square&color=blueviolet)](https://discord.gg/3J3GgnzpyT)
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
@@ -26,82 +25,104 @@ If you like **EMS-ESP**, please give it a star, or fork it and contribute!
[![GitHub forks](https://img.shields.io/github/forks/emsesp/EMS-ESP32.svg?style=social&label=Fork)](https://github.com/emsesp/EMS-ES32P/network)
[![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/paypalme/prderbyshire/2)
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at https://bbqkees-electronics.nl.
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at <https://bbqkees-electronics.nl> or contact the contributors that can provide the schematic and designs.
<img src="media/gateway-integration.jpg" width=40%>
---
## **Features**
# **Features**
- Compatible with both ESP8266 and ESP32
- A multi-user secure web interface to change settings and monitor the data
- A console, accessible via Serial and Telnet for more monitoring
- Native support for Home Assistant via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
- Easy first-time configuration via a web Captive Portal
- Support for more than [70 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
- Support for more than [80 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
## **Screenshots**
## **Demo**
### Web Interface:
See a live demo [here](https://ems-esp.derbyshire.nl) using fake data. Log in with any username/password.
# **Screenshots**
## Web Interface
| | |
| --- | --- |
| <img src="media/web_settings.PNG"> | <img src="media/web_status.PNG"> |
| <img src="media/web_devices.PNG"> | <img src="media/web_mqtt.PNG"> |
| ---------------------------------- | -------------------------------- |
| <img src="media/web_settings.png"> | <img src="media/web_status.png"> |
| <img src="media/web_devices.png"> | <img src="media/web_mqtt.png"> |
| <img src="media/web_edit.png"> | <img src="media/web_log.png"> |
### Telnet Console:
<img src="media/console.PNG" width=80% height=80%>
## Telnet Console
### In Home Assistant:
<img src="media/ha_lovelace.PNG" width=80% height=80%>
<img src="media/console.png" width=80% height=80%>
## **Installing**
## In Home Assistant
<img src="media/ha_lovelace.png" width=80% height=80%>
# **Installing**
Refer to the [official documentation](https://emsesp.github.io/docs) to how to install the firmware and configure it. The documentation is being constantly updated as new features and settings are added.
You can choose to use an pre-built firmware image or compile the code yourself:
* [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
* [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
- [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
- [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
## **Support Information**
# **Support Information**
If you're looking for support on **EMS-ESP** there are some options available:
### Documentation
## Documentation
* [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
* [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
- [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
- [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
### Support Community
## Support Community
* [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
* [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
- [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
- [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
### Developer's Community
## Developer's Community
* [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
* [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
* [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new *Troubleshooting & Question* issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
- [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
- [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
- [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new _Troubleshooting & Question_ issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
## **Contributing**
# **Contributors ✨**
EMS-ESP is a project originally created and owned by [proddy](https://github.com/proddy). Key contributors are:
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center">
<a href="https://github.com/MichaelDvP"><img src="https://avatars.githubusercontent.com/u/59284019?v=3?s=100" width="100px;" alt=""/><br /><sub><b>MichaelDvP</b></sub></a><br /></a> <a href="https://github.com/emsesp/EMS-ESP/commits?author=MichaelDvP" title="v2 Commits">v2</a>
<a href="https://github.com/emsesp/EMS-ESP32/commits?author=MichaelDvP" title="v3 Commits">v3</a>
</td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
You can also contribute to EMS-ESP by
You can contribute to EMS-ESP by
- providing Pull Requests (Features, Fixes, suggestions)
- testing new released features and report issues on your EMS equipment
- contributing to missing [Documentation](https://emsesp.github.io/docs)
## **Credits**
# **Libraries used**
A shout out to the people helping EMS-ESP get to where it is today...
- **@MichaelDvP** for all his amazing contributions and patience. Specifically for the improved uart library, thermostat and mixer logic.
- **@BBQKees** for his endless testing and building the awesome circuit boards
- **@rjwats** for his [esp8266-react](https://github.com/rjwats/esp8266-react) framework that provides the new Web UI
- **@nomis** for his core [console](https://github.com/nomis/mcu-uuid-console), telnet and syslog core libraries
- plus everyone else providing suggestions, PRs and the odd donation that keeps this project open source. Thanks!
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for JSON
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
## **License**
# **License**
This program is licensed under GPL-3.0

View File

@@ -1,6 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x10000, 0x1F0000,
app1, app, ota_1, 0x200000, 0x1F0000,
spiffs, data, spiffs, 0x3F0000,0x10000,
spiffs, data, spiffs, 0x3F0000, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0xE000 0x2000
4 app0 app ota_0 0x10000 0x1F0000
5 app1 app ota_1 0x200000 0x1F0000
6 spiffs data spiffs 0x3F0000 0x10000

View File

@@ -0,0 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x10000, 0x210000,
spiffs, data, spiffs, 0x220000, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xE000 0x2000
4 app0 app ota_0 0x10000 0x210000
5 spiffs data spiffs 0x220000 0x10000

3
interface/.env.hosted Normal file
View File

@@ -0,0 +1,3 @@
GENERATE_SOURCEMAP=false
REACT_APP_HOSTED=true

2
interface/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
# don't ever lint node_modules
node_modules

27
interface/.eslintrc Normal file
View File

@@ -0,0 +1,27 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
// 0 = ignore, 1 = warning, 2 = error
"no-console": 0,
"prettier/prettier": ["error", { endOfLine: "auto" }],
"explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-non-null-asserted-optional-chain": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-explicit-any": 0
}
}

6
interface/.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "none",
"printWidth": 80
}

View File

@@ -4,34 +4,49 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const ProgmemGenerator = require('./progmem-generator.js');
const path = require('path');
const fs = require('fs');
module.exports = function override(config, env) {
if (env === "production") {
const hosted = process.env.REACT_APP_HOSTED;
if (env === 'production' && !hosted) {
console.log('Custom webpack...');
// 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';
// take out the manifest and service worker plugins
config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin));
config.plugins = config.plugins.filter(plugin => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW));
config.plugins = config.plugins.filter(
(plugin) => !(plugin instanceof ManifestPlugin)
);
config.plugins = config.plugins.filter(
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW)
);
// shorten css filenames
const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin);
miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css";
miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css";
const miniCssExtractPlugin = config.plugins.find(
(plugin) => plugin instanceof MiniCssExtractPlugin
);
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
miniCssExtractPlugin.options.chunkFilename =
'css/[id].[contenthash:4].c.css';
// build progmem data files
config.plugins.push(new ProgmemGenerator({ outputPath: "../lib/framework/WWWData.h", bytesPerLine: 20 }));
config.plugins.push(
new ProgmemGenerator({
outputPath: '../lib/framework/WWWData.h',
bytesPerLine: 20
})
);
// add compression plugin, compress javascript
config.plugins.push(new CompressionPlugin({
filename: "[path].gz[query]",
algorithm: "gzip",
config.plugins.push(
new CompressionPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js)$/,
deleteOriginalAssets: true
}));
})
);
}
return config;
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
{
"name": "esp8266-react",
"name": "emsesp-react",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@msgpack/msgpack": "^2.7.0",
"@types/lodash": "^4.14.168",
"@types/node": "^15.0.1",
"@types/react": "^17.0.4",
@@ -13,6 +14,7 @@
"@types/react-router": "^5.1.13",
"@types/react-router-dom": "^5.1.7",
"compression-webpack-plugin": "^5.0.2",
"env-cmd": "^10.1.0",
"express": "^4.17.1",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
@@ -34,9 +36,12 @@
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"eject": "react-scripts eject",
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
"build-hosted": "env-cmd -f .env.hosted npm run build",
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
"dev": "run-p start mock-api"
"standalone": "npm-run-all -p start mock-api",
"lint": "eslint . --ext .ts,.tsx"
},
"eslintConfig": {
"extends": "react-app"
@@ -55,9 +60,12 @@
},
"devDependencies": {
"concurrently": "^6.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"http-proxy-middleware": "^1.1.1",
"nodemon": "^2.0.7",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"react-app-rewired": "^2.1.8"
}
}

View File

@@ -1,19 +1,25 @@
const { resolve, relative, sep } = require('path');
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
const {
readdirSync,
existsSync,
unlinkSync,
readFileSync,
createWriteStream
} = require('fs');
var zlib = require('zlib');
var mime = require('mime-types');
const ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
function getFilesSync(dir, files = []) {
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
const entryPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
getFilesSync(entryPath, files);
} else {
files.push(entryPath);
}
})
});
return files;
}
@@ -25,13 +31,17 @@ function cleanAndOpen(path) {
if (existsSync(path)) {
unlinkSync(path);
}
return createWriteStream(path, { flags: "w+" });
return createWriteStream(path, { flags: 'w+' });
}
class ProgmemGenerator {
constructor(options = {}) {
const { outputPath, bytesPerLine = 20, indent = " ", includes = ARDUINO_INCLUDES } = options;
const {
outputPath,
bytesPerLine = 20,
indent = ' ',
includes = ARDUINO_INCLUDES
} = options;
this.options = { outputPath, bytesPerLine, indent, includes };
}
@@ -41,30 +51,34 @@ class ProgmemGenerator {
(compilation, callback) => {
const { outputPath, bytesPerLine, indent, includes } = this.options;
const fileInfo = [];
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
const writeStream = cleanAndOpen(
resolve(compilation.options.context, outputPath)
);
try {
const writeIncludes = () => {
writeStream.write(includes);
}
};
const writeFile = (relativeFilePath, buffer) => {
const variable = "ESP_REACT_DATA_" + fileInfo.length;
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
const mimeType = mime.lookup(relativeFilePath);
var size = 0;
writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
const zipBuffer = zlib.gzipSync(buffer);
zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) {
writeStream.write("\n");
writeStream.write('\n');
writeStream.write(indent);
}
writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
writeStream.write(
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ','
);
size++;
});
if (size % bytesPerLine) {
writeStream.write("\n");
writeStream.write('\n');
}
writeStream.write("};\n\n");
writeStream.write('};\n\n');
fileInfo.push({
uri: '/' + relativeFilePath.replace(sep, '/'),
mimeType,
@@ -84,25 +98,37 @@ class ProgmemGenerator {
// process assets
const { assets } = compilation;
Object.keys(assets).forEach((relativeFilePath) => {
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
writeFile(
relativeFilePath,
coherseToBuffer(assets[relativeFilePath].source())
);
});
}
};
const generateWWWClass = () => {
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
class WWWData {
${indent}public:
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo.map(file => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`).join('\n')}
${indent.repeat(
2
)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo
.map(
(file) =>
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${
file.variable
}, ${file.size});`
)
.join('\n')}
${indent.repeat(2)}}
};
`;
}
};
const writeWWWClass = () => {
writeStream.write(generateWWWClass());
}
};
writeIncludes();
writeFiles();

View File

@@ -1,12 +1,12 @@
{
"name":"EMS-ESP",
"icons":[
"name": "EMS-ESP",
"icons": [
{
"src":"/app/icon.png",
"sizes":"48x48 72x72 96x96 128x128 256x256"
"src": "/app/icon.png",
"sizes": "48x48 72x72 96x96 128x128 256x256"
}
],
"start_url":"/",
"display":"fullscreen",
"orientation":"any"
"start_url": "/",
"display": "fullscreen",
"orientation": "any"
}

View File

@@ -3,20 +3,26 @@
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/li.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
src: local('Roboto Light'), local('Roboto-Light'),
url(../fonts/li.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
src: local('Roboto'), local('Roboto-Regular'),
url(../fonts/re.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/me.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
src: local('Roboto Medium'), local('Roboto-Medium'),
url(../fonts/me.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
}

View File

@@ -14,7 +14,6 @@ import FeaturesWrapper from './features/FeaturesWrapper';
const unauthorizedRedirect = () => <Redirect to="/" />;
class App extends Component {
notistackRef: RefObject<any> = React.createRef();
componentDidMount() {
@@ -23,21 +22,29 @@ class App extends Component {
onClickDismiss = (key: string | number | undefined) => () => {
this.notistackRef.current.closeSnackbar(key);
}
};
render() {
return (
<CustomMuiTheme>
<SnackbarProvider autoHideDuration={3000} 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">
<CloseIcon />
</IconButton>
)}>
)}
>
<FeaturesWrapper>
<Switch>
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
<Route
exact
path="/unauthorized"
component={unauthorizedRedirect}
/>
<Route component={AppRouting} />
</Switch>
</FeaturesWrapper>
@@ -47,4 +54,4 @@ class App extends Component {
}
}
export default App
export default App;

View File

@@ -19,9 +19,9 @@ import Mqtt from './mqtt/Mqtt';
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
import { Features } from './features/types';
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/network/";
export const getDefaultRoute = (features: Features) =>
features.project ? `/${PROJECT_PATH}/` : '/network/';
class AppRouting extends Component<WithFeaturesProps> {
componentDidMount() {
Authentication.clearLoginRedirect();
}
@@ -35,9 +35,17 @@ class AppRouting extends Component<WithFeaturesProps> {
<UnauthenticatedRoute exact path="/" component={SignIn} />
)}
{features.project && (
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/*`}
component={ProjectRouting}
/>
)}
<AuthenticatedRoute exact path="/network/*" component={NetworkConnection} />
<AuthenticatedRoute
exact
path="/network/*"
component={NetworkConnection}
/>
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
{features.ntp && (
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
@@ -52,7 +60,7 @@ class AppRouting extends Component<WithFeaturesProps> {
<Redirect to={getDefaultRoute(features)} />
</Switch>
</AuthenticationWrapper>
)
);
}
}

View File

@@ -1,17 +1,21 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { CssBaseline } from '@material-ui/core';
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
import {
MuiThemeProvider,
createMuiTheme,
StylesProvider
} from '@material-ui/core/styles';
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
const theme = createMuiTheme({
palette: {
type: "dark",
type: 'dark',
primary: {
main: '#33bfff',
main: '#33bfff'
},
secondary: {
main: '#3d5afe',
main: '#3d5afe'
},
info: {
main: blueGrey[500]
@@ -29,7 +33,6 @@ const theme = createMuiTheme({
});
export default class CustomMuiTheme extends Component {
render() {
return (
<StylesProvider>
@@ -40,5 +43,4 @@ export default class CustomMuiTheme extends Component {
</StylesProvider>
);
}
}

View File

@@ -2,53 +2,63 @@ import React, { Component } from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
import {
withStyles,
createStyles,
Theme,
WithStyles
} from '@material-ui/core/styles';
import { Paper, Typography, Fab } from '@material-ui/core';
import ForwardIcon from '@material-ui/icons/Forward';
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
import {PasswordValidator} from './components';
import {
withAuthenticationContext,
AuthenticationContextProps
} from './authentication/AuthenticationContext';
import { PasswordValidator } from './components';
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
const styles = (theme: Theme) => createStyles({
const styles = (theme: Theme) =>
createStyles({
signInPage: {
display: "flex",
height: "100vh",
margin: "auto",
display: 'flex',
height: '100vh',
margin: 'auto',
padding: theme.spacing(2),
justifyContent: "center",
flexDirection: "column",
justifyContent: 'center',
flexDirection: 'column',
maxWidth: theme.breakpoints.values.sm
},
signInPanel: {
textAlign: "center",
textAlign: 'center',
padding: theme.spacing(2),
paddingTop: "200px",
paddingTop: '200px',
backgroundImage: 'url("/app/icon.png")',
backgroundRepeat: "no-repeat",
backgroundPosition: "50% " + theme.spacing(2) + "px",
backgroundSize: "auto 150px",
width: "100%"
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% ' + theme.spacing(2) + 'px',
backgroundSize: 'auto 150px',
width: '100%'
},
extendedIcon: {
marginRight: theme.spacing(0.5),
marginRight: theme.spacing(0.5)
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
marginTop: theme.spacing(2)
}
});
});
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
type SignInProps = WithSnackbarProps &
WithStyles<typeof styles> &
AuthenticationContextProps;
interface SignInState {
username: string,
password: string,
processing: boolean
username: string;
password: string;
processing: boolean;
}
class SignIn extends Component<SignInProps, SignInState> {
constructor(props: SignInProps) {
super(props);
this.state = {
@@ -60,10 +70,10 @@ class SignIn extends Component<SignInProps, SignInState> {
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
const { name, value } = event.currentTarget;
this.setState(prevState => ({
this.setState((prevState) => ({
...prevState,
[name]: value,
}))
[name]: value
}));
};
onSubmit = () => {
@@ -77,20 +87,21 @@ class SignIn extends Component<SignInProps, SignInState> {
'Content-Type': 'application/json'
})
})
.then(response => {
.then((response) => {
if (response.status === 200) {
return response.json();
} else if (response.status === 401) {
throw Error("Invalid credentials.");
throw Error('Invalid credentials.');
} else {
throw Error("Invalid status code: " + response.status);
throw Error('Invalid status code: ' + response.status);
}
}).then(json => {
})
.then((json) => {
authenticationContext.signIn(json.access_token);
})
.catch(error => {
.catch((error) => {
this.props.enqueueSnackbar(error.message, {
variant: 'warning',
variant: 'warning'
});
this.setState({ processing: false });
});
@@ -116,8 +127,8 @@ class SignIn extends Component<SignInProps, SignInState> {
onChange={this.updateInputElement}
margin="normal"
inputProps={{
autoCapitalize: "none",
autoCorrect: "off",
autoCapitalize: 'none',
autoCorrect: 'off'
}}
/>
<PasswordValidator
@@ -132,7 +143,13 @@ class SignIn extends Component<SignInProps, SignInState> {
onChange={this.updateInputElement}
margin="normal"
/>
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
<Fab
variant="extended"
color="primary"
className={classes.button}
type="submit"
disabled={processing}
>
<ForwardIcon className={classes.extendedIcon} />
Sign In
</Fab>
@@ -141,7 +158,8 @@ class SignIn extends Component<SignInProps, SignInState> {
</div>
);
}
}
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
export default withAuthenticationContext(
withSnackbar(withStyles(styles)(SignIn))
);

View File

@@ -1,5 +1,8 @@
import { APSettings, APProvisionMode } from "./types";
import { APSettings, APProvisionMode } from './types';
export const isAPEnabled = ({ provision_mode }: APSettings) => {
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
}
return (
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED
);
};

View File

@@ -1,7 +1,12 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { AP_SETTINGS_ENDPOINT } from '../api';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import APSettingsForm from './APSettingsForm';
import { APSettings } from './types';
@@ -9,7 +14,6 @@ import { APSettings } from './types';
type APSettingsControllerProps = RestControllerProps<APSettings>;
class APSettingsController extends Component<APSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class APSettingsController extends Component<APSettingsControllerProps> {
<SectionContent title="Access Point Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <APSettingsForm {...formProps} />}
render={(formProps) => <APSettingsForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);

View File

@@ -1,10 +1,19 @@
import React, { Fragment } from 'react';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import {
TextValidator,
ValidatorForm,
SelectValidator
} from 'react-material-ui-form-validator';
import MenuItem from '@material-ui/core/MenuItem';
import SaveIcon from '@material-ui/icons/Save';
import { PasswordValidator, RestFormProps, FormActions, FormButton } from '../components';
import {
PasswordValidator,
RestFormProps,
FormActions,
FormButton
} from '../components';
import { isAPEnabled } from './APModes';
import { APSettings, APProvisionMode } from './types';
@@ -13,7 +22,6 @@ import { isIP } from '../validators';
type APSettingsFormProps = RestFormProps<APSettings>;
class APSettingsForm extends React.Component<APSettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isIP', isIP);
}
@@ -22,23 +30,29 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
const { data, handleValueChange, saveData } = this.props;
return (
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
<SelectValidator name="provision_mode"
<SelectValidator
name="provision_mode"
label="Provide Access Point&hellip;"
value={data.provision_mode}
fullWidth
variant="outlined"
onChange={handleValueChange('provision_mode')}
margin="normal">
margin="normal"
>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When Network Disconnected</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
When Network Disconnected
</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
</SelectValidator>
{
isAPEnabled(data) &&
{isAPEnabled(data) && (
<Fragment>
<TextValidator
validators={['required', 'matchRegexp:^.{1,32}$']}
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
errorMessages={[
'Access Point SSID is required',
'Access Point SSID must be 32 characters or less'
]}
name="ssid"
label="Access Point SSID"
fullWidth
@@ -49,7 +63,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{8,64}$']}
errorMessages={['Access Point Password is required', 'Access Point Password must be 8-64 characters']}
errorMessages={[
'Access Point Password is required',
'Access Point Password must be 8-64 characters'
]}
name="password"
label="Access Point Password"
fullWidth
@@ -71,7 +88,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Gateway IP is required', 'Must be an IP address']}
errorMessages={[
'Gateway IP is required',
'Must be an IP address'
]}
name="gateway_ip"
label="Gateway"
fullWidth
@@ -82,7 +102,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Subnet mask is required', 'Must be an IP address']}
errorMessages={[
'Subnet mask is required',
'Must be an IP address'
]}
name="subnet_mask"
label="Subnet"
fullWidth
@@ -92,9 +115,14 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
margin="normal"
/>
</Fragment>
}
)}
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -1,5 +1,5 @@
import { Theme } from "@material-ui/core";
import { APStatus, APNetworkStatus } from "./types";
import { Theme } from '@material-ui/core';
import { APStatus, APNetworkStatus } from './types';
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
switch (status) {
@@ -12,17 +12,17 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
default:
return theme.palette.warning.main;
}
}
};
export const apStatus = ({ status }: APStatus) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return "Active";
return 'Active';
case APNetworkStatus.INACTIVE:
return "Inactive";
return 'Inactive';
case APNetworkStatus.LINGERING:
return "Lingering until idle";
return 'Lingering until idle';
default:
return "Unknown";
return 'Unknown';
}
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { AP_STATUS_ENDPOINT } from '../api';
import APStatusForm from './APStatusForm';
@@ -9,7 +14,6 @@ import { APStatus } from './types';
type APStatusControllerProps = RestControllerProps<APStatus>;
class APStatusController extends Component<APStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,10 +23,10 @@ class APStatusController extends Component<APStatusControllerProps> {
<SectionContent title="Access Point Status">
<RestFormLoader
{...this.props}
render={formProps => <APStatusForm {...formProps} />}
render={(formProps) => <APStatusForm {...formProps} />}
/>
</SectionContent>
)
);
}
}

View File

@@ -1,23 +1,34 @@
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 {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@material-ui/core';
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import ComputerIcon from '@material-ui/icons/Computer';
import RefreshIcon from '@material-ui/icons/Refresh';
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
import {
RestFormProps,
FormActions,
FormButton,
HighlightAvatar
} from '../components';
import { apStatusHighlight, apStatus } from './APStatus';
import { APStatus } from './types';
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
class APStatusForm extends Component<APStatusFormProps> {
createListItems() {
const { data, theme } = this.props
const { data, theme } = this.props;
return (
<Fragment>
<ListItem>
@@ -61,18 +72,20 @@ class APStatusForm extends Component<APStatusFormProps> {
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(APStatusForm);

View File

@@ -1,9 +1,13 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
import {
AuthenticatedContextProps,
withAuthenticatedContext,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import APSettingsController from './APSettingsController';
@@ -12,8 +16,7 @@ import APStatusController from './APStatusController';
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
class AccessPoint extends Component<AccessPointProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -21,17 +24,33 @@ class AccessPoint extends Component<AccessPointProps> {
const { authenticatedContext } = this.props;
return (
<MenuAppBar sectionTitle="Access Point">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/ap/status" label="Access Point Status" />
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/ap/settings"
label="Access Point Settings"
disabled={!authenticatedContext.me.admin}
/>
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
<AuthenticatedRoute
exact
path="/ap/status"
component={APStatusController}
/>
<AuthenticatedRoute
exact
path="/ap/settings"
component={APSettingsController}
/>
<Redirect to="/ap/status" />
</Switch>
</MenuAppBar>
)
);
}
}

View File

@@ -1,23 +1,24 @@
import { ENDPOINT_ROOT } from './Env';
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
export const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "networkSettings";
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + "networkStatus";
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
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";
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features';
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus';
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings';
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time';
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings';
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus';
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks';
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks';
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings';
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus';
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings';
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware';
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings';
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus';
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

@@ -1,8 +1,10 @@
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
export const EVENT_SOURCE_ROOT = calculateEndpointRoot('/es/');
export const API_ENDPOINT_ROOT = calculateEndpointRoot('/api/');
function calculateEndpointRoot(endpointPath: string) {
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
@@ -10,7 +12,7 @@ function calculateEndpointRoot(endpointPath: string) {
return httpRoot + endpointPath;
}
const location = window.location;
return location.protocol + "//" + location.host + endpointPath;
return location.protocol + '//' + location.host + endpointPath;
}
function calculateWebSocketRoot(webSocketPath: string) {
@@ -19,6 +21,6 @@ function calculateWebSocketRoot(webSocketPath: string) {
return webSocketRoot + webSocketPath;
}
const location = window.location;
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
return webProtocol + "//" + location.host + webSocketPath;
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
return webProtocol + '//' + location.host + webSocketPath;
}

View File

@@ -1,2 +1,2 @@
export * from './Env'
export * from './Endpoints'
export * from './Env';
export * from './Endpoints';

View File

@@ -1,40 +1,56 @@
import * as React from 'react';
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
import {
Redirect,
Route,
RouteProps,
RouteComponentProps
} from 'react-router-dom';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import * as Authentication from './Authentication';
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
import {
withAuthenticationContext,
AuthenticationContextProps,
AuthenticatedContext,
AuthenticatedContextValue
} from './AuthenticationContext';
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
interface AuthenticatedRouteProps
extends RouteProps,
WithSnackbarProps,
AuthenticationContextProps {
component:
| React.ComponentType<RouteComponentProps<any>>
| React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
render() {
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
const {
enqueueSnackbar,
authenticationContext,
component: Component,
...rest
} = this.props;
const { location } = this.props;
const renderComponent: RenderComponent = (props) => {
if (authenticationContext.me) {
return (
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
<AuthenticatedContext.Provider
value={authenticationContext as AuthenticatedContextValue}
>
<Component {...props} />
</AuthenticatedContext.Provider>
);
}
Authentication.storeLoginRedirect(location);
enqueueSnackbar("Please sign in to continue", { variant: 'info' });
return (
<Redirect to='/' />
);
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
return <Redirect to="/" />;
};
return <Route {...rest} render={renderComponent} />;
}
return (
<Route {...rest} render={renderComponent} />
);
}
}
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));

View File

@@ -27,7 +27,9 @@ export function clearLoginRedirect() {
getStorage().removeItem(SIGN_IN_SEARCH);
}
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
export function fetchLoginRedirect(
features: Features
): H.LocationDescriptorObject {
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
clearLoginRedirect();
@@ -38,16 +40,19 @@ export function fetchLoginRedirect(features: Features): H.LocationDescriptorObje
}
/**
* Wraps the normal fetch routene with one with provides the access token if present.
* Wraps the normal fetch routine with one with provides the access token if present.
*/
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
export function authorizedFetch(
url: RequestInfo,
params?: RequestInit
): Promise<Response> {
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
params = params || {};
params.credentials = 'include';
params.headers = {
...params.headers,
"Authorization": 'Bearer ' + accessToken
Authorization: 'Bearer ' + accessToken
};
}
return fetch(url, params);
@@ -55,26 +60,31 @@ export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise
/**
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
* for a single file upload and takes care of adding the Authroization header and redirecting on
* authroization errors as we do for normal fetch operations.
* for a single file upload and takes care of adding the Authorization header and redirecting on
* authorization errors as we do for normal fetch operations.
*/
export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, file: File, onProgress: (event: ProgressEvent<EventTarget>) => void): Promise<void> {
export function redirectingAuthorizedUpload(
xhr: XMLHttpRequest,
url: string,
file: File,
onProgress: (event: ProgressEvent<EventTarget>) => void
): Promise<void> {
return new Promise((resolve, reject) => {
xhr.open("POST", url, true);
xhr.open('POST', url, true);
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
xhr.withCredentials = true;
xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
}
xhr.upload.onprogress = onProgress;
xhr.onload = function () {
if (xhr.status === 401 || xhr.status === 403) {
history.push("/unauthorized");
history.push('/unauthorized');
} else {
resolve();
}
};
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
xhr.onerror = function () {
reject(new DOMException('Error', 'UploadError'));
};
xhr.onabort = function () {
@@ -87,17 +97,22 @@ export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, fi
}
/**
* Wraps the normal fetch routene which redirects on 401 response.
* Wraps the normal fetch routine which redirects on 401 response.
*/
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
export function redirectingAuthorizedFetch(
url: RequestInfo,
params?: RequestInit
): Promise<Response> {
return new Promise<Response>((resolve, reject) => {
authorizedFetch(url, params).then(response => {
authorizedFetch(url, params)
.then((response) => {
if (response.status === 401 || response.status === 403) {
history.push("/unauthorized");
history.push('/unauthorized');
} else {
resolve(response);
}
}).catch(error => {
})
.catch((error) => {
reject(error);
});
});

View File

@@ -1,9 +1,8 @@
import * as React from "react";
import * as React from 'react';
export interface Me {
username: string;
admin: boolean;
version: string; // proddy added
}
export interface AuthenticationContextValue {
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
me?: Me;
}
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
export const AuthenticationContext = React.createContext(
AuthenticationContextDefaultValue
);
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
authenticationContext: AuthenticationContextValue;
}
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
export function withAuthenticationContext<T extends AuthenticationContextProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<
Omit<T, keyof AuthenticationContextProps>
> {
render() {
return (
<AuthenticationContext.Consumer>
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
{(authenticationContext) => (
<Component
{...(this.props as T)}
authenticationContext={authenticationContext}
/>
)}
</AuthenticationContext.Consumer>
);
}
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
me: Me;
}
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
export const AuthenticatedContext = React.createContext(
AuthenticatedContextDefaultValue
);
@@ -47,12 +55,21 @@ export interface AuthenticatedContextProps {
authenticatedContext: AuthenticatedContextValue;
}
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<
Omit<T, keyof AuthenticatedContextProps>
> {
render() {
return (
<AuthenticatedContext.Consumer>
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
{(authenticatedContext) => (
<Component
{...(this.props as T)}
authenticatedContext={authenticatedContext}
/>
)}
</AuthenticatedContext.Consumer>
);
}

View File

@@ -2,14 +2,19 @@ import * as React from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import jwtDecode from 'jwt-decode';
import history from '../history'
import history from '../history';
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
import { AuthenticationContext, AuthenticationContextValue, Me } from './AuthenticationContext';
import {
AuthenticationContext,
AuthenticationContextValue,
Me
} from './AuthenticationContext';
import FullScreenLoading from '../components/FullScreenLoading';
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
export const decodeMeJWT = (accessToken: string): Me =>
jwtDecode(accessToken) as Me;
interface AuthenticationWrapperState {
context: AuthenticationContextValue;
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
class AuthenticationWrapper extends React.Component<
AuthenticationWrapperProps,
AuthenticationWrapperState
> {
constructor(props: AuthenticationWrapperProps) {
super(props);
this.state = {
context: {
refresh: this.refresh,
signIn: this.signIn,
signOut: this.signOut,
signOut: this.signOut
},
initialized: false
};
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
render() {
return (
<React.Fragment>
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
{this.state.initialized
? this.renderContent()
: this.renderContentLoading()}
</React.Fragment>
);
}
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
}
renderContentLoading() {
return (
<FullScreenLoading />
);
return <FullScreenLoading />;
}
refresh = () => {
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
// return;
// }
const accessToken = getStorage().getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
.then(response => {
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
this.setState({ initialized: true, context: { ...this.state.context, me } });
}).catch(error => {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
variant: 'error',
.then((response) => {
const me =
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
this.setState({
initialized: true,
context: { ...this.state.context, me }
});
})
.catch((error) => {
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
this.props.enqueueSnackbar(
'Error verifying authorization: ' + error.message,
{
variant: 'error'
}
);
});
} else {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
}
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
}
};
signIn = (accessToken: string) => {
try {
getStorage().setItem(ACCESS_TOKEN, accessToken);
const me: Me = decodeMeJWT(accessToken);
this.setState({ context: { ...this.state.context, me } });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, {
variant: 'success'
});
} catch (err) {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
throw new Error("Failed to parse JWT " + err.message);
}
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
throw new Error('Failed to parse JWT ' + err.message);
}
};
signOut = () => {
getStorage().removeItem(ACCESS_TOKEN);
@@ -101,10 +127,9 @@ 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('/');
}
};
}
export default withFeatures(withSnackbar(AuthenticationWrapper))
export default withFeatures(withSnackbar(AuthenticationWrapper));

View File

@@ -1,31 +1,46 @@
import * as React from 'react';
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
import {
Redirect,
Route,
RouteProps,
RouteComponentProps
} from 'react-router-dom';
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
import {
withAuthenticationContext,
AuthenticationContextProps
} from './AuthenticationContext';
import * as Authentication from './Authentication';
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
interface UnauthenticatedRouteProps
extends RouteProps,
AuthenticationContextProps,
WithFeaturesProps {
component:
| React.ComponentType<RouteComponentProps<any>>
| React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
public render() {
const { authenticationContext, component: Component, features, ...rest } = this.props;
const {
authenticationContext,
component: Component,
features,
...rest
} = this.props;
const renderComponent: RenderComponent = (props) => {
if (authenticationContext.me) {
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
}
if (Component) {
return (<Component {...props} />);
return <Component {...props} />;
}
}
return (
<Route {...rest} render={renderComponent} />
);
};
return <Route {...rest} render={renderComponent} />;
}
}

View File

@@ -1,27 +1,25 @@
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/styles';
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
import WarningIcon from "@material-ui/icons/Warning"
import { Paper, Typography, Box, CssBaseline } from '@material-ui/core';
import WarningIcon from '@material-ui/icons/Warning';
const styles = makeStyles(
{
const styles = makeStyles({
siteErrorPage: {
display: "flex",
height: "100vh",
justifyContent: "center",
flexDirection: "column"
display: 'flex',
height: '100vh',
justifyContent: 'center',
flexDirection: 'column'
},
siteErrorPagePanel: {
textAlign: "center",
padding: "280px 0 40px 0",
textAlign: 'center',
padding: '280px 0 40px 0',
backgroundImage: 'url("/app/icon.png")',
backgroundRepeat: "no-repeat",
backgroundPosition: "50% 40px",
backgroundSize: "200px auto",
width: "100%",
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% 40px',
backgroundSize: '200px auto',
width: '100%'
}
}
);
});
interface ApplicationErrorProps {
error?: string;
@@ -33,27 +31,29 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
<div className={classes.siteErrorPage}>
<CssBaseline />
<Paper className={classes.siteErrorPagePanel} elevation={10}>
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}>
<Box
display="flex"
flexDirection="row"
justifyContent="center"
alignItems="center"
mb={2}
>
<WarningIcon fontSize="large" color="error" />
<Box ml={2}>
<Typography variant="h4">
Application error
</Typography>
<Typography variant="h4">Application error</Typography>
</Box>
</Box>
<Typography variant="subtitle1" gutterBottom>
Failed to configure the application, please refresh to try again.
</Typography>
{error &&
(
{error && (
<Typography variant="subtitle2" gutterBottom>
Error: {error}
</Typography>
)
}
)}
</Paper>
</div>
);
}
};
export default ApplicationError;

View File

@@ -1,10 +1,10 @@
import React, { FC } from "react";
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
import { FC } from 'react';
import { FormControlLabel, FormControlLabelProps } from '@material-ui/core';
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
<div>
<FormControlLabel {...props} />
</div>
)
);
export default BlockFormControlLabel;

View File

@@ -1,10 +1,10 @@
import { Button, styled } from "@material-ui/core";
import { Button, styled } from '@material-ui/core';
const ErrorButton = styled(Button)(({ theme }) => ({
color: theme.palette.getContrastText(theme.palette.error.main),
backgroundColor: theme.palette.error.main,
'&:hover': {
backgroundColor: theme.palette.error.dark,
backgroundColor: theme.palette.error.dark
}
}));

View File

@@ -1,4 +1,4 @@
import { styled, Box } from "@material-ui/core";
import { styled, Box } from '@material-ui/core';
const FormActions = styled(Box)(({ theme }) => ({
marginTop: theme.spacing(1)

View File

@@ -1,12 +1,12 @@
import { Button, styled } from "@material-ui/core";
import { Button, styled } from '@material-ui/core';
const FormButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(0, 1),
'&:last-child': {
marginRight: 0,
marginRight: 0
},
'&:first-child': {
marginLeft: 0,
marginLeft: 0
}
}));

View File

@@ -3,30 +3,30 @@ import CircularProgress from '@material-ui/core/CircularProgress';
import { Typography, Theme } from '@material-ui/core';
import { makeStyles, createStyles } from '@material-ui/styles';
const useStyles = makeStyles((theme: Theme) => createStyles({
const useStyles = makeStyles((theme: Theme) =>
createStyles({
fullScreenLoading: {
padding: theme.spacing(2),
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
flexDirection: "column"
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
flexDirection: 'column'
},
progress: {
margin: theme.spacing(4),
margin: theme.spacing(4)
}
}));
})
);
const FullScreenLoading = () => {
const classes = useStyles();
return (
<div className={classes.fullScreenLoading}>
<CircularProgress className={classes.progress} size={100} />
<Typography variant="h4">
Loading&hellip;
</Typography>
<Typography variant="h4">Loading&hellip;</Typography>
</div>
)
}
);
};
export default FullScreenLoading;

View File

@@ -1,5 +1,5 @@
import { Avatar, makeStyles } from "@material-ui/core";
import React, { FC } from "react";
import { Avatar, makeStyles } from '@material-ui/core';
import { FC } from 'react';
interface HighlightAvatarProps {
color: string;
@@ -13,11 +13,7 @@ const useStyles = makeStyles({
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
const classes = useStyles(props);
return (
<Avatar className={classes.root}>
{props.children}
</Avatar>
);
}
return <Avatar className={classes.root}>{props.children}</Avatar>;
};
export default HighlightAvatar;

View File

@@ -1,12 +1,39 @@
import React, { RefObject, Fragment } from 'react';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
import {
Drawer,
AppBar,
Toolbar,
Avatar,
Divider,
Button,
Box,
IconButton
} from '@material-ui/core';
import {
ClickAwayListener,
Popper,
Hidden,
Typography
} from '@material-ui/core';
import {
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemAvatar
} from '@material-ui/core';
import { Card, CardContent, CardActions } from '@material-ui/core';
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
import {
withStyles,
createStyles,
Theme,
WithTheme,
WithStyles,
withTheme
} from '@material-ui/core/styles';
import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
import SettingsIcon from '@material-ui/icons/Settings';
@@ -19,20 +46,24 @@ import MenuIcon from '@material-ui/icons/Menu';
import ProjectMenu from '../project/ProjectMenu';
import { PROJECT_NAME } from '../api';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
const drawerWidth = 290;
const styles = (theme: Theme) => createStyles({
const styles = (theme: Theme) =>
createStyles({
root: {
display: 'flex',
display: 'flex'
},
drawer: {
[theme.breakpoints.up('md')]: {
width: drawerWidth,
flexShrink: 0,
},
flexShrink: 0
}
},
title: {
flexGrow: 1
@@ -40,8 +71,8 @@ const styles = (theme: Theme) => createStyles({
appBar: {
marginLeft: drawerWidth,
[theme.breakpoints.up('md')]: {
width: `calc(100% - ${drawerWidth}px)`,
},
width: `calc(100% - ${drawerWidth}px)`
}
},
toolbarImage: {
[theme.breakpoints.up('xs')]: {
@@ -51,44 +82,48 @@ const styles = (theme: Theme) => createStyles({
[theme.breakpoints.up('sm')]: {
height: 36,
marginRight: theme.spacing(3)
},
}
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up('md')]: {
display: 'none',
},
display: 'none'
}
},
toolbar: theme.mixins.toolbar,
drawerPaper: {
width: drawerWidth,
width: drawerWidth
},
content: {
flexGrow: 1
},
authMenu: {
zIndex: theme.zIndex.tooltip,
maxWidth: 400,
maxWidth: 400
},
authMenuActions: {
padding: theme.spacing(2),
"& > * + *": {
marginLeft: theme.spacing(2),
'& > * + *': {
marginLeft: theme.spacing(2)
}
},
});
}
});
interface MenuAppBarState {
mobileOpen: boolean;
authMenuOpen: boolean;
}
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
interface MenuAppBarProps
extends WithFeaturesProps,
AuthenticatedContextProps,
WithTheme,
WithStyles<typeof styles>,
RouteComponentProps {
sectionTitle: string;
}
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
constructor(props: MenuAppBarProps) {
super(props);
this.state = {
@@ -101,38 +136,48 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
handleToggle = () => {
this.setState({ authMenuOpen: !this.state.authMenuOpen });
}
};
handleClose = (event: React.MouseEvent<Document>) => {
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
if (
this.anchorRef.current &&
this.anchorRef.current.contains(event.currentTarget)
) {
return;
}
this.setState({ authMenuOpen: false });
}
};
handleDrawerToggle = () => {
this.setState({ mobileOpen: !this.state.mobileOpen });
};
render() {
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
const {
classes,
theme,
children,
sectionTitle,
authenticatedContext,
features
} = this.props;
const { mobileOpen, authMenuOpen } = this.state;
const path = this.props.match.url;
const drawer = (
<div>
<Toolbar>
<Box display="flex">
<img src="/app/icon.png" className={classes.toolbarImage} alt={PROJECT_NAME} />
<img
src="/app/icon.png"
className={classes.toolbarImage}
alt={PROJECT_NAME}
/>
</Box>
<Typography variant="h6" color="textPrimary">
{PROJECT_NAME}
</Typography>
<Typography align="right" variant="caption" color="textPrimary">
&nbsp;&nbsp;v{authenticatedContext.me.version}
</Typography>
<Divider absolute />
</Toolbar>
@@ -144,20 +189,35 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
)}
<List>
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
<ListItem
to="/network/"
selected={path.startsWith('/network/')}
button
component={Link}
>
<ListItemIcon>
<SettingsEthernetIcon />
</ListItemIcon>
<ListItemText primary="Network Connection" />
</ListItem>
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
<ListItem
to="/ap/"
selected={path.startsWith('/ap/')}
button
component={Link}
>
<ListItemIcon>
<SettingsInputAntennaIcon />
</ListItemIcon>
<ListItemText primary="Access Point" />
</ListItem>
{features.ntp && (
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
<ListItem
to="/ntp/"
selected={path.startsWith('/ntp/')}
button
component={Link}
>
<ListItemIcon>
<AccessTimeIcon />
</ListItemIcon>
@@ -165,7 +225,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
</ListItem>
)}
{features.mqtt && (
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
<ListItem
to="/mqtt/"
selected={path.startsWith('/mqtt/')}
button
component={Link}
>
<ListItemIcon>
<DeviceHubIcon />
</ListItemIcon>
@@ -173,14 +238,25 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
</ListItem>
)}
{features.security && (
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
<ListItem
to="/security/"
selected={path.startsWith('/security/')}
button
component={Link}
disabled={!authenticatedContext.me.admin}
>
<ListItemIcon>
<LockIcon />
</ListItemIcon>
<ListItemText primary="Security" />
</ListItem>
)}
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
<ListItem
to="/system/"
selected={path.startsWith('/system/')}
button
component={Link}
>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
@@ -201,7 +277,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
>
<AccountCircleIcon />
</IconButton>
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
<Popper
open={authMenuOpen}
anchorEl={this.anchorRef.current}
transition
className={classes.authMenu}
>
<ClickAwayListener onClickAway={this.handleClose}>
<Card id="menu-list-grow">
<CardContent>
@@ -212,13 +293,27 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
<AccountCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
<ListItemText
primary={
'Signed in as: ' + authenticatedContext.me.username
}
secondary={
authenticatedContext.me.admin ? 'Admin User' : undefined
}
/>
</ListItem>
</List>
</CardContent>
<Divider />
<CardActions className={classes.authMenuActions}>
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
<Button
variant="contained"
fullWidth
color="primary"
onClick={authenticatedContext.signOut}
>
Sign Out
</Button>
</CardActions>
</Card>
</ClickAwayListener>
@@ -239,7 +334,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
>
<MenuIcon />
</IconButton>
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
<Typography
variant="h6"
color="inherit"
noWrap
className={classes.title}
>
{sectionTitle}
</Typography>
{features.security && userMenu}
@@ -253,10 +353,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
open={mobileOpen}
onClose={this.handleDrawerToggle}
classes={{
paper: classes.drawerPaper,
paper: classes.drawerPaper
}}
ModalProps={{
keepMounted: true,
keepMounted: true
}}
>
{drawer}
@@ -265,7 +365,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
<Hidden smDown implementation="css">
<Drawer
classes={{
paper: classes.drawerPaper,
paper: classes.drawerPaper
}}
variant="permanent"
open
@@ -285,10 +385,6 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
export default withRouter(
withTheme(
withFeatures(
withAuthenticatedContext(
withStyles(styles)(MenuAppBar)
)
)
withFeatures(withAuthenticatedContext(withStyles(styles)(MenuAppBar)))
)
);

View File

@@ -1,26 +1,32 @@
import React from 'react';
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
import {
TextValidator,
ValidatorComponentProps
} from 'react-material-ui-form-validator';
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
import { InputAdornment, IconButton } from '@material-ui/core';
import {Visibility,VisibilityOff } from '@material-ui/icons';
import { Visibility, VisibilityOff } from '@material-ui/icons';
const styles = createStyles({
input: {
"&::-ms-reveal": {
display: "none"
'&::-ms-reveal': {
display: 'none'
}
}
});
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
type PasswordValidatorProps = WithStyles<typeof styles> &
Exclude<ValidatorComponentProps, 'type' | 'InputProps'>;
interface PasswordValidatorState {
showPassword: boolean;
}
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
class PasswordValidator extends React.Component<
PasswordValidatorProps,
PasswordValidatorState
> {
state = {
showPassword: false
};
@@ -29,7 +35,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
this.setState({
showPassword: !this.state.showPassword
});
}
};
render() {
const { classes, ...rest } = this.props;
@@ -39,7 +45,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
type={this.state.showPassword ? 'text' : 'password'}
InputProps={{
classes,
endAdornment:
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
@@ -48,11 +54,11 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
}}
/>
);
}
}
export default withStyles(styles)(PasswordValidator);

View File

@@ -4,7 +4,9 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
import { redirectingAuthorizedFetch } from '../authentication';
export interface RestControllerProps<D> extends WithSnackbarProps {
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
handleValueChange: (
name: keyof D
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
setData: (data: D, callback?: () => void) => void;
saveData: () => void;
@@ -15,16 +17,18 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
errorMessage?: string;
}
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
export const extractEventValue = (
event: React.ChangeEvent<HTMLInputElement>
) => {
switch (event.target.type) {
case "number":
case 'number':
return event.target.valueAsNumber;
case "checkbox":
case 'checkbox':
return event.target.checked;
default:
return event.target.value
return event.target.value;
}
}
};
interface RestControllerState<D> {
data?: D;
@@ -32,10 +36,15 @@ interface RestControllerState<D> {
errorMessage?: string;
}
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
export function restController<D, P extends RestControllerProps<D>>(
endpointUrl: string,
RestController: React.ComponentType<P & RestControllerProps<D>>
) {
return withSnackbar(
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
class extends React.Component<
Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps,
RestControllerState<D>
> {
state: RestControllerState<D> = {
data: undefined,
loading: false,
@@ -43,12 +52,15 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
};
setData = (data: D, callback?: () => void) => {
this.setState({
this.setState(
{
data,
loading: false,
errorMessage: undefined
}, callback);
}
},
callback
);
};
loadData = () => {
this.setState({
@@ -56,19 +68,24 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
loading: true,
errorMessage: undefined
});
redirectingAuthorizedFetch(endpointUrl).then(response => {
redirectingAuthorizedFetch(endpointUrl)
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Invalid status code: " + response.status);
}).then(json => {
this.setState({ data: json, loading: false })
}).catch(error => {
const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
throw Error('Invalid status code: ' + response.status);
})
.then((json) => {
this.setState({ data: json, loading: false });
})
.catch((error) => {
const errorMessage = error.message || 'Unknown error';
this.props.enqueueSnackbar('Problem fetching: ' + errorMessage, {
variant: 'error'
});
this.setState({ data: undefined, loading: false, errorMessage });
});
}
};
saveData = () => {
this.setState({ loading: true });
@@ -78,36 +95,47 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
})
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Invalid status code: " + response.status);
}).then(json => {
this.props.enqueueSnackbar("Update successful.", { variant: 'success' });
throw Error('Invalid status code: ' + response.status);
})
.then((json) => {
this.props.enqueueSnackbar('Update successful.', {
variant: 'success'
});
this.setState({ data: json, loading: false });
}).catch(error => {
const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
})
.catch((error) => {
const errorMessage = error.message || 'Unknown error';
this.props.enqueueSnackbar('Problem updating: ' + errorMessage, {
variant: 'error'
});
this.setState({ data: undefined, loading: false, errorMessage });
});
}
};
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
handleValueChange = (name: keyof D) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
const data = { ...this.state.data!, [name]: extractEventValue(event) };
this.setState({ data });
}
};
render() {
return <RestController
return (
<RestController
{...this.state}
{...this.props as P}
{...(this.props as P)}
handleValueChange={this.handleValueChange}
setData={this.setData}
saveData={this.saveData}
loadData={this.loadData}
/>;
/>
);
}
});
}
);
}

View File

@@ -8,20 +8,23 @@ import { RestControllerProps } from '.';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
loadingSettings: {
margin: theme.spacing(0.5),
margin: theme.spacing(0.5)
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
marginTop: theme.spacing(2)
}
})
);
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
export type RestFormProps<D> = Omit<
RestControllerProps<D>,
'loading' | 'errorMessage'
> & { data: D };
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
render: (props: RestFormProps<D>) => JSX.Element;
@@ -46,7 +49,12 @@ export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
<Typography variant="h6" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
<Button
variant="contained"
color="secondary"
className={classes.button}
onClick={loadData}
>
Retry
</Button>
</div>

View File

@@ -7,7 +7,7 @@ const useStyles = makeStyles((theme: Theme) =>
createStyles({
content: {
padding: theme.spacing(2),
margin: theme.spacing(3),
margin: theme.spacing(3)
}
})
);
@@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) =>
interface SectionContentProps {
title: string;
titleGutter?: boolean;
id?: string;
}
const SectionContent: React.FC<SectionContentProps> = (props) => {
const { children, title, titleGutter } = props;
const { children, title, titleGutter, id } = props;
const classes = useStyles();
return (
<Paper className={classes.content}>
<Paper id={id} className={classes.content}>
<Typography variant="h6" gutterBottom={titleGutter}>
{title}
</Typography>

View File

@@ -4,13 +4,20 @@ import { useDropzone, DropzoneState } from 'react-dropzone';
import { makeStyles, createStyles } from '@material-ui/styles';
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
import CancelIcon from '@material-ui/icons/Cancel';
import { Theme, Box, Typography, LinearProgress, Button } from '@material-ui/core';
import {
Theme,
Box,
Typography,
LinearProgress,
Button
} from '@material-ui/core';
interface SingleUploadStyleProps extends DropzoneState {
uploading: boolean;
}
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
const progressPercentage = (progress: ProgressEvent) =>
Math.round((progress.loaded * 100) / progress.total);
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
if (props.isDragAccept) {
@@ -23,9 +30,10 @@ const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
return theme.palette.info.main;
}
return theme.palette.grey[700];
}
};
const useStyles = makeStyles((theme: Theme) => createStyles({
const useStyles = makeStyles((theme: Theme) =>
createStyles({
dropzone: {
padding: theme.spacing(8, 2),
borderWidth: 2,
@@ -33,11 +41,14 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
borderStyle: 'dashed',
color: theme.palette.grey[700],
transition: 'border .24s ease-in-out',
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
cursor: (props: SingleUploadStyleProps) =>
props.uploading ? 'default' : 'pointer',
width: '100%',
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
borderColor: (props: SingleUploadStyleProps) =>
getBorderColor(theme, props)
}
}));
})
);
export interface SingleUploadProps {
onDrop: (acceptedFiles: File[]) => void;
@@ -47,26 +58,44 @@ export interface SingleUploadProps {
progress?: ProgressEvent;
}
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
const SingleUpload: FC<SingleUploadProps> = ({
onDrop,
onCancel,
accept,
uploading,
progress
}) => {
const dropzoneState = useDropzone({
onDrop,
accept,
disabled: uploading,
multiple: false
});
const { getRootProps, getInputProps } = dropzoneState;
const classes = useStyles({ ...dropzoneState, uploading });
const renderProgressText = () => {
if (uploading) {
if (progress?.lengthComputable) {
return `Uploading: ${progressPercentage(progress)}%`;
}
return "Uploading\u2026";
}
return "Drop file or click here";
return 'Uploading\u2026';
}
return 'Drop file or click here';
};
const renderProgress = (progress?: ProgressEvent) => (
<LinearProgress
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
variant={
!progress || progress.lengthComputable ? 'determinate' : 'indeterminate'
}
value={
!progress
? 0
: progress.lengthComputable
? progressPercentage(progress)
: 0
}
/>
);
@@ -74,16 +103,19 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
<div {...getRootProps({ className: classes.dropzone })}>
<input {...getInputProps()} />
<Box flexDirection="column" display="flex" alignItems="center">
<CloudUploadIcon fontSize='large' />
<Typography variant="h6">
{renderProgressText()}
</Typography>
<CloudUploadIcon fontSize="large" />
<Typography variant="h6">{renderProgressText()}</Typography>
{uploading && (
<Fragment>
<Box width="100%" p={2}>
{renderProgress(progress)}
</Box>
<Button startIcon={<CancelIcon />} variant="contained" color="secondary" onClick={onCancel}>
<Button
startIcon={<CancelIcon />}
variant="contained"
color="secondary"
onClick={onCancel}
>
Cancel
</Button>
</Fragment>
@@ -91,6 +123,6 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
</Box>
</div>
);
}
};
export default SingleUpload;

View File

@@ -7,7 +7,9 @@ import { addAccessTokenParameter } from '../authentication';
import { extractEventValue } from '.';
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
handleValueChange: (
name: keyof D
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
setData: (data: D, callback?: () => void) => void;
saveData: () => void;
@@ -25,8 +27,8 @@ interface WebSocketControllerState<D> {
}
enum WebSocketMessageType {
ID = "id",
PAYLOAD = "payload"
ID = 'id',
PAYLOAD = 'payload'
}
interface WebSocketIdMessage {
@@ -40,21 +42,32 @@ interface WebSocketPayloadMessage<D> {
payload: D;
}
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>;
export type WebSocketMessage<D> =
| WebSocketIdMessage
| WebSocketPayloadMessage<D>;
export function webSocketController<D, P extends WebSocketControllerProps<D>>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>) {
export function webSocketController<D, P extends WebSocketControllerProps<D>>(
wsUrl: string,
wsThrottle: number,
WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>
) {
return withSnackbar(
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
class extends React.Component<
Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps,
WebSocketControllerState<D>
> {
constructor(
props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps
) {
super(props);
this.state = {
ws: new Sockette(addAccessTokenParameter(wsUrl), {
onmessage: this.onMessage,
onopen: this.onOpen,
onclose: this.onClose,
onclose: this.onClose
}),
connected: false
}
};
}
componentWillUnmount() {
@@ -64,37 +77,42 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
onMessage = (event: MessageEvent) => {
const rawData = event.data;
if (typeof rawData === 'string' || rawData instanceof String) {
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
}
this.handleMessage(
JSON.parse(rawData as string) as WebSocketMessage<D>
);
}
};
handleMessage = (message: WebSocketMessage<D>) => {
const { clientId, data } = this.state;
switch (message.type) {
case WebSocketMessageType.ID:
this.setState({ clientId: message.id });
break;
case WebSocketMessageType.PAYLOAD:
const { clientId, data } = this.state;
if (clientId && (!data || clientId !== message.origin_id)) {
this.setState(
{ data: message.payload }
);
this.setState({ data: message.payload });
}
break;
}
}
};
onOpen = () => {
this.setState({ connected: true });
}
};
onClose = () => {
this.setState({ connected: false, clientId: undefined, data: undefined });
}
this.setState({
connected: false,
clientId: undefined,
data: undefined
});
};
setData = (data: D, callback?: () => void) => {
this.setState({ data }, callback);
}
};
saveData = throttle(() => {
const { ws, connected, data } = this.state;
@@ -106,28 +124,35 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
saveDataAndClear = throttle(() => {
const { ws, connected, data } = this.state;
if (connected) {
this.setState({
this.setState(
{
data: undefined
}, () => ws.json(data));
},
() => ws.json(data)
);
}
}, wsThrottle);
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
handleValueChange = (name: keyof D) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
const data = { ...this.state.data!, [name]: extractEventValue(event) };
this.setState({ data });
}
};
render() {
return <WebSocketController
{...this.props as P}
return (
<WebSocketController
{...(this.props as P)}
handleValueChange={this.handleValueChange}
setData={this.setData}
saveData={this.saveData}
saveDataAndClear={this.saveDataAndClear}
connected={this.state.connected}
data={this.state.data}
/>;
/>
);
}
});
}
);
}

View File

@@ -1,5 +1,3 @@
import React from 'react';
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import { LinearProgress, Typography } from '@material-ui/core';
@@ -8,22 +6,27 @@ import { WebSocketControllerProps } from '.';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
loadingSettings: {
margin: theme.spacing(0.5),
margin: theme.spacing(0.5)
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
}
})
);
export type WebSocketFormProps<D> = Omit<WebSocketControllerProps<D>, "connected"> & { data: D };
export type WebSocketFormProps<D> = Omit<
WebSocketControllerProps<D>,
'connected'
> & { data: D };
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
render: (props: WebSocketFormProps<D>) => JSX.Element;
}
export default function WebSocketFormLoader<D>(props: WebSocketFormLoaderProps<D>) {
export default function WebSocketFormLoader<D>(
props: WebSocketFormLoaderProps<D>
) {
const { connected, render, data, ...rest } = props;
const classes = useStyles();
if (!connected || !data) {

View File

@@ -0,0 +1,14 @@
import { useLayoutEffect, useState } from 'react';
export function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}

View File

@@ -15,3 +15,5 @@ export * from './RestController';
export * from './WebSocketFormLoader';
export * from './WebSocketController';
export * from './WindowSize';

View File

@@ -5,21 +5,26 @@ export interface FeaturesContextValue {
features: Features;
}
const FeaturesContextDefaultValue = {} as FeaturesContextValue
export const FeaturesContext = React.createContext(
FeaturesContextDefaultValue
);
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
export const FeaturesContext = React.createContext(FeaturesContextDefaultValue);
export interface WithFeaturesProps {
features: Features;
}
export function withFeatures<T extends WithFeaturesProps>(Component: React.ComponentType<T>) {
export function withFeatures<T extends WithFeaturesProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
render() {
return (
<FeaturesContext.Consumer>
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
{(featuresContext) => (
<Component
{...(this.props as T)}
features={featuresContext.features}
/>
)}
</FeaturesContext.Consumer>
);
}

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { Features } from './types';
import { FeaturesContext } from './FeaturesContext';
@@ -9,10 +9,9 @@ import { FEATURES_ENDPOINT } from '../api';
interface FeaturesWrapperState {
features?: Features;
error?: string;
};
}
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
state: FeaturesWrapperState = {};
componentDidMount() {
@@ -21,41 +20,39 @@ class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
fetchFeaturesDetails = () => {
fetch(FEATURES_ENDPOINT)
.then(response => {
.then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw Error("Unexpected status code: " + response.status);
throw Error('Unexpected status code: ' + response.status);
}
}).then(features => {
})
.then((features) => {
this.setState({ features });
})
.catch(error => {
.catch((error) => {
this.setState({ error: error.message });
});
}
};
render() {
const { features, error } = this.state;
if (features) {
return (
<FeaturesContext.Provider value={{
<FeaturesContext.Provider
value={{
features
}}>
}}
>
{this.props.children}
</FeaturesContext.Provider>
);
}
if (error) {
return (
<ApplicationError error={error} />
);
return <ApplicationError error={error} />;
}
return (
<FullScreenLoading />
);
return <FullScreenLoading />;
}
}
export default FeaturesWrapper;

View File

@@ -2,4 +2,4 @@ import { createBrowserHistory } from 'history';
export default createBrowserHistory({
/* pass a configuration object here if needed */
})
});

View File

@@ -6,8 +6,9 @@ import { Router } from 'react-router';
import App from './App';
render((
render(
<Router history={history}>
<App/>
</Router>
), document.getElementById("root"))
<App />
</Router>,
document.getElementById('root')
);

View File

@@ -1,9 +1,13 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
import {
AuthenticatedContextProps,
withAuthenticatedContext,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import MqttStatusController from './MqttStatusController';
import MqttSettingsController from './MqttSettingsController';
@@ -11,8 +15,7 @@ import MqttSettingsController from './MqttSettingsController';
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
class Mqtt extends Component<MqttProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -20,17 +23,33 @@ class Mqtt extends Component<MqttProps> {
const { authenticatedContext } = this.props;
return (
<MenuAppBar sectionTitle="MQTT">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/mqtt/status" label="MQTT Status" />
<Tab value="/mqtt/settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/mqtt/settings"
label="MQTT Settings"
disabled={!authenticatedContext.me.admin}
/>
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
<AuthenticatedRoute
exact
path="/mqtt/status"
component={MqttStatusController}
/>
<AuthenticatedRoute
exact
path="/mqtt/settings"
component={MqttSettingsController}
/>
<Redirect to="/mqtt/status" />
</Switch>
</MenuAppBar>
)
);
}
}

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { MQTT_SETTINGS_ENDPOINT } from '../api';
import MqttSettingsForm from './MqttSettingsForm';
@@ -9,7 +14,6 @@ import { MqttSettings } from './types';
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class MqttSettingsController extends Component<MqttSettingsControllerProps> {
<SectionContent title="MQTT Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <MqttSettingsForm {...formProps} />}
render={(formProps) => <MqttSettingsForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);

View File

@@ -1,31 +1,31 @@
import React from "react";
import React from 'react';
import {
TextValidator,
ValidatorForm,
SelectValidator,
} from "react-material-ui-form-validator";
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";
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() {
@@ -36,38 +36,38 @@ 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"]}
validators={['required', 'isIPOrHostname']}
errorMessages={[
"Host is required",
"Not a valid IP address or hostname",
'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",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Port is required",
"Must be a number",
"Must be greater than 0 ",
"Max value is 65535",
'Port is required',
'Must be a number',
'Must be greater than 0 ',
'Max value is 65535'
]}
name="port"
label="Port"
@@ -75,18 +75,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
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
@@ -95,7 +95,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.username}
onChange={handleValueChange("username")}
onChange={handleValueChange('username')}
margin="normal"
/>
<PasswordValidator
@@ -104,30 +104,30 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.password}
onChange={handleValueChange("password")}
onChange={handleValueChange('password')}
margin="normal"
/>
<TextField
name="client_id"
label="Client ID (optional)"
label="Client ID"
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",
'required',
'isNumber',
'minNumber:1',
'maxNumber:65535'
]}
errorMessages={[
"Keep alive is required",
"Must be a number",
"Must be greater than 0",
"Max value is 65535",
'Keep alive is required',
'Must be a number',
'Must be greater than 0',
'Max value is 65535'
]}
name="keep_alive"
label="Keep Alive (seconds)"
@@ -135,7 +135,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.keep_alive}
type="number"
onChange={handleValueChange("keep_alive")}
onChange={handleValueChange('keep_alive')}
margin="normal"
/>
<SelectValidator
@@ -144,7 +144,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.mqtt_qos}
fullWidth
variant="outlined"
onChange={handleValueChange("mqtt_qos")}
onChange={handleValueChange('mqtt_qos')}
margin="normal"
>
<MenuItem value={0}>0 (default)</MenuItem>
@@ -155,21 +155,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.clean_session}
onChange={handleValueChange("clean_session")}
onChange={handleValueChange('clean_session')}
value="clean_session"
/>
}
label="Clean Session"
label="Set Clean Session"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.mqtt_retain}
onChange={handleValueChange("mqtt_retain")}
onChange={handleValueChange('mqtt_retain')}
value="mqtt_retain"
/>
}
label="Retain Flag"
label="Use Retain Flag"
/>
<br></br>
<Typography variant="h6" color="primary">
@@ -181,37 +181,11 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.nested_format}
fullWidth
variant="outlined"
onChange={handleValueChange("nested_format")}
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"
>
<MenuItem value={1}>by Sensor ID</MenuItem>
<MenuItem value={2}>by Number</MenuItem>
</SelectValidator>
<SelectValidator
name="bool_format"
label="Boolean Format"
value={data.bool_format}
fullWidth
variant="outlined"
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>
<MenuItem value={1}>Nested on a single topic</MenuItem>
<MenuItem value={2}>As individual topics</MenuItem>
</SelectValidator>
<SelectValidator
name="subscribe_format"
@@ -219,18 +193,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.subscribe_format}
fullWidth
variant="outlined"
onChange={handleValueChange("subscribe_format")}
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>
<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"
/>
}
@@ -243,12 +217,12 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.ha_climate_format}
fullWidth
variant="outlined"
onChange={handleValueChange("ha_climate_format")}
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>
<MenuItem value={1}>Use Current temperature</MenuItem>
<MenuItem value={2}>Use Setpoint temperature</MenuItem>
<MenuItem value={3}>Always set to 0</MenuItem>
</SelectValidator>
)}
<br></br>
@@ -257,16 +231,16 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
</Typography>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'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)"
@@ -274,21 +248,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
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",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'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)"
@@ -296,21 +270,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
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",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'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)"
@@ -318,21 +292,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
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",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'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)"
@@ -340,21 +314,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
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",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'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)"
@@ -362,21 +336,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
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",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'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)"
@@ -384,7 +358,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.publish_time_other}
type="number"
onChange={handleValueChange("publish_time_other")}
onChange={handleValueChange('publish_time_other')}
margin="normal"
/>
<FormActions>

View File

@@ -1,7 +1,10 @@
import { Theme } from "@material-ui/core";
import { MqttStatus, MqttDisconnectReason } from "./types";
import { Theme } from '@material-ui/core';
import { MqttStatus, MqttDisconnectReason } from './types';
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
export const mqttStatusHighlight = (
{ enabled, connected }: MqttStatus,
theme: Theme
) => {
if (!enabled) {
return theme.palette.info.main;
}
@@ -9,48 +12,48 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
return theme.palette.success.main;
}
return theme.palette.error.main;
}
};
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
if (!enabled) {
return "Not enabled";
return 'Not enabled';
}
if (connected) {
return "Connected";
return 'Connected';
}
return "Disconnected";
}
return 'Disconnected';
};
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
switch (disconnect_reason) {
case MqttDisconnectReason.TCP_DISCONNECTED:
return "TCP disconnected";
return 'TCP disconnected';
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
return "Unacceptable protocol version";
return 'Unacceptable protocol version';
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
return "Client ID rejected";
return 'Client ID rejected';
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
return "Server unavailable";
return 'Server unavailable';
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
return "Malformed credentials";
return 'Malformed credentials';
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
return "Not authorized";
return 'Not authorized';
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
return "Device out of memory";
return 'Device out of memory';
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
return "Server fingerprint invalid";
return 'Server fingerprint invalid';
default:
return "Unknown"
return 'Unknown';
}
}
};
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
export const mqttPublishHighlight = (
{ mqtt_fails }: MqttStatus,
theme: Theme
) => {
if (mqtt_fails === 0) return theme.palette.success.main;
if (mqtt_fails === 0)
return theme.palette.success.main;
if (mqtt_fails < 10)
return theme.palette.warning.main;
if (mqtt_fails < 10) return theme.palette.warning.main;
return theme.palette.error.main;
}
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { MQTT_STATUS_ENDPOINT } from '../api';
import MqttStatusForm from './MqttStatusForm';
@@ -9,7 +14,6 @@ import { MqttStatus } from './types';
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
class MqttStatusController extends Component<MqttStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,10 +23,10 @@ class MqttStatusController extends Component<MqttStatusControllerProps> {
<SectionContent title="MQTT Status">
<RestFormLoader
{...this.props}
render={formProps => <MqttStatusForm {...formProps} />}
render={(formProps) => <MqttStatusForm {...formProps} />}
/>
</SectionContent>
)
);
}
}

View File

@@ -1,23 +1,39 @@
import React, { Component, Fragment } from 'react';
import { Component, Fragment } from 'react';
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@material-ui/core';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import RefreshIcon from '@material-ui/icons/Refresh';
import ReportIcon from '@material-ui/icons/Report';
import SpeakerNotesOffIcon from "@material-ui/icons/SpeakerNotesOff";
import SpeakerNotesOffIcon from '@material-ui/icons/SpeakerNotesOff';
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
import { mqttStatusHighlight, mqttStatus, mqttPublishHighlight, disconnectReason } from './MqttStatus';
import {
RestFormProps,
FormActions,
FormButton,
HighlightAvatar
} from '../components';
import {
mqttStatusHighlight,
mqttStatus,
mqttPublishHighlight,
disconnectReason
} from './MqttStatus';
import { MqttStatus } from './types';
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
class MqttStatusForm extends Component<MqttStatusFormProps> {
renderConnectionStatus() {
const { data, theme } = this.props
const { data, theme } = this.props;
if (data.connected) {
return (
<Fragment>
@@ -50,7 +66,10 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
<ReportIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
<ListItemText
primary="Disconnect Reason"
secondary={disconnectReason(data)}
/>
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
@@ -58,7 +77,7 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
}
createListItems() {
const { data, theme } = this.props
const { data, theme } = this.props;
return (
<Fragment>
<ListItem>
@@ -78,18 +97,20 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
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(MqttStatusForm);

View File

@@ -34,8 +34,6 @@ export interface MqttSettings {
publish_time_mixer: number;
publish_time_other: number;
publish_time_sensor: number;
dallas_format: number;
bool_format: number;
mqtt_qos: number;
mqtt_retain: boolean;
ha_enabled: boolean;

View File

@@ -1,22 +1,31 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
import {
withAuthenticatedContext,
AuthenticatedContextProps,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import NetworkStatusController from './NetworkStatusController';
import NetworkSettingsController from './NetworkSettingsController';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
import {
NetworkConnectionContext,
NetworkConnectionContextValue
} from './NetworkConnectionContext';
import { WiFiNetwork } from './types';
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnectionContextValue> {
class NetworkConnection extends Component<
NetworkConnectionProps,
NetworkConnectionContextValue
> {
constructor(props: NetworkConnectionProps) {
super(props);
this.state = {
@@ -28,13 +37,13 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
selectNetwork = (network: WiFiNetwork) => {
this.setState({ selectedNetwork: network });
this.props.history.push('/network/settings');
}
};
deselectNetwork = () => {
this.setState({ selectedNetwork: undefined });
}
};
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -43,20 +52,44 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
return (
<NetworkConnectionContext.Provider value={this.state}>
<MenuAppBar sectionTitle="Network Connection">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/network/status" label="Network Status" />
<Tab value="/network/scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
<Tab value="/network/settings" label="Network Settings" 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>
<AuthenticatedRoute exact path="/network/status" component={NetworkStatusController} />
<AuthenticatedRoute exact path="/network/scan" component={WiFiNetworkScanner} />
<AuthenticatedRoute exact path="/network/settings" component={NetworkSettingsController} />
<AuthenticatedRoute
exact
path="/network/status"
component={NetworkStatusController}
/>
<AuthenticatedRoute
exact
path="/network/scan"
component={WiFiNetworkScanner}
/>
<AuthenticatedRoute
exact
path="/network/settings"
component={NetworkSettingsController}
/>
<Redirect to="/network/status" />
</Switch>
</MenuAppBar>
</NetworkConnectionContext.Provider>
)
);
}
}

View File

@@ -7,7 +7,7 @@ export interface NetworkConnectionContextValue {
deselectNetwork: () => void;
}
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue;
export const NetworkConnectionContext = React.createContext(
NetworkConnectionContextDefaultValue
);

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import NetworkSettingsForm from './NetworkSettingsForm';
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
import { NetworkSettings } from './types';
@@ -8,7 +13,6 @@ import { NetworkSettings } from './types';
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -18,12 +22,14 @@ class NetworkSettingsController extends Component<NetworkSettingsControllerProps
<SectionContent title="Network Settings">
<RestFormLoader
{...this.props}
render={formProps => <NetworkSettingsForm {...formProps} />}
render={(formProps) => <NetworkSettingsForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(NETWORK_SETTINGS_ENDPOINT, NetworkSettingsController);
export default restController(
NETWORK_SETTINGS_ENDPOINT,
NetworkSettingsController
);

View File

@@ -1,7 +1,14 @@
import React, { Fragment } from 'react';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { Checkbox, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction } from '@material-ui/core';
import {
Checkbox,
List,
ListItem,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction
} from '@material-ui/core';
import Avatar from '@material-ui/core/Avatar';
import IconButton from '@material-ui/core/IconButton';
@@ -10,31 +17,46 @@ import LockOpenIcon from '@material-ui/icons/LockOpen';
import DeleteIcon from '@material-ui/icons/Delete';
import SaveIcon from '@material-ui/icons/Save';
import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
import {
RestFormProps,
PasswordValidator,
BlockFormControlLabel,
FormActions,
FormButton
} from '../components';
import { isIP, isHostname, optional } from '../validators';
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
import {
NetworkConnectionContext,
NetworkConnectionContextValue
} from './NetworkConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
import { NetworkSettings } from './types';
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
static contextType = NetworkConnectionContext;
context!: React.ContextType<typeof NetworkConnectionContext>;
constructor(props: NetworkStatusFormProps, context: NetworkConnectionContextValue) {
constructor(
props: NetworkStatusFormProps,
context: NetworkConnectionContextValue
) {
super(props);
const { selectedNetwork } = context;
if (selectedNetwork) {
const networkSettings: NetworkSettings = {
ssid: selectedNetwork.ssid,
password: "",
password: '',
hostname: props.data.hostname,
static_ip_config: false,
}
enableIPv6: false,
bandwidth20: false,
tx_power: 20,
nosleep: false
};
props.setData(networkSettings);
}
}
@@ -48,7 +70,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
deselectNetworkAndLoadData = () => {
this.context.deselectNetwork();
this.props.loadData();
}
};
componentWillUnmount() {
this.context.deselectNetwork();
@@ -59,27 +81,38 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
const { data, handleValueChange, saveData } = this.props;
return (
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
{
selectedNetwork ?
{selectedNetwork ? (
<List>
<ListItem>
<ListItemAvatar>
<Avatar>
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
{isNetworkOpen(selectedNetwork) ? (
<LockOpenIcon />
) : (
<LockIcon />
)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={selectedNetwork.ssid}
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
secondary={
'Security: ' +
networkSecurityMode(selectedNetwork) +
', Ch: ' +
selectedNetwork.channel
}
/>
<ListItemSecondaryAction>
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
<IconButton
aria-label="Manual Config"
onClick={deselectNetwork}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</List>
:
) : (
<TextValidator
validators={['matchRegexp:^.{0,32}$']}
errorMessages={['SSID must be 32 characters or less']}
@@ -91,9 +124,8 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
onChange={handleValueChange('ssid')}
margin="normal"
/>
}
{
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
)}
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
<PasswordValidator
validators={['matchRegexp:^.{0,64}$']}
errorMessages={['Password must be 64 characters or less']}
@@ -105,10 +137,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
onChange={handleValueChange('password')}
margin="normal"
/>
}
)}
<TextValidator
validators={['required', 'isHostname']}
errorMessages={['Hostname is required', "Not a valid hostname"]}
errorMessages={['Hostname is required', 'Not a valid hostname']}
name="hostname"
label="Hostname"
fullWidth
@@ -117,18 +149,64 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
onChange={handleValueChange('hostname')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:20']}
errorMessages={[
'Tx Power is required',
'Must be a number',
'Must be greater than 0dBm ',
'Max value is 20dBm'
]}
name="tx_power"
label="WiFi Tx Power (dBm)"
fullWidth
variant="outlined"
value={data.tx_power}
type="number"
onChange={handleValueChange('tx_power')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
value="enableIPv6"
checked={data.enableIPv6}
onChange={handleValueChange('enableIPv6')}
/>
}
label="Enable IPv6 support"
/>
<BlockFormControlLabel
control={
<Checkbox
value="bandwidth20"
checked={data.bandwidth20}
onChange={handleValueChange('bandwidth20')}
/>
}
label="Use Lower WiFi Bandwidth"
/>
<BlockFormControlLabel
control={
<Checkbox
value="nosleep"
checked={data.nosleep}
onChange={handleValueChange('nosleep')}
/>
}
label="Disable WiFi Sleep Mode"
/>
<BlockFormControlLabel
control={
<Checkbox
value="static_ip_config"
checked={data.static_ip_config}
onChange={handleValueChange("static_ip_config")}
onChange={handleValueChange('static_ip_config')}
/>
}
label="Static IP Config"
label="Use Static IPs"
/>
{
data.static_ip_config &&
{data.static_ip_config && (
<Fragment>
<TextValidator
validators={['required', 'isIP']}
@@ -154,7 +232,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Subnet mask is required', 'Must be an IP address']}
errorMessages={[
'Subnet mask is required',
'Must be an IP address'
]}
name="subnet_mask"
label="Subnet"
fullWidth
@@ -186,9 +267,14 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
margin="normal"
/>
</Fragment>
}
)}
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -1,5 +1,5 @@
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 (
@@ -36,22 +36,22 @@ export const networkStatusHighlight = (
export const networkStatus = ({ status }: NetworkStatus) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return "Inactive";
return 'Inactive';
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
return "Idle";
return 'Idle';
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return "No SSID Available";
return 'No SSID Available';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
return "Connected (WiFi)";
return 'Connected (WiFi)';
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return "Connected (Ethernet)";
return 'Connected (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return "Connection Failed";
return 'Connection Failed';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return "Connection Lost";
return 'Connection Lost';
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
return "Disconnected";
return 'Disconnected';
default:
return "Unknown";
return 'Unknown';
}
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import NetworkStatusForm from './NetworkStatusForm';
import { NETWORK_STATUS_ENDPOINT } from '../api';
import { NetworkStatus } from './types';
@@ -8,7 +13,6 @@ import { NetworkStatus } from './types';
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -18,12 +22,11 @@ class NetworkStatusController extends Component<NetworkStatusControllerProps> {
<SectionContent title="Network Status">
<RestFormLoader
{...this.props}
render={formProps => <NetworkStatusForm {...formProps} />}
render={(formProps) => <NetworkStatusForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);

View File

@@ -1,46 +1,61 @@
import React, { Component, Fragment } from "react";
import { Component, Fragment } from 'react';
import { WithTheme, withTheme } from "@material-ui/core/styles";
import { WithTheme, withTheme } from '@material-ui/core/styles';
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from "@material-ui/core";
ListItemText
} from '@material-ui/core';
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 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";
HighlightAvatar
} from '../components';
import {
networkStatus,
networkStatusHighlight,
isConnected,
isWiFi,
isEthernet,
} from "./NetworkStatus";
import { NetworkStatus } from "./types";
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 'none';
}
return status.dns_ip_1 + (status.dns_ip_2 ? "," + status.dns_ip_2 : "");
if (!status.dns_ip_2 || status.dns_ip_2 === '0.0.0.0') {
return status.dns_ip_1;
}
return status.dns_ip_1 + ', ' + status.dns_ip_2;
}
IPs(status: NetworkStatus) {
if (
!status.local_ipv6 ||
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000'
) {
return status.local_ip;
}
if (!status.local_ip || status.local_ip === '0.0.0.0') {
return status.local_ipv6;
}
return status.local_ip + ', ' + status.local_ipv6;
}
createListItems() {
@@ -77,7 +92,7 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
<ListItemAvatar>
<Avatar>IP</Avatar>
</ListItemAvatar>
<ListItemText primary="IP Address" secondary={data.local_ip} />
<ListItemText primary="IP Address" secondary={this.IPs(data)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -110,7 +125,7 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
</ListItemAvatar>
<ListItemText
primary="Gateway IP"
secondary={data.gateway_ip || "none"}
secondary={data.gateway_ip || 'none'}
/>
</ListItem>
<Divider variant="inset" component="li" />

View File

@@ -1,7 +1,14 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import { createStyles, WithStyles, Theme, withStyles, Typography, LinearProgress } from '@material-ui/core';
import {
createStyles,
WithStyles,
Theme,
withStyles,
Typography,
LinearProgress
} from '@material-ui/core';
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
import { FormActions, FormButton, SectionContent } from '../components';
@@ -11,9 +18,9 @@ import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
import WiFiNetworkSelector from './WiFiNetworkSelector';
import { WiFiNetworkList, WiFiNetwork } from './types';
const NUM_POLLS = 10
const POLLING_FREQUENCY = 500
const RETRY_EXCEPTION_TYPE = "retry"
const NUM_POLLS = 10;
const POLLING_FREQUENCY = 500;
const RETRY_EXCEPTION_TYPE = 'retry';
interface WiFiNetworkScannerState {
scanningForNetworks: boolean;
@@ -21,28 +28,31 @@ interface WiFiNetworkScannerState {
networkList?: WiFiNetworkList;
}
const styles = (theme: Theme) => createStyles({
const styles = (theme: Theme) =>
createStyles({
scanningSettings: {
margin: theme.spacing(0.5),
margin: theme.spacing(0.5)
},
scanningSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
},
scanningProgress: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
}
});
});
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
pollCount: number = 0;
class WiFiNetworkScanner extends Component<
WiFiNetworkScannerProps,
WiFiNetworkScannerState
> {
pollCount = 0;
state: WiFiNetworkScannerState = {
scanningForNetworks: false,
scanningForNetworks: false
};
componentDidMount() {
@@ -54,22 +64,35 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
if (!scanningForNetworks) {
this.scanNetworks();
}
}
};
scanNetworks() {
this.pollCount = 0;
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
this.setState({
scanningForNetworks: true,
networkList: undefined,
errorMessage: undefined
});
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT)
.then((response) => {
if (response.status === 202) {
this.schedulePollTimeout();
return;
}
throw Error("Scanning for networks returned unexpected response code: " + response.status);
}).catch(error => {
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
variant: 'error',
throw Error(
'Scanning for networks returned unexpected response code: ' +
response.status
);
})
.catch((error) => {
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
variant: 'error'
});
this.setState({
scanningForNetworks: false,
networkList: undefined,
errorMessage: error.message
});
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
});
}
@@ -80,21 +103,20 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
retryError() {
return {
name: RETRY_EXCEPTION_TYPE,
message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
message:
'Network list not ready, will retry in ' + POLLING_FREQUENCY + 'ms.'
};
}
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
if (network1.rssi < network2.rssi)
return 1;
if (network1.rssi > network2.rssi)
return -1;
if (network1.rssi < network2.rssi) return 1;
if (network1.rssi > network2.rssi) return -1;
return 0;
}
pollNetworkList = () => {
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
.then(response => {
.then((response) => {
if (response.status === 200) {
return response.json();
}
@@ -103,24 +125,34 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
this.schedulePollTimeout();
throw this.retryError();
} else {
throw Error("Device did not return network list in timely manner.");
throw Error('Device did not return network list in timely manner.');
}
}
throw Error("Device returned unexpected response code: " + response.status);
throw Error(
'Device returned unexpected response code: ' + response.status
);
})
.then(json => {
json.networks.sort(this.compareNetworks)
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
.then((json) => {
json.networks.sort(this.compareNetworks);
this.setState({
scanningForNetworks: false,
networkList: json,
errorMessage: undefined
});
})
.catch(error => {
.catch((error) => {
if (error.name !== RETRY_EXCEPTION_TYPE) {
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
variant: 'error',
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
variant: 'error'
});
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
}
this.setState({
scanningForNetworks: false,
networkList: undefined,
errorMessage: error.message
});
}
});
};
renderNetworkScanner() {
const { classes } = this.props;
@@ -144,9 +176,7 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
</div>
);
}
return (
<WiFiNetworkSelector networkList={networkList} />
);
return <WiFiNetworkSelector networkList={networkList} />;
}
render() {
@@ -155,14 +185,19 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
<SectionContent title="Network Scanner">
{this.renderNetworkScanner()}
<FormActions>
<FormButton startIcon={<PermScanWifiIcon />} variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}>
<FormButton
startIcon={<PermScanWifiIcon />}
variant="contained"
color="secondary"
onClick={this.requestNetworkScan}
disabled={scanningForNetworks}
>
Scan again&hellip;
</FormButton>
</FormActions>
</SectionContent>
);
}
}
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));

View File

@@ -1,7 +1,13 @@
import React, { Component } from 'react';
import { Avatar, Badge } from '@material-ui/core';
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
import {
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemAvatar
} from '@material-ui/core';
import WifiIcon from '@material-ui/icons/Wifi';
import LockIcon from '@material-ui/icons/Lock';
@@ -16,13 +22,16 @@ interface WiFiNetworkSelectorProps {
}
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
static contextType = NetworkConnectionContext;
context!: React.ContextType<typeof NetworkConnectionContext>;
renderNetwork = (network: WiFiNetwork) => {
return (
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
<ListItem
key={network.bssid}
button
onClick={() => this.context.selectNetwork(network)}
>
<ListItemAvatar>
<Avatar>
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
@@ -30,25 +39,27 @@ class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
</ListItemAvatar>
<ListItemText
primary={network.ssid}
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
secondary={
'Security: ' +
networkSecurityMode(network) +
', Ch: ' +
network.channel
}
/>
<ListItemIcon>
<Badge badgeContent={network.rssi + "db"}>
<Badge badgeContent={network.rssi + 'db'}>
<WifiIcon />
</Badge>
</ListItemIcon>
</ListItem>
);
}
};
render() {
return (
<List>
{this.props.networkList.networks.map(this.renderNetwork)}
</List>
<List>{this.props.networkList.networks.map(this.renderNetwork)}</List>
);
}
}
export default WiFiNetworkSelector;

View File

@@ -1,22 +1,23 @@
import { WiFiNetwork, WiFiEncryptionType } from "./types";
import { WiFiNetwork, WiFiEncryptionType } from './types';
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) => encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
switch (encryption_type) {
case WiFiEncryptionType.WIFI_AUTH_WEP:
return "WEP";
return 'WEP';
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
return "WPA";
return 'WPA';
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
return "WPA2";
return 'WPA2';
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WPA2";
return 'WPA/WPA2';
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
return "WPA2 Enterprise";
return 'WPA2 Enterprise';
case WiFiEncryptionType.WIFI_AUTH_OPEN:
return "None";
return 'None';
default:
return "Unknown";
return 'Unknown';
}
}
};

View File

@@ -21,6 +21,7 @@ export enum WiFiEncryptionType {
export interface NetworkStatus {
status: NetworkConnectionStatus;
local_ip: string;
local_ipv6: string;
mac_address: string;
rssi: number;
ssid: string;
@@ -37,6 +38,10 @@ export interface NetworkSettings {
password: string;
hostname: string;
static_ip_config: boolean;
enableIPv6: boolean;
bandwidth20: boolean;
nosleep: boolean;
tx_power: number;
local_ip?: string;
gateway_ip?: string;
subnet_mask?: string;

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { NTP_SETTINGS_ENDPOINT } from '../api';
import NTPSettingsForm from './NTPSettingsForm';
@@ -9,7 +14,6 @@ import { NTPSettings } from './types';
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class NTPSettingsController extends Component<NTPSettingsControllerProps> {
<SectionContent title="NTP Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <NTPSettingsForm {...formProps} />}
render={(formProps) => <NTPSettingsForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);

View File

@@ -1,10 +1,19 @@
import React from 'react';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import {
TextValidator,
ValidatorForm,
SelectValidator
} from 'react-material-ui-form-validator';
import { Checkbox, MenuItem } from '@material-ui/core';
import SaveIcon from '@material-ui/icons/Save';
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
import {
RestFormProps,
FormActions,
FormButton,
BlockFormControlLabel
} from '../components';
import { isIP, isHostname, or } from '../validators';
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
@@ -13,7 +22,6 @@ import { NTPSettings } from './types';
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
}
@@ -25,7 +33,7 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value]
});
}
};
render() {
const { data, handleValueChange, saveData } = this.props;
@@ -43,7 +51,10 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
/>
<TextValidator
validators={['required', 'isIPOrHostname']}
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
errorMessages={[
'Server is required',
'Not a valid IP address or hostname'
]}
name="server"
label="Server"
fullWidth
@@ -68,7 +79,12 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
{timeZoneSelectItems()}
</SelectValidator>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -1,7 +1,8 @@
import { Theme } from "@material-ui/core";
import { NTPStatus, NTPSyncStatus } from "./types";
import { Theme } from '@material-ui/core';
import { NTPStatus, NTPSyncStatus } from './types';
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
export const isNtpActive = ({ status }: NTPStatus) =>
status === NTPSyncStatus.NTP_ACTIVE;
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
switch (status) {
@@ -12,15 +13,15 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
default:
return theme.palette.error.main;
}
}
};
export const ntpStatus = ({ status }: NTPStatus) => {
switch (status) {
case NTPSyncStatus.NTP_INACTIVE:
return "Inactive";
return 'Inactive';
case NTPSyncStatus.NTP_ACTIVE:
return "Active";
return 'Active';
default:
return "Unknown";
return 'Unknown';
}
}
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { NTP_STATUS_ENDPOINT } from '../api';
import NTPStatusForm from './NTPStatusForm';
@@ -9,7 +14,6 @@ import { NTPStatus } from './types';
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
class NTPStatusController extends Component<NTPStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class NTPStatusController extends Component<NTPStatusControllerProps> {
<SectionContent title="NTP Status">
<RestFormLoader
{...this.props}
render={formProps => <NTPStatusForm {...formProps} />}
render={(formProps) => <NTPStatusForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);

View File

@@ -1,8 +1,23 @@
import React, { Component, Fragment } from 'react';
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
Button
} from '@material-ui/core';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
TextField
} from '@material-ui/core';
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
import AccessTimeIcon from '@material-ui/icons/AccessTime';
@@ -13,12 +28,22 @@ import RefreshIcon from '@material-ui/icons/Refresh';
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
import { formatDuration, formatDateTime, formatLocalDateTime } from './TimeFormat';
import {
formatDuration,
formatDateTime,
formatLocalDateTime
} from './TimeFormat';
import { NTPStatus, Time } from './types';
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import {
redirectingAuthorizedFetch,
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import { TIME_ENDPOINT } from '../api';
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
type NTPStatusFormProps = RestFormProps<NTPStatus> &
WithTheme &
AuthenticatedContextProps;
interface NTPStatusFormState {
settingTime: boolean;
@@ -27,7 +52,6 @@ interface NTPStatusFormState {
}
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
constructor(props: NTPStatusFormProps) {
super(props);
this.state = {
@@ -41,20 +65,20 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
this.setState({
localTime: event.target.value
});
}
};
openSetTime = () => {
this.setState({
localTime: formatLocalDateTime(new Date()),
settingTime: true
});
}
};
closeSetTime = () => {
this.setState({
settingTime: false
});
}
};
createTime = (): Time => ({
local_time: formatLocalDateTime(new Date(this.state.localTime))
@@ -62,27 +86,34 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
configureTime = () => {
this.setState({ processing: true });
redirectingAuthorizedFetch(TIME_ENDPOINT,
{
redirectingAuthorizedFetch(TIME_ENDPOINT, {
method: 'POST',
body: JSON.stringify(this.createTime()),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
this.setState({ processing: false, settingTime: false }, this.props.loadData);
this.props.enqueueSnackbar('Time set successfully', {
variant: 'success'
});
this.setState(
{ processing: false, settingTime: false },
this.props.loadData
);
} else {
throw Error("Error setting time, status code: " + response.status);
throw Error('Error setting time, status code: ' + response.status);
}
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem setting the time',
{ variant: 'error' }
);
this.setState({ processing: false, settingTime: false });
});
}
};
renderSetTimeDialog() {
return (
@@ -94,7 +125,9 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
>
<DialogTitle>Set Time</DialogTitle>
<DialogContent dividers>
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
<Box mb={2}>
Enter local date and time below to set the device's time.
</Box>
<TextField
label="Local Time"
type="datetime-local"
@@ -104,24 +137,35 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
variant="outlined"
fullWidth
InputLabelProps={{
shrink: true,
shrink: true
}}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
<Button
variant="contained"
onClick={this.closeSetTime}
color="secondary"
>
Cancel
</Button>
<Button startIcon={<AccessTimeIcon />} variant="contained" onClick={this.configureTime} disabled={this.state.processing} color="primary" autoFocus>
<Button
startIcon={<AccessTimeIcon />}
variant="contained"
onClick={this.configureTime}
disabled={this.state.processing}
color="primary"
autoFocus
>
Set Time
</Button>
</DialogActions>
</Dialog>
)
);
}
render() {
const { data, theme } = this.props
const { data, theme } = this.props;
const me = this.props.authenticatedContext.me;
return (
<Fragment>
@@ -154,7 +198,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
<AccessTimeIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
<ListItemText
primary="Local Time"
secondary={formatDateTime(data.local_time)}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -163,7 +210,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
<SwapVerticalCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
<ListItemText
primary="UTC Time"
secondary={formatDateTime(data.utc_time)}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -172,19 +222,32 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
<AvTimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Uptime" secondary={formatDuration(data.uptime)} />
<ListItemText
primary="Uptime"
secondary={formatDuration(data.uptime)}
/>
</ListItem>
<Divider variant="inset" component="li" />
</List>
<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>
{me.admin && !isNtpActive(data) && (
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<Button onClick={this.openSetTime} variant="contained" color="primary" startIcon={<AccessTimeIcon />}>
<Button
onClick={this.openSetTime}
variant="contained"
color="primary"
startIcon={<AccessTimeIcon />}
>
Set Time
</Button>
</Box>

View File

@@ -1,9 +1,13 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
import {
withAuthenticatedContext,
AuthenticatedContextProps,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import NTPStatusController from './NTPStatusController';
@@ -12,8 +16,7 @@ import NTPSettingsController from './NTPSettingsController';
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
class NetworkTime extends Component<NetworkTimeProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -21,19 +24,34 @@ class NetworkTime extends Component<NetworkTimeProps> {
const { authenticatedContext } = this.props;
return (
<MenuAppBar sectionTitle="Network Time">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/ntp/status" label="NTP Status" />
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/ntp/settings"
label="NTP Settings"
disabled={!authenticatedContext.me.admin}
/>
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/ntp/status" component={NTPStatusController} />
<AuthenticatedRoute exact path="/ntp/settings" component={NTPSettingsController} />
<AuthenticatedRoute
exact
path="/ntp/status"
component={NTPStatusController}
/>
<AuthenticatedRoute
exact
path="/ntp/settings"
component={NTPSettingsController}
/>
<Redirect to="/ntp/status" />
</Switch>
</MenuAppBar>
)
);
}
}
export default withAuthenticatedContext(NetworkTime)
export default withAuthenticatedContext(NetworkTime);

View File

@@ -1,479 +1,480 @@
import React from 'react';
import MenuItem from '@material-ui/core/MenuItem';
type TimeZones = {
[name: string]: string
[name: string]: string;
};
export const TIME_ZONES: TimeZones = {
"Africa/Abidjan": "GMT0",
"Africa/Accra": "GMT0",
"Africa/Addis_Ababa": "EAT-3",
"Africa/Algiers": "CET-1",
"Africa/Asmara": "EAT-3",
"Africa/Bamako": "GMT0",
"Africa/Bangui": "WAT-1",
"Africa/Banjul": "GMT0",
"Africa/Bissau": "GMT0",
"Africa/Blantyre": "CAT-2",
"Africa/Brazzaville": "WAT-1",
"Africa/Bujumbura": "CAT-2",
"Africa/Cairo": "EET-2",
"Africa/Casablanca": "UNK-1",
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Africa/Conakry": "GMT0",
"Africa/Dakar": "GMT0",
"Africa/Dar_es_Salaam": "EAT-3",
"Africa/Djibouti": "EAT-3",
"Africa/Douala": "WAT-1",
"Africa/El_Aaiun": "UNK-1",
"Africa/Freetown": "GMT0",
"Africa/Gaborone": "CAT-2",
"Africa/Harare": "CAT-2",
"Africa/Johannesburg": "SAST-2",
"Africa/Juba": "EAT-3",
"Africa/Kampala": "EAT-3",
"Africa/Khartoum": "CAT-2",
"Africa/Kigali": "CAT-2",
"Africa/Kinshasa": "WAT-1",
"Africa/Lagos": "WAT-1",
"Africa/Libreville": "WAT-1",
"Africa/Lome": "GMT0",
"Africa/Luanda": "WAT-1",
"Africa/Lubumbashi": "CAT-2",
"Africa/Lusaka": "CAT-2",
"Africa/Malabo": "WAT-1",
"Africa/Maputo": "CAT-2",
"Africa/Maseru": "SAST-2",
"Africa/Mbabane": "SAST-2",
"Africa/Mogadishu": "EAT-3",
"Africa/Monrovia": "GMT0",
"Africa/Nairobi": "EAT-3",
"Africa/Ndjamena": "WAT-1",
"Africa/Niamey": "WAT-1",
"Africa/Nouakchott": "GMT0",
"Africa/Ouagadougou": "GMT0",
"Africa/Porto-Novo": "WAT-1",
"Africa/Sao_Tome": "GMT0",
"Africa/Tripoli": "EET-2",
"Africa/Tunis": "CET-1",
"Africa/Windhoek": "CAT-2",
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Anguilla": "AST4",
"America/Antigua": "AST4",
"America/Araguaina": "UNK3",
"America/Argentina/Buenos_Aires": "UNK3",
"America/Argentina/Catamarca": "UNK3",
"America/Argentina/Cordoba": "UNK3",
"America/Argentina/Jujuy": "UNK3",
"America/Argentina/La_Rioja": "UNK3",
"America/Argentina/Mendoza": "UNK3",
"America/Argentina/Rio_Gallegos": "UNK3",
"America/Argentina/Salta": "UNK3",
"America/Argentina/San_Juan": "UNK3",
"America/Argentina/San_Luis": "UNK3",
"America/Argentina/Tucuman": "UNK3",
"America/Argentina/Ushuaia": "UNK3",
"America/Aruba": "AST4",
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
"America/Atikokan": "EST5",
"America/Bahia": "UNK3",
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
"America/Barbados": "AST4",
"America/Belem": "UNK3",
"America/Belize": "CST6",
"America/Blanc-Sablon": "AST4",
"America/Boa_Vista": "UNK4",
"America/Bogota": "UNK5",
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
"America/Campo_Grande": "UNK4",
"America/Cancun": "EST5",
"America/Caracas": "UNK4",
"America/Cayenne": "UNK3",
"America/Cayman": "EST5",
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
"America/Costa_Rica": "CST6",
"America/Creston": "MST7",
"America/Cuiaba": "UNK4",
"America/Curacao": "AST4",
"America/Danmarkshavn": "GMT0",
"America/Dawson": "MST7",
"America/Dawson_Creek": "MST7",
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
"America/Dominica": "AST4",
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
"America/Eirunepe": "UNK5",
"America/El_Salvador": "CST6",
"America/Fort_Nelson": "MST7",
"America/Fortaleza": "UNK3",
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
"America/Grenada": "AST4",
"America/Guadeloupe": "AST4",
"America/Guatemala": "CST6",
"America/Guayaquil": "UNK5",
"America/Guyana": "UNK4",
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
"America/Hermosillo": "MST7",
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
"America/Jamaica": "EST5",
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
"America/Kralendijk": "AST4",
"America/La_Paz": "UNK4",
"America/Lima": "UNK5",
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
"America/Lower_Princes": "AST4",
"America/Maceio": "UNK3",
"America/Managua": "CST6",
"America/Manaus": "UNK4",
"America/Marigot": "AST4",
"America/Martinique": "AST4",
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
"America/Montevideo": "UNK3",
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
"America/Montserrat": "AST4",
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Noronha": "UNK2",
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
"America/Panama": "EST5",
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
"America/Paramaribo": "UNK3",
"America/Phoenix": "MST7",
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
"America/Port_of_Spain": "AST4",
"America/Porto_Velho": "UNK4",
"America/Puerto_Rico": "AST4",
"America/Punta_Arenas": "UNK3",
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
"America/Recife": "UNK3",
"America/Regina": "CST6",
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
"America/Rio_Branco": "UNK5",
"America/Santarem": "UNK3",
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo": "AST4",
"America/Sao_Paulo": "UNK3",
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy": "AST4",
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
"America/St_Kitts": "AST4",
"America/St_Lucia": "AST4",
"America/St_Thomas": "AST4",
"America/St_Vincent": "AST4",
"America/Swift_Current": "CST6",
"America/Tegucigalpa": "CST6",
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
"America/Tortola": "AST4",
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
"America/Whitehorse": "MST7",
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey": "UNK-8",
"Antarctica/Davis": "UNK-7",
"Antarctica/DumontDUrville": "UNK-10",
"Antarctica/Macquarie": "UNK-11",
"Antarctica/Mawson": "UNK-5",
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Antarctica/Palmer": "UNK3",
"Antarctica/Rothera": "UNK3",
"Antarctica/Syowa": "UNK-3",
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok": "UNK-6",
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden": "UNK-3",
"Asia/Almaty": "UNK-6",
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
"Asia/Anadyr": "UNK-12",
"Asia/Aqtau": "UNK-5",
"Asia/Aqtobe": "UNK-5",
"Asia/Ashgabat": "UNK-5",
"Asia/Atyrau": "UNK-5",
"Asia/Baghdad": "UNK-3",
"Asia/Bahrain": "UNK-3",
"Asia/Baku": "UNK-4",
"Asia/Bangkok": "UNK-7",
"Asia/Barnaul": "UNK-7",
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
"Asia/Bishkek": "UNK-6",
"Asia/Brunei": "UNK-8",
"Asia/Chita": "UNK-9",
"Asia/Choibalsan": "UNK-8",
"Asia/Colombo": "UNK-5:30",
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
"Asia/Dhaka": "UNK-6",
"Asia/Dili": "UNK-9",
"Asia/Dubai": "UNK-4",
"Asia/Dushanbe": "UNK-5",
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Ho_Chi_Minh": "UNK-7",
"Asia/Hong_Kong": "HKT-8",
"Asia/Hovd": "UNK-7",
"Asia/Irkutsk": "UNK-8",
"Asia/Jakarta": "WIB-7",
"Asia/Jayapura": "WIT-9",
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
"Asia/Kabul": "UNK-4:30",
"Asia/Kamchatka": "UNK-12",
"Asia/Karachi": "PKT-5",
"Asia/Kathmandu": "UNK-5:45",
"Asia/Khandyga": "UNK-9",
"Asia/Kolkata": "IST-5:30",
"Asia/Krasnoyarsk": "UNK-7",
"Asia/Kuala_Lumpur": "UNK-8",
"Asia/Kuching": "UNK-8",
"Asia/Kuwait": "UNK-3",
"Asia/Macau": "CST-8",
"Asia/Magadan": "UNK-11",
"Asia/Makassar": "WITA-8",
"Asia/Manila": "PST-8",
"Asia/Muscat": "UNK-4",
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Novokuznetsk": "UNK-7",
"Asia/Novosibirsk": "UNK-7",
"Asia/Omsk": "UNK-6",
"Asia/Oral": "UNK-5",
"Asia/Phnom_Penh": "UNK-7",
"Asia/Pontianak": "WIB-7",
"Asia/Pyongyang": "KST-9",
"Asia/Qatar": "UNK-3",
"Asia/Qyzylorda": "UNK-5",
"Asia/Riyadh": "UNK-3",
"Asia/Sakhalin": "UNK-11",
"Asia/Samarkand": "UNK-5",
"Asia/Seoul": "KST-9",
"Asia/Shanghai": "CST-8",
"Asia/Singapore": "UNK-8",
"Asia/Srednekolymsk": "UNK-11",
"Asia/Taipei": "CST-8",
"Asia/Tashkent": "UNK-5",
"Asia/Tbilisi": "UNK-4",
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
"Asia/Thimphu": "UNK-6",
"Asia/Tokyo": "JST-9",
"Asia/Tomsk": "UNK-7",
"Asia/Ulaanbaatar": "UNK-8",
"Asia/Urumqi": "UNK-6",
"Asia/Ust-Nera": "UNK-10",
"Asia/Vientiane": "UNK-7",
"Asia/Vladivostok": "UNK-10",
"Asia/Yakutsk": "UNK-9",
"Asia/Yangon": "UNK-6:30",
"Asia/Yekaterinburg": "UNK-5",
"Asia/Yerevan": "UNK-4",
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Cape_Verde": "UNK1",
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Reykjavik": "GMT0",
"Atlantic/South_Georgia": "UNK2",
"Atlantic/St_Helena": "GMT0",
"Atlantic/Stanley": "UNK3",
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Brisbane": "AEST-10",
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Darwin": "ACST-9:30",
"Australia/Eucla": "UNK-8:45",
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Lindeman": "AEST-10",
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Perth": "AWST-8",
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Etc/GMT": "GMT0",
"Etc/GMT+0": "GMT0",
"Etc/GMT+1": "UNK1",
"Etc/GMT+10": "UNK10",
"Etc/GMT+11": "UNK11",
"Etc/GMT+12": "UNK12",
"Etc/GMT+2": "UNK2",
"Etc/GMT+3": "UNK3",
"Etc/GMT+4": "UNK4",
"Etc/GMT+5": "UNK5",
"Etc/GMT+6": "UNK6",
"Etc/GMT+7": "UNK7",
"Etc/GMT+8": "UNK8",
"Etc/GMT+9": "UNK9",
"Etc/GMT-0": "GMT0",
"Etc/GMT-1": "UNK-1",
"Etc/GMT-10": "UNK-10",
"Etc/GMT-11": "UNK-11",
"Etc/GMT-12": "UNK-12",
"Etc/GMT-13": "UNK-13",
"Etc/GMT-14": "UNK-14",
"Etc/GMT-2": "UNK-2",
"Etc/GMT-3": "UNK-3",
"Etc/GMT-4": "UNK-4",
"Etc/GMT-5": "UNK-5",
"Etc/GMT-6": "UNK-6",
"Etc/GMT-7": "UNK-7",
"Etc/GMT-8": "UNK-8",
"Etc/GMT-9": "UNK-9",
"Etc/GMT0": "GMT0",
"Etc/Greenwich": "GMT0",
"Etc/UCT": "UTC0",
"Etc/UTC": "UTC0",
"Etc/Universal": "UTC0",
"Etc/Zulu": "UTC0",
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Astrakhan": "UNK-4",
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Istanbul": "UNK-3",
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Kaliningrad": "EET-2",
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Kirov": "UNK-3",
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Minsk": "UNK-3",
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Moscow": "MSK-3",
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Samara": "UNK-4",
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Saratov": "UNK-4",
"Europe/Simferopol": "MSK-3",
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Ulyanovsk": "UNK-4",
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Volgograd": "UNK-4",
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
"Indian/Antananarivo": "EAT-3",
"Indian/Chagos": "UNK-6",
"Indian/Christmas": "UNK-7",
"Indian/Cocos": "UNK-6:30",
"Indian/Comoro": "EAT-3",
"Indian/Kerguelen": "UNK-5",
"Indian/Mahe": "UNK-4",
"Indian/Maldives": "UNK-5",
"Indian/Mauritius": "UNK-4",
"Indian/Mayotte": "EAT-3",
"Indian/Reunion": "UNK-4",
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Pacific/Bougainville": "UNK-11",
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
"Pacific/Chuuk": "UNK-10",
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
"Pacific/Efate": "UNK-11",
"Pacific/Enderbury": "UNK-13",
"Pacific/Fakaofo": "UNK-13",
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
"Pacific/Funafuti": "UNK-12",
"Pacific/Galapagos": "UNK6",
"Pacific/Gambier": "UNK9",
"Pacific/Guadalcanal": "UNK-11",
"Pacific/Guam": "ChST-10",
"Pacific/Honolulu": "HST10",
"Pacific/Kiritimati": "UNK-14",
"Pacific/Kosrae": "UNK-11",
"Pacific/Kwajalein": "UNK-12",
"Pacific/Majuro": "UNK-12",
"Pacific/Marquesas": "UNK9:30",
"Pacific/Midway": "SST11",
"Pacific/Nauru": "UNK-12",
"Pacific/Niue": "UNK11",
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
"Pacific/Noumea": "UNK-11",
"Pacific/Pago_Pago": "SST11",
"Pacific/Palau": "UNK-9",
"Pacific/Pitcairn": "UNK8",
"Pacific/Pohnpei": "UNK-11",
"Pacific/Port_Moresby": "UNK-10",
"Pacific/Rarotonga": "UNK10",
"Pacific/Saipan": "ChST-10",
"Pacific/Tahiti": "UNK10",
"Pacific/Tarawa": "UNK-12",
"Pacific/Tongatapu": "UNK-13",
"Pacific/Wake": "UNK-12",
"Pacific/Wallis": "UNK-12"
}
'Africa/Abidjan': 'GMT0',
'Africa/Accra': 'GMT0',
'Africa/Addis_Ababa': 'EAT-3',
'Africa/Algiers': 'CET-1',
'Africa/Asmara': 'EAT-3',
'Africa/Bamako': 'GMT0',
'Africa/Bangui': 'WAT-1',
'Africa/Banjul': 'GMT0',
'Africa/Bissau': 'GMT0',
'Africa/Blantyre': 'CAT-2',
'Africa/Brazzaville': 'WAT-1',
'Africa/Bujumbura': 'CAT-2',
'Africa/Cairo': 'EET-2',
'Africa/Casablanca': 'UNK-1',
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Africa/Conakry': 'GMT0',
'Africa/Dakar': 'GMT0',
'Africa/Dar_es_Salaam': 'EAT-3',
'Africa/Djibouti': 'EAT-3',
'Africa/Douala': 'WAT-1',
'Africa/El_Aaiun': 'UNK-1',
'Africa/Freetown': 'GMT0',
'Africa/Gaborone': 'CAT-2',
'Africa/Harare': 'CAT-2',
'Africa/Johannesburg': 'SAST-2',
'Africa/Juba': 'EAT-3',
'Africa/Kampala': 'EAT-3',
'Africa/Khartoum': 'CAT-2',
'Africa/Kigali': 'CAT-2',
'Africa/Kinshasa': 'WAT-1',
'Africa/Lagos': 'WAT-1',
'Africa/Libreville': 'WAT-1',
'Africa/Lome': 'GMT0',
'Africa/Luanda': 'WAT-1',
'Africa/Lubumbashi': 'CAT-2',
'Africa/Lusaka': 'CAT-2',
'Africa/Malabo': 'WAT-1',
'Africa/Maputo': 'CAT-2',
'Africa/Maseru': 'SAST-2',
'Africa/Mbabane': 'SAST-2',
'Africa/Mogadishu': 'EAT-3',
'Africa/Monrovia': 'GMT0',
'Africa/Nairobi': 'EAT-3',
'Africa/Ndjamena': 'WAT-1',
'Africa/Niamey': 'WAT-1',
'Africa/Nouakchott': 'GMT0',
'Africa/Ouagadougou': 'GMT0',
'Africa/Porto-Novo': 'WAT-1',
'Africa/Sao_Tome': 'GMT0',
'Africa/Tripoli': 'EET-2',
'Africa/Tunis': 'CET-1',
'Africa/Windhoek': 'CAT-2',
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Anguilla': 'AST4',
'America/Antigua': 'AST4',
'America/Araguaina': 'UNK3',
'America/Argentina/Buenos_Aires': 'UNK3',
'America/Argentina/Catamarca': 'UNK3',
'America/Argentina/Cordoba': 'UNK3',
'America/Argentina/Jujuy': 'UNK3',
'America/Argentina/La_Rioja': 'UNK3',
'America/Argentina/Mendoza': 'UNK3',
'America/Argentina/Rio_Gallegos': 'UNK3',
'America/Argentina/Salta': 'UNK3',
'America/Argentina/San_Juan': 'UNK3',
'America/Argentina/San_Luis': 'UNK3',
'America/Argentina/Tucuman': 'UNK3',
'America/Argentina/Ushuaia': 'UNK3',
'America/Aruba': 'AST4',
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
'America/Atikokan': 'EST5',
'America/Bahia': 'UNK3',
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
'America/Barbados': 'AST4',
'America/Belem': 'UNK3',
'America/Belize': 'CST6',
'America/Blanc-Sablon': 'AST4',
'America/Boa_Vista': 'UNK4',
'America/Bogota': 'UNK5',
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
'America/Campo_Grande': 'UNK4',
'America/Cancun': 'EST5',
'America/Caracas': 'UNK4',
'America/Cayenne': 'UNK3',
'America/Cayman': 'EST5',
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
'America/Costa_Rica': 'CST6',
'America/Creston': 'MST7',
'America/Cuiaba': 'UNK4',
'America/Curacao': 'AST4',
'America/Danmarkshavn': 'GMT0',
'America/Dawson': 'MST7',
'America/Dawson_Creek': 'MST7',
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
'America/Dominica': 'AST4',
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
'America/Eirunepe': 'UNK5',
'America/El_Salvador': 'CST6',
'America/Fort_Nelson': 'MST7',
'America/Fortaleza': 'UNK3',
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
'America/Grenada': 'AST4',
'America/Guadeloupe': 'AST4',
'America/Guatemala': 'CST6',
'America/Guayaquil': 'UNK5',
'America/Guyana': 'UNK4',
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
'America/Hermosillo': 'MST7',
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Knox': 'CST6CDT,M3.2.0,M11.1.0',
'America/Indiana/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Petersburg': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
'America/Indiana/Vevay': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Winamac': 'EST5EDT,M3.2.0,M11.1.0',
'America/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
'America/Jamaica': 'EST5',
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Kentucky/Louisville': 'EST5EDT,M3.2.0,M11.1.0',
'America/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
'America/Kralendijk': 'AST4',
'America/La_Paz': 'UNK4',
'America/Lima': 'UNK5',
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
'America/Lower_Princes': 'AST4',
'America/Maceio': 'UNK3',
'America/Managua': 'CST6',
'America/Manaus': 'UNK4',
'America/Marigot': 'AST4',
'America/Martinique': 'AST4',
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
'America/Moncton': 'AST4ADT,M3.2.0,M11.1.0',
'America/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
'America/Montevideo': 'UNK3',
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
'America/Montserrat': 'AST4',
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
'America/New_York': 'EST5EDT,M3.2.0,M11.1.0',
'America/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Noronha': 'UNK2',
'America/North_Dakota/Beulah': 'CST6CDT,M3.2.0,M11.1.0',
'America/North_Dakota/Center': 'CST6CDT,M3.2.0,M11.1.0',
'America/North_Dakota/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
'America/Panama': 'EST5',
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
'America/Paramaribo': 'UNK3',
'America/Phoenix': 'MST7',
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
'America/Port_of_Spain': 'AST4',
'America/Porto_Velho': 'UNK4',
'America/Puerto_Rico': 'AST4',
'America/Punta_Arenas': 'UNK3',
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
'America/Recife': 'UNK3',
'America/Regina': 'CST6',
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
'America/Rio_Branco': 'UNK5',
'America/Santarem': 'UNK3',
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
'America/Santo_Domingo': 'AST4',
'America/Sao_Paulo': 'UNK3',
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/St_Barthelemy': 'AST4',
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
'America/St_Kitts': 'AST4',
'America/St_Lucia': 'AST4',
'America/St_Thomas': 'AST4',
'America/St_Vincent': 'AST4',
'America/Swift_Current': 'CST6',
'America/Tegucigalpa': 'CST6',
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
'America/Thunder_Bay': 'EST5EDT,M3.2.0,M11.1.0',
'America/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
'America/Tortola': 'AST4',
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
'America/Whitehorse': 'MST7',
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
'Antarctica/Casey': 'UNK-8',
'Antarctica/Davis': 'UNK-7',
'Antarctica/DumontDUrville': 'UNK-10',
'Antarctica/Macquarie': 'UNK-11',
'Antarctica/Mawson': 'UNK-5',
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
'Antarctica/Palmer': 'UNK3',
'Antarctica/Rothera': 'UNK3',
'Antarctica/Syowa': 'UNK-3',
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
'Antarctica/Vostok': 'UNK-6',
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Asia/Aden': 'UNK-3',
'Asia/Almaty': 'UNK-6',
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
'Asia/Anadyr': 'UNK-12',
'Asia/Aqtau': 'UNK-5',
'Asia/Aqtobe': 'UNK-5',
'Asia/Ashgabat': 'UNK-5',
'Asia/Atyrau': 'UNK-5',
'Asia/Baghdad': 'UNK-3',
'Asia/Bahrain': 'UNK-3',
'Asia/Baku': 'UNK-4',
'Asia/Bangkok': 'UNK-7',
'Asia/Barnaul': 'UNK-7',
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
'Asia/Bishkek': 'UNK-6',
'Asia/Brunei': 'UNK-8',
'Asia/Chita': 'UNK-9',
'Asia/Choibalsan': 'UNK-8',
'Asia/Colombo': 'UNK-5:30',
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
'Asia/Dhaka': 'UNK-6',
'Asia/Dili': 'UNK-9',
'Asia/Dubai': 'UNK-4',
'Asia/Dushanbe': 'UNK-5',
'Asia/Famagusta': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Asia/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
'Asia/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
'Asia/Ho_Chi_Minh': 'UNK-7',
'Asia/Hong_Kong': 'HKT-8',
'Asia/Hovd': 'UNK-7',
'Asia/Irkutsk': 'UNK-8',
'Asia/Jakarta': 'WIB-7',
'Asia/Jayapura': 'WIT-9',
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
'Asia/Kabul': 'UNK-4:30',
'Asia/Kamchatka': 'UNK-12',
'Asia/Karachi': 'PKT-5',
'Asia/Kathmandu': 'UNK-5:45',
'Asia/Khandyga': 'UNK-9',
'Asia/Kolkata': 'IST-5:30',
'Asia/Krasnoyarsk': 'UNK-7',
'Asia/Kuala_Lumpur': 'UNK-8',
'Asia/Kuching': 'UNK-8',
'Asia/Kuwait': 'UNK-3',
'Asia/Macau': 'CST-8',
'Asia/Magadan': 'UNK-11',
'Asia/Makassar': 'WITA-8',
'Asia/Manila': 'PST-8',
'Asia/Muscat': 'UNK-4',
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Asia/Novokuznetsk': 'UNK-7',
'Asia/Novosibirsk': 'UNK-7',
'Asia/Omsk': 'UNK-6',
'Asia/Oral': 'UNK-5',
'Asia/Phnom_Penh': 'UNK-7',
'Asia/Pontianak': 'WIB-7',
'Asia/Pyongyang': 'KST-9',
'Asia/Qatar': 'UNK-3',
'Asia/Qyzylorda': 'UNK-5',
'Asia/Riyadh': 'UNK-3',
'Asia/Sakhalin': 'UNK-11',
'Asia/Samarkand': 'UNK-5',
'Asia/Seoul': 'KST-9',
'Asia/Shanghai': 'CST-8',
'Asia/Singapore': 'UNK-8',
'Asia/Srednekolymsk': 'UNK-11',
'Asia/Taipei': 'CST-8',
'Asia/Tashkent': 'UNK-5',
'Asia/Tbilisi': 'UNK-4',
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
'Asia/Thimphu': 'UNK-6',
'Asia/Tokyo': 'JST-9',
'Asia/Tomsk': 'UNK-7',
'Asia/Ulaanbaatar': 'UNK-8',
'Asia/Urumqi': 'UNK-6',
'Asia/Ust-Nera': 'UNK-10',
'Asia/Vientiane': 'UNK-7',
'Asia/Vladivostok': 'UNK-10',
'Asia/Yakutsk': 'UNK-9',
'Asia/Yangon': 'UNK-6:30',
'Asia/Yekaterinburg': 'UNK-5',
'Asia/Yerevan': 'UNK-4',
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Cape_Verde': 'UNK1',
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Reykjavik': 'GMT0',
'Atlantic/South_Georgia': 'UNK2',
'Atlantic/St_Helena': 'GMT0',
'Atlantic/Stanley': 'UNK3',
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
'Australia/Brisbane': 'AEST-10',
'Australia/Broken_Hill': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
'Australia/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Darwin': 'ACST-9:30',
'Australia/Eucla': 'UNK-8:45',
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Lindeman': 'AEST-10',
'Australia/Lord_Howe': 'UNK-10:30UNK-11,M10.1.0,M4.1.0',
'Australia/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Perth': 'AWST-8',
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Etc/GMT': 'GMT0',
'Etc/GMT+0': 'GMT0',
'Etc/GMT+1': 'UNK1',
'Etc/GMT+10': 'UNK10',
'Etc/GMT+11': 'UNK11',
'Etc/GMT+12': 'UNK12',
'Etc/GMT+2': 'UNK2',
'Etc/GMT+3': 'UNK3',
'Etc/GMT+4': 'UNK4',
'Etc/GMT+5': 'UNK5',
'Etc/GMT+6': 'UNK6',
'Etc/GMT+7': 'UNK7',
'Etc/GMT+8': 'UNK8',
'Etc/GMT+9': 'UNK9',
'Etc/GMT-0': 'GMT0',
'Etc/GMT-1': 'UNK-1',
'Etc/GMT-10': 'UNK-10',
'Etc/GMT-11': 'UNK-11',
'Etc/GMT-12': 'UNK-12',
'Etc/GMT-13': 'UNK-13',
'Etc/GMT-14': 'UNK-14',
'Etc/GMT-2': 'UNK-2',
'Etc/GMT-3': 'UNK-3',
'Etc/GMT-4': 'UNK-4',
'Etc/GMT-5': 'UNK-5',
'Etc/GMT-6': 'UNK-6',
'Etc/GMT-7': 'UNK-7',
'Etc/GMT-8': 'UNK-8',
'Etc/GMT-9': 'UNK-9',
'Etc/GMT0': 'GMT0',
'Etc/Greenwich': 'GMT0',
'Etc/UCT': 'UTC0',
'Etc/UTC': 'UTC0',
'Etc/Universal': 'UTC0',
'Etc/Zulu': 'UTC0',
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Andorra': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Astrakhan': 'UNK-4',
'Europe/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Berlin': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Busingen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
'Europe/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Helsinki': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Istanbul': 'UNK-3',
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Kaliningrad': 'EET-2',
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Kirov': 'UNK-3',
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Madrid': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Minsk': 'UNK-3',
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Moscow': 'MSK-3',
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Paris': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Samara': 'UNK-4',
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Sarajevo': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Saratov': 'UNK-4',
'Europe/Simferopol': 'MSK-3',
'Europe/Skopje': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Ulyanovsk': 'UNK-4',
'Europe/Uzhgorod': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vatican': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Volgograd': 'UNK-4',
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Zagreb': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Indian/Antananarivo': 'EAT-3',
'Indian/Chagos': 'UNK-6',
'Indian/Christmas': 'UNK-7',
'Indian/Cocos': 'UNK-6:30',
'Indian/Comoro': 'EAT-3',
'Indian/Kerguelen': 'UNK-5',
'Indian/Mahe': 'UNK-4',
'Indian/Maldives': 'UNK-5',
'Indian/Mauritius': 'UNK-4',
'Indian/Mayotte': 'EAT-3',
'Indian/Reunion': 'UNK-4',
'Pacific/Apia': 'UNK-13UNK,M9.5.0/3,M4.1.0/4',
'Pacific/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
'Pacific/Bougainville': 'UNK-11',
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
'Pacific/Chuuk': 'UNK-10',
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
'Pacific/Efate': 'UNK-11',
'Pacific/Enderbury': 'UNK-13',
'Pacific/Fakaofo': 'UNK-13',
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
'Pacific/Funafuti': 'UNK-12',
'Pacific/Galapagos': 'UNK6',
'Pacific/Gambier': 'UNK9',
'Pacific/Guadalcanal': 'UNK-11',
'Pacific/Guam': 'ChST-10',
'Pacific/Honolulu': 'HST10',
'Pacific/Kiritimati': 'UNK-14',
'Pacific/Kosrae': 'UNK-11',
'Pacific/Kwajalein': 'UNK-12',
'Pacific/Majuro': 'UNK-12',
'Pacific/Marquesas': 'UNK9:30',
'Pacific/Midway': 'SST11',
'Pacific/Nauru': 'UNK-12',
'Pacific/Niue': 'UNK11',
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
'Pacific/Noumea': 'UNK-11',
'Pacific/Pago_Pago': 'SST11',
'Pacific/Palau': 'UNK-9',
'Pacific/Pitcairn': 'UNK8',
'Pacific/Pohnpei': 'UNK-11',
'Pacific/Port_Moresby': 'UNK-10',
'Pacific/Rarotonga': 'UNK10',
'Pacific/Saipan': 'ChST-10',
'Pacific/Tahiti': 'UNK10',
'Pacific/Tarawa': 'UNK-12',
'Pacific/Tongatapu': 'UNK-13',
'Pacific/Wake': 'UNK-12',
'Pacific/Wallis': 'UNK-12'
};
export function selectedTimeZone(label: string, format: string) {
return TIME_ZONES[label] === format ? label : undefined;
}
export function timeZoneSelectItems() {
return Object.keys(TIME_ZONES).map(label => (
<MenuItem key={label} value={label}>{label}</MenuItem>
return Object.keys(TIME_ZONES).map((label) => (
<MenuItem key={label} value={label}>
{label}
</MenuItem>
));
}

View File

@@ -1,8 +1,6 @@
import parseMilliseconds from 'parse-ms';
const LOCALE_FORMAT = new Intl.DateTimeFormat(
[...window.navigator.languages],
{
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
day: 'numeric',
month: 'short',
year: 'numeric',
@@ -10,23 +8,22 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat(
minute: 'numeric',
second: 'numeric',
hour12: false
}
);
});
export const formatDateTime = (dateTime: string) => {
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
}
};
export const formatLocalDateTime = (date: Date) => {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, -1)
.substr(0, 19);
}
};
export const formatDuration = (duration: number) => {
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
var formatted = '';
let formatted = '';
if (days) {
formatted += pluralize(days, 'day');
}
@@ -40,6 +37,7 @@ export const formatDuration = (duration: number) => {
formatted += pluralize(seconds, 'second');
}
return formatted;
}
};
const pluralize = (count: number, noun: string, suffix: string = 's') => ` ${count} ${noun}${count !== 1 ? suffix : ''} `;
const pluralize = (count: number, noun: string, suffix = 's') =>
` ${count} ${noun}${count !== 1 ? suffix : ''} `;

View File

@@ -1,23 +1,24 @@
import React from 'react';
import MenuItem from '@material-ui/core/MenuItem';
type BoardProfiles = {
[name: string]: string
[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)"
}
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>
return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}>
{BOARD_PROFILES[code]}
</MenuItem>
));
}

View File

@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
@@ -8,34 +8,47 @@ import { MenuAppBar } from '../components';
import { AuthenticatedRoute } from '../authentication';
import EMSESPStatusController from './EMSESPStatusController';
import EMSESPDevicesController from './EMSESPDevicesController';
import EMSESPDataController from './EMSESPDataController';
import EMSESPHelp from './EMSESPHelp';
class EMSESP extends Component<RouteComponentProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Dashboard">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tab value={`/${PROJECT_PATH}/devices`} label="Devices & Sensors" />
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value={`/${PROJECT_PATH}/data`} label="Devices &amp; Sensors" />
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/devices`} component={EMSESPDevicesController} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/help`} component={EMSESPHelp} />
<Redirect to={`/${PROJECT_PATH}/devices`} />
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/data`}
component={EMSESPDataController}
/>
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/status`}
component={EMSESPStatusController}
/>
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/help`}
component={EMSESPHelp}
/>
<Redirect to={`/${PROJECT_PATH}/data`} />
</Switch>
</MenuAppBar>
)
);
}
}
export default EMSESP;

View File

@@ -0,0 +1,35 @@
import React, { Component } from 'react';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { ENDPOINT_ROOT } from '../api';
import EMSESPDataForm from './EMSESPDataForm';
import { EMSESPData } from './EMSESPtypes';
export const EMSESP_DATA_ENDPOINT = ENDPOINT_ROOT + 'data';
type EMSESPDataControllerProps = RestControllerProps<EMSESPData>;
class EMSESPDataController extends Component<EMSESPDataControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
<SectionContent title="Devices &amp; Sensors">
<RestFormLoader
{...this.props}
render={(formProps) => <EMSESPDataForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(EMSESP_DATA_ENDPOINT, EMSESPDataController);

View File

@@ -0,0 +1,677 @@
import React, { Component, Fragment } from 'react';
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
import parseMilliseconds from 'parse-ms';
import { Decoder } from '@msgpack/msgpack';
const decoder = new Decoder();
import {
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 { RestFormProps, FormButton, extractEventValue } from '../components';
import {
EMSESPData,
EMSESPDeviceData,
Device,
DeviceValue,
DeviceValueUOM,
DeviceValueUOM_s,
Sensor
} from './EMSESPtypes';
import ValueForm from './ValueForm';
import SensorForm from './SensorForm';
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';
export const WRITE_SENSOR_ENDPOINT = ENDPOINT_ROOT + 'writeSensor';
const StyledTableCell = withStyles((theme: Theme) =>
createStyles({
head: {
backgroundColor: theme.palette.common.black,
color: theme.palette.common.white
},
body: {
fontSize: 14
}
})
)(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;
}
if (a.type > b.type) {
return 1;
}
return 0;
}
interface EMSESPDataFormState {
confirmScanDevices: boolean;
processing: boolean;
deviceData?: EMSESPDeviceData;
selectedDevice?: number;
edit_devicevalue?: DeviceValue;
edit_Sensor?: Sensor;
}
type EMSESPDataFormProps = RestFormProps<EMSESPData> &
AuthenticatedContextProps &
WithWidthProps;
export const formatDuration = (duration_min: number) => {
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
let formatted = '';
if (days) {
formatted += pluralize(days, 'day');
}
if (hours) {
formatted += pluralize(hours, 'hour');
}
if (minutes) {
formatted += pluralize(minutes, 'minute');
}
return formatted;
};
const pluralize = (count: number, noun: string, suffix = 's') =>
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
function formatValue(value: any, uom: number, digit: number) {
switch (uom) {
case DeviceValueUOM.HOURS:
return value ? formatDuration(value * 60) : '0 hours';
case DeviceValueUOM.MINUTES:
return value ? formatDuration(value) : '0 minutes';
case DeviceValueUOM.NONE:
case DeviceValueUOM.LIST:
return value;
case DeviceValueUOM.NUM:
return new Intl.NumberFormat().format(value);
case DeviceValueUOM.BOOLEAN:
return value ? 'on' : 'off';
case DeviceValueUOM.DEGREES:
return (
new Intl.NumberFormat(undefined, {
minimumFractionDigits: digit
}).format(value) +
' ' +
DeviceValueUOM_s[uom]
);
default:
return (
new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]
);
}
}
class EMSESPDataForm extends Component<
EMSESPDataFormProps,
EMSESPDataFormState
> {
state: EMSESPDataFormState = {
confirmScanDevices: false,
processing: false
};
handleDeviceValueChange = (name: keyof DeviceValue) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({
edit_devicevalue: {
...this.state.edit_devicevalue!,
[name]: extractEventValue(event)
}
});
};
cancelEditingDeviceValue = () => {
this.setState({ edit_devicevalue: undefined });
};
doneEditingDeviceValue = () => {
const { edit_devicevalue, selectedDevice } = this.state;
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
method: 'POST',
body: JSON.stringify({
id: selectedDevice,
devicevalue: edit_devicevalue
}),
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar('Write command sent to device', {
variant: 'success'
});
this.handleRowClick(selectedDevice);
} 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 (edit_devicevalue) {
this.setState({ edit_devicevalue: undefined });
}
};
sendCommand = (dv: DeviceValue) => {
this.setState({ edit_devicevalue: dv });
};
handleSensorChange = (name: keyof Sensor) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({
edit_Sensor: {
...this.state.edit_Sensor!,
[name]: extractEventValue(event)
}
});
};
cancelEditingSensor = () => {
this.setState({ edit_Sensor: undefined });
};
doneEditingSensor = () => {
const { edit_Sensor } = this.state;
redirectingAuthorizedFetch(WRITE_SENSOR_ENDPOINT, {
method: 'POST',
body: JSON.stringify({
// because input field with type=number doens't like negative values, force it here
sensor: {
no: edit_Sensor?.no,
id: edit_Sensor?.id,
temp: edit_Sensor?.temp,
offset: Number(edit_Sensor?.offset)
}
}),
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar('Sensor updated', {
variant: 'success'
});
this.props.loadData();
} else if (response.status === 204) {
this.props.enqueueSnackbar('Sensor change 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 (edit_Sensor) {
this.setState({ edit_Sensor: undefined });
}
};
sendSensor = (sn: Sensor) => {
this.setState({ edit_Sensor: sn });
};
noDevices = () => {
return this.props.data.devices.length === 0;
};
noSensors = () => {
return this.props.data.sensors.length === 0;
};
noDeviceData = () => {
return (this.state.deviceData?.data || []).length === 0;
};
renderDeviceItems() {
const { width, data } = this.props;
return (
<TableContainer>
<Typography variant="h6" color="primary">
EMS Devices
</Typography>
<p></p>
{!this.noDevices() && (
<Table
size="small"
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
>
<TableBody>
{data.devices.sort(compareDevices).map((device) => (
<TableRow
hover
key={device.id}
onClick={() => this.handleRowClick(device.id)}
>
<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"
>
{device.type}
</Button>
</CustomTooltip>
</TableCell>
<TableCell align="right">
{device.brand + ' ' + device.name}{' '}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{this.noDevices() && (
<Box
bgcolor="error.main"
color="error.contrastText"
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
No EMS devices found. Check the connections and for possible Tx
errors.
</Typography>
</Box>
)}
</TableContainer>
);
}
renderSensorItems() {
const { data } = this.props;
const me = this.props.authenticatedContext.me;
return (
<TableContainer>
<p></p>
<Typography variant="h6" color="primary" paragraph>
Sensors
</Typography>
{!this.noSensors() && (
<Table size="small" padding="default">
<TableHead>
<TableRow>
<StyledTableCell
padding="checkbox"
style={{ width: 18 }}
></StyledTableCell>
<StyledTableCell>Sensor #</StyledTableCell>
<StyledTableCell align="left">ID / Name</StyledTableCell>
<StyledTableCell align="right">Temperature</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{data.sensors.map((sensorData) => (
<TableRow key={sensorData.no} hover>
<TableCell padding="checkbox" style={{ width: 18 }}>
{me.admin && (
<CustomTooltip title="edit" placement="left-end">
<IconButton
edge="start"
size="small"
aria-label="Edit"
onClick={() => this.sendSensor(sensorData)}
>
<EditIcon color="primary" fontSize="small" />
</IconButton>
</CustomTooltip>
)}
</TableCell>
<TableCell component="th" scope="row">
{sensorData.no}
</TableCell>
<TableCell align="left">{sensorData.id}</TableCell>
<TableCell align="right">
{formatValue(sensorData.temp, DeviceValueUOM.DEGREES, 1)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{this.noSensors() && (
<Box color="warning.main" p={0} mt={0} mb={0}>
<Typography variant="body1">
<i>no connected Dallas temperature sensors were detected</i>
</Typography>
</Box>
)}
</TableContainer>
);
}
renderAnalog() {
const { data } = this.props;
return (
<TableContainer>
{data.analog > 0 && (
<Table size="small" padding="default">
<TableHead>
<TableRow>
<StyledTableCell>Sensortype</StyledTableCell>
<StyledTableCell align="right">Value</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
Analog Input
</TableCell>
<TableCell align="right">
{formatValue(data.analog, DeviceValueUOM.MV, 0)}
</TableCell>
</TableRow>
</TableBody>
</Table>
)}
</TableContainer>
);
}
renderScanDevicesDialog() {
return (
<Dialog
open={this.state.confirmScanDevices}
onClose={this.onScanDevicesRejected}
fullWidth
maxWidth="sm"
>
<DialogTitle>Confirm Scan Devices</DialogTitle>
<DialogContent dividers>
Are you sure you want to start a scan on the EMS bus for all new
devices?
</DialogContent>
<DialogActions>
<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
>
Start Scan
</Button>
</DialogActions>
</Dialog>
);
}
onScanDevices = () => {
this.setState({ confirmScanDevices: true });
};
onScanDevicesRejected = () => {
this.setState({ confirmScanDevices: false });
};
onScanDevicesConfirmed = () => {
this.setState({ processing: true });
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar('Device scan is starting...', {
variant: 'info'
});
this.setState({ processing: false, confirmScanDevices: false });
} else {
throw Error('Invalid status code: ' + response.status);
}
})
.catch((error) => {
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
variant: 'error'
});
this.setState({ processing: false, confirmScanDevices: false });
});
};
handleRowClick = (device: any) => {
this.setState({ selectedDevice: device, deviceData: undefined });
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
method: 'POST',
body: JSON.stringify({ id: device }),
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status === 200) {
return response.arrayBuffer();
}
throw Error('Unexpected response code: ' + response.status);
})
.then((arrayBuffer) => {
const json: any = decoder.decode(arrayBuffer);
this.setState({ deviceData: json });
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem getting device data',
{ variant: 'error' }
);
this.setState({ deviceData: undefined });
});
};
renderDeviceData() {
const { deviceData } = this.state;
const { width } = this.props;
const me = this.props.authenticatedContext.me;
if (this.noDevices()) {
return;
}
if (!deviceData) {
return;
}
return (
<Fragment>
<p></p>
<Box bgcolor="info.main" p={1} mt={1} mb={1}>
<Typography variant="body1" color="initial">
{deviceData.name}
</Typography>
</Box>
{!this.noDeviceData() && (
<TableContainer>
<Table
size="small"
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
>
<TableHead></TableHead>
<TableBody>
{deviceData.data.map((item, i) => (
<TableRow hover key={i}>
<TableCell padding="checkbox" style={{ width: 18 }}>
{item.c && me.admin && (
<CustomTooltip
title="change value"
placement="left-end"
>
<IconButton
edge="start"
size="small"
aria-label="Edit"
onClick={() => this.sendCommand(item)}
>
<EditIcon color="primary" fontSize="small" />
</IconButton>
</CustomTooltip>
)}
</TableCell>
<TableCell padding="none" component="th" scope="row">
{item.n}
</TableCell>
<TableCell padding="none" align="right">
{formatValue(item.v, item.u, 0)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</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>
)}
</Fragment>
);
}
render() {
const { edit_devicevalue, edit_Sensor } = this.state;
return (
<Fragment>
<br></br>
{this.renderDeviceItems()}
{this.renderDeviceData()}
{this.renderSensorItems()}
{this.renderAnalog()}
<br></br>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} padding={1}>
<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"
onClick={this.onScanDevices}
>
Scan Devices
</FormButton>
</Box>
</Box>
{this.renderScanDevicesDialog()}
{edit_devicevalue && (
<ValueForm
devicevalue={edit_devicevalue}
onDoneEditing={this.doneEditingDeviceValue}
onCancelEditing={this.cancelEditingDeviceValue}
handleValueChange={this.handleDeviceValueChange}
/>
)}
{edit_Sensor && (
<SensorForm
sensor={edit_Sensor}
onDoneEditing={this.doneEditingSensor}
onCancelEditing={this.cancelEditingSensor}
handleSensorChange={this.handleSensorChange}
/>
)}
</Fragment>
);
}
}
export default withAuthenticatedContext(withWidth()(EMSESPDataForm));

View File

@@ -1,30 +0,0 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import { ENDPOINT_ROOT } from '../api';
import EMSESPDevicesForm from './EMSESPDevicesForm';
import { EMSESPDevices } from './EMSESPtypes';
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices";
type EMSESPDevicesControllerProps = RestControllerProps<EMSESPDevices>;
class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
<SectionContent title="Devices & Sensors">
<RestFormLoader
{...this.props}
render={formProps => <EMSESPDevicesForm {...formProps} />}
/>
</SectionContent>
)
}
}
export default restController(EMSESP_DEVICES_ENDPOINT, EMSESPDevicesController);

View File

@@ -1,433 +0,0 @@
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
} 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 { RestFormProps, FormButton, extractEventValue } from "../components";
import { EMSESPDevices, EMSESPDeviceData, Device, DeviceValue } 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({
head: {
backgroundColor: theme.palette.common.black,
color: theme.palette.common.white,
},
body: {
fontSize: 14,
},
})
)(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;
}
if (a.type > b.type) {
return 1;
}
return 0;
}
interface EMSESPDevicesFormState {
confirmScanDevices: boolean;
processing: boolean;
deviceData?: EMSESPDeviceData;
selectedDevice?: number;
devicevalue?: DeviceValue;
}
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> & AuthenticatedContextProps & WithWidthProps;
function formatTemp(t: string) {
if (t == null) {
return "n/a";
}
return t + " °C";
}
function formatUnit(u: string) {
if (u == null) {
return u;
}
return " " + u;
}
class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesFormState> {
state: EMSESPDevicesFormState = {
confirmScanDevices: 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;
};
noSensors = () => {
return this.props.data.sensors.length === 0;
};
noDeviceData = () => {
return (this.state.deviceData?.data || []).length === 0;
};
renderDeviceItems() {
const { width, data } = this.props;
return (
<TableContainer>
<Typography variant="h6" color="primary">
EMS Devices
</Typography>
<p></p>
{!this.noDevices() && (
<Table
size="small"
padding={isWidthDown("xs", width!) ? "none" : "default"}
>
<TableBody>
{data.devices.sort(compareDevices).map((device) => (
<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">
{device.type}
</Button>
</CustomTooltip>
</TableCell>
<TableCell align="right">{device.brand + " " + device.name} </TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{this.noDevices() && (
<Box
bgcolor="error.main"
color="error.contrastText"
p={2} mt={2} mb={2}
>
<Typography variant="body1">
No EMS devices found. Check the connections and for possible Tx errors.
</Typography>
</Box>
)}
</TableContainer>
);
}
renderSensorItems() {
const { data } = this.props;
return (
<TableContainer>
<p></p>
<Typography variant="h6" color="primary" paragraph>
Dallas Sensors
</Typography>
{!this.noSensors() && (
<Table size="small" padding="default">
<TableHead>
<TableRow>
<StyledTableCell>Sensor #</StyledTableCell>
<StyledTableCell align="center">ID</StyledTableCell>
<StyledTableCell align="right">Temperature</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{data.sensors.map((sensorData) => (
<TableRow key={sensorData.no}>
<TableCell component="th" scope="row">
{sensorData.no}
</TableCell>
<TableCell align="center">{sensorData.id}</TableCell>
<TableCell align="right">
{formatTemp(sensorData.temp)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{this.noSensors() && (
<Box color="warning.main" p={0} mt={0} mb={0}>
<Typography variant="body1">
<i>no external temperature sensors were detected</i>
</Typography>
</Box>
)}
</TableContainer>
);
}
renderScanDevicesDialog() {
return (
<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?
</DialogContent>
<DialogActions>
<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>
Start Scan
</Button>
</DialogActions>
</Dialog>
);
}
onScanDevices = () => {
this.setState({ confirmScanDevices: true });
};
onScanDevicesRejected = () => {
this.setState({ confirmScanDevices: false });
};
onScanDevicesConfirmed = () => {
this.setState({ processing: true });
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Device scan is starting...", {
variant: "info",
});
this.setState({ processing: false, confirmScanDevices: false });
} else {
throw Error("Invalid status code: " + response.status);
}
})
.catch((error) => {
this.props.enqueueSnackbar(error.message || "Problem with scan", {
variant: "error",
});
this.setState({ processing: false, confirmScanDevices: false });
});
};
handleRowClick = (device: any) => {
this.setState({ selectedDevice: device.id, deviceData: undefined });
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
method: "POST",
body: JSON.stringify({ id: device.id }),
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Unexpected response code: " + response.status);
})
.then((json) => {
this.setState({ deviceData: json });
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || "Problem getting device data",
{ variant: "error" }
);
this.setState({ deviceData: undefined });
});
};
renderDeviceData() {
const { deviceData } = this.state;
const { width } = this.props;
const me = this.props.authenticatedContext.me;
if (this.noDevices()) {
return;
}
if (!deviceData) {
return;
}
return (
<Fragment>
<p></p>
<Box bgcolor="info.main" p={1} mt={1} mb={1}>
<Typography variant="body1" color="initial">
{deviceData.name}
</Typography>
</Box>
{!this.noDeviceData() && (
<TableContainer>
<Table
size="small"
padding={isWidthDown("xs", width!) ? "none" : "default"}
>
<TableHead>
</TableHead>
<TableBody>
{deviceData.data.map((item, i) => {
if (i % 4) {
return null;
} else {
return (
<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>
);
}
})}
</TableBody>
</Table>
</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>
)}
</Fragment >
);
}
render() {
const { devicevalue } = this.state;
return (
<Fragment>
<br></br>
{this.renderDeviceItems()}
{this.renderDeviceData()}
{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} >
Refresh
</FormButton>
</Box>
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<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>
);
}
}
export default withAuthenticatedContext(withWidth()(EMSESPDevicesForm));

View File

@@ -1,31 +1,66 @@
import React, { Component } from 'react';
import { Typography, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@material-ui/core';
import { Component } from 'react';
import {
Typography,
Box,
List,
ListItem,
ListItemText,
Link,
ListItemAvatar
} from '@material-ui/core';
import { SectionContent } from '../components';
import CommentIcon from "@material-ui/icons/CommentTwoTone";
import MenuBookIcon from "@material-ui/icons/MenuBookTwoTone";
import GitHubIcon from "@material-ui/icons/GitHub";
import StarIcon from "@material-ui/icons/Star";
import ImportExportIcon from "@material-ui/icons/ImportExport";
import BugReportIcon from "@material-ui/icons/BugReportTwoTone";
import CommentIcon from '@material-ui/icons/CommentTwoTone';
import MenuBookIcon from '@material-ui/icons/MenuBookTwoTone';
import GitHubIcon from '@material-ui/icons/GitHub';
import StarIcon from '@material-ui/icons/Star';
import DownloadIcon from '@material-ui/icons/GetApp';
export const WebAPISystemSettings = window.location.origin + "/api?device=system&cmd=settings";
export const WebAPISystemInfo = window.location.origin + "/api?device=system&cmd=info";
import { FormButton } from '../components';
import { API_ENDPOINT_ROOT } from '../api';
import { redirectingAuthorizedFetch } from '../authentication';
class EMSESPHelp extends Component {
onDownload = (endpoint: string) => {
redirectingAuthorizedFetch(API_ENDPOINT_ROOT + 'system/' + endpoint)
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error(
'Device returned unexpected response code: ' + response.status
);
})
.then((json) => {
const a = document.createElement('a');
const filename = 'emsesp_system_' + endpoint + '.txt';
a.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
type: 'text/plain'
})
);
a.setAttribute('download', filename);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
};
render() {
return (
<SectionContent title='EMS-ESP Help' titleGutter>
<SectionContent title="EMS-ESP Help" titleGutter>
<List>
<ListItem>
<ListItemAvatar>
<MenuBookIcon />
</ListItemAvatar>
<ListItemText>
For the latest news and updates go to the <Link href="https://emsesp.github.io/docs" color="primary">{'official documentation'}&nbsp;website</Link>
For help and information on the latest updates visit the{' '}
<Link href="https://emsesp.github.io/docs" color="primary">
{'online documentation'}
</Link>
</ListItemText>
</ListItem>
@@ -34,7 +69,10 @@ class EMSESPHelp extends Component {
<CommentIcon />
</ListItemAvatar>
<ListItemText>
For live community chat join our <Link href="https://discord.gg/3J3GgnzpyT" color="primary">{'Discord'}&nbsp;server</Link>
For live community chat join our{' '}
<Link href="https://discord.gg/3J3GgnzpyT" color="primary">
{'Discord'}&nbsp;server
</Link>
</ListItemText>
</ListItem>
@@ -43,43 +81,51 @@ class EMSESPHelp extends Component {
<GitHubIcon />
</ListItemAvatar>
<ListItemText>
To report an issue or feature request go to <Link href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">{'click here'}</Link>
To report an issue or request a feature go to{' '}
<Link
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
color="primary"
>
{'GitHub'}
</Link>
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<ImportExportIcon />
</ListItemAvatar>
<ListItemText>
To list your system settings <Link target="_blank" href={WebAPISystemSettings} color="primary">{'click here'}</Link>
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<BugReportIcon />
</ListItemAvatar>
<ListItemText>
To create a report of the current EMS-ESP status <Link target="_blank" href={WebAPISystemInfo} color="primary">{'click here'}</Link>
</ListItemText>
</ListItem>
</List>
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<FormButton
startIcon={<DownloadIcon />}
variant="contained"
color="primary"
onClick={() => this.onDownload('info')}
>
download system info
</FormButton>
<FormButton
startIcon={<DownloadIcon />}
variant="contained"
color="primary"
onClick={() => this.onDownload('settings')}
>
download all settings
</FormButton>
</Box>
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
<Typography variant="h6">
EMS-ESP is free and open-source.
<br></br>Please consider supporting this project by giving it a <StarIcon style={{ color: '#fdff3a' }} /> on our <Link href="https://github.com/emsesp/EMS-ESP32" color="primary">{'GitHub page'}</Link>.
<br></br>Please consider supporting this project by giving it a{' '}
<StarIcon style={{ color: '#fdff3a' }} /> on our{' '}
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
{'GitHub page'}
</Link>
.
</Typography>
</Box>
<br></br>
</SectionContent>
)
);
}
}
export default EMSESPHelp;

View File

@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
@@ -10,26 +10,31 @@ import { AuthenticatedRoute } from '../authentication';
import EMSESPSettingsController from './EMSESPSettingsController';
class EMSESP extends Component<RouteComponentProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Settings">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/settings`} component={EMSESPSettingsController} />
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/settings`}
component={EMSESPSettingsController}
/>
<Redirect to={`/${PROJECT_PATH}/settings`} />
</Switch>
</MenuAppBar>
)
);
}
}
export default EMSESP;

View File

@@ -1,38 +1,39 @@
import React, { Component } from 'react';
// import { Container } from '@material-ui/core';
import { Component } from 'react';
import { ENDPOINT_ROOT } from '../api';
import EMSESPSettingsForm from './EMSESPSettingsForm';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { EMSESPSettings } from './EMSESPtypes';
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings';
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
// <Container maxWidth="md" disableGutters>
<SectionContent title='' titleGutter>
<SectionContent title="" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => (
<EMSESPSettingsForm {...formProps} />
)}
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
/>
</SectionContent>
// </Container>
)
);
}
}
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
export default restController(
EMSESP_SETTINGS_ENDPOINT,
EMSESPSettingsController
);

View File

@@ -1,9 +1,10 @@
import React from "react";
import { Component } from 'react';
import {
ValidatorForm,
TextValidator,
SelectValidator,
} from "react-material-ui-form-validator";
SelectValidator
} from 'react-material-ui-form-validator';
import {
Checkbox,
@@ -12,33 +13,33 @@ import {
Link,
withWidth,
WithWidthProps,
} from "@material-ui/core";
import SaveIcon from "@material-ui/icons/Save";
import MenuItem from "@material-ui/core/MenuItem";
Grid
} from '@material-ui/core';
import Grid from "@material-ui/core/Grid";
import SaveIcon from '@material-ui/icons/Save';
import MenuItem from '@material-ui/core/MenuItem';
import {
redirectingAuthorizedFetch,
withAuthenticatedContext,
AuthenticatedContextProps,
} from "../authentication";
AuthenticatedContextProps
} from '../authentication';
import {
RestFormProps,
FormActions,
FormButton,
BlockFormControlLabel,
} from "../components";
BlockFormControlLabel
} from '../components';
import { isIP, optional } from "../validators";
import { isIPv4, optional, isHostname, or } from '../validators';
import { EMSESPSettings } from "./EMSESPtypes";
import { EMSESPSettings } from './EMSESPtypes';
import { boardProfileSelectItems } from "./EMSESPBoardProfiles";
import { boardProfileSelectItems } from './EMSESPBoardProfiles';
import { ENDPOINT_ROOT } from "../api";
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
import { ENDPOINT_ROOT } from '../api';
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile';
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
AuthenticatedContextProps &
@@ -48,40 +49,43 @@ interface EMSESPSettingsFormState {
processing: boolean;
}
class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
class EMSESPSettingsForm extends Component<EMSESPSettingsFormProps> {
state: EMSESPSettingsFormState = {
processing: false,
processing: false
};
componentDidMount() {
ValidatorForm.addValidationRule("isOptionalIP", optional(isIP));
ValidatorForm.addValidationRule(
'isOptionalIPorHost',
optional(or(isIPv4, isHostname))
);
}
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { data, setData } = this.props;
setData({
...data,
board_profile: event.target.value,
board_profile: event.target.value
});
if (event.target.value === "CUSTOM") return;
if (event.target.value === 'CUSTOM') return;
this.setState({ processing: true });
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
method: "POST",
method: 'POST',
body: JSON.stringify({ code: event.target.value }),
headers: {
"Content-Type": "application/json",
},
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Unexpected response code: " + response.status);
throw Error('Unexpected response code: ' + response.status);
})
.then((json) => {
this.props.enqueueSnackbar("Profile loaded", { variant: "success" });
this.props.enqueueSnackbar('Profile loaded', { variant: 'success' });
setData({
...data,
led_gpio: json.led_gpio,
@@ -89,14 +93,14 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
rx_gpio: json.rx_gpio,
tx_gpio: json.tx_gpio,
pbutton_gpio: json.pbutton_gpio,
board_profile: event.target.value,
board_profile: event.target.value
});
this.setState({ processing: false });
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || "Problem fetching board profile",
{ variant: "warning" }
error.message || 'Problem fetching board profile',
{ variant: 'warning' }
);
this.setState({ processing: false });
});
@@ -108,15 +112,17 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<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{" "}
<i>
Refer to the
<Link
target="_blank"
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
color="primary"
>
{"online documentation"}
{' documentation'}
</Link>
.
&nbsp;for information on each setting
</i>
</Typography>
</Box>
@@ -139,7 +145,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
value={data.tx_mode}
fullWidth
variant="outlined"
onChange={handleValueChange("tx_mode")}
onChange={handleValueChange('tx_mode')}
margin="normal"
>
<MenuItem value={0}>Off</MenuItem>
@@ -156,7 +162,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
value={data.ems_bus_id}
fullWidth
variant="outlined"
onChange={handleValueChange("ems_bus_id")}
onChange={handleValueChange('ems_bus_id')}
margin="normal"
>
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
@@ -169,16 +175,16 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:120",
'required',
'isNumber',
'minNumber:0',
'maxNumber:120'
]}
errorMessages={[
"Tx delay is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 120",
'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)"
@@ -186,7 +192,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.tx_delay}
type="number"
onChange={handleValueChange("tx_delay")}
onChange={handleValueChange('tx_delay')}
margin="normal"
/>
</Grid>
@@ -201,7 +207,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<Typography variant="body2">
<i>
Select a pre-configured board layout to automatically set the GPIO
pins, or set your own custom configuration
pins. Select "Custom..." to view or manually edit the values.
</i>
</Typography>
</Box>
@@ -216,12 +222,12 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
margin="normal"
>
{boardProfileSelectItems()}
<MenuItem key={"CUSTOM"} value={"CUSTOM"}>
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
Custom...
</MenuItem>
</SelectValidator>
{data.board_profile === "CUSTOM" && (
{data.board_profile === 'CUSTOM' && (
<Grid
container
spacing={1}
@@ -232,18 +238,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<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]*)$",
'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",
'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"
@@ -251,25 +257,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.rx_gpio}
type="number"
onChange={handleValueChange("rx_gpio")}
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]*)$",
'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",
'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"
@@ -277,25 +283,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.tx_gpio}
type="number"
onChange={handleValueChange("tx_gpio")}
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]*)$",
'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",
'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"
@@ -303,25 +309,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.pbutton_gpio}
type="number"
onChange={handleValueChange("pbutton_gpio")}
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]*)$",
'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",
'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)"
@@ -329,25 +335,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.dallas_gpio}
type="number"
onChange={handleValueChange("dallas_gpio")}
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]*)$",
'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",
'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)"
@@ -355,7 +361,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.led_gpio}
type="number"
onChange={handleValueChange("led_gpio")}
onChange={handleValueChange('led_gpio')}
margin="normal"
/>
</Grid>
@@ -364,7 +370,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<br></br>
<Typography variant="h6" color="primary">
Options
General Options
</Typography>
{data.led_gpio !== 0 && (
@@ -372,7 +378,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.hide_led}
onChange={handleValueChange("hide_led")}
onChange={handleValueChange('hide_led')}
value="hide_led"
/>
}
@@ -385,34 +391,44 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.dallas_parasite}
onChange={handleValueChange("dallas_parasite")}
onChange={handleValueChange('dallas_parasite')}
value="dallas_parasite"
/>
}
label="Enable Dallas parasite mode"
label="Use Dallas Sensor parasite power"
/>
)}
<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")}
onChange={handleValueChange('analog_enabled')}
value="analog_enabled"
/>
}
label="Enable ADC"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.low_clock}
onChange={handleValueChange('low_clock')}
value="low_clock"
/>
}
label="Run at a lower CPU clock speed"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.notoken_api}
onChange={handleValueChange('notoken_api')}
value="notoken_api"
/>
}
label="Bypass Access Token authorization on API calls"
/>
<Grid
container
spacing={0}
@@ -424,7 +440,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.shower_timer}
onChange={handleValueChange("shower_timer")}
onChange={handleValueChange('shower_timer')}
value="shower_timer"
/>
}
@@ -434,7 +450,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.shower_alert}
onChange={handleValueChange("shower_alert")}
onChange={handleValueChange('shower_alert')}
value="shower_alert"
/>
}
@@ -442,6 +458,65 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
/>
</Grid>
<br></br>
<Typography variant="h6" color="primary">
Formatting Options
</Typography>
<Grid
container
spacing={1}
direction="row"
justify="flex-start"
alignItems="flex-start"
>
<Grid item xs={4}>
<SelectValidator
name="bool_format"
label="Boolean Format"
value={data.bool_format}
fullWidth
variant="outlined"
onChange={handleValueChange('bool_format')}
margin="normal"
>
<MenuItem value={1}>"on"/"off"</MenuItem>
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
<MenuItem value={3}>true/false</MenuItem>
<MenuItem value={4}>1/0</MenuItem>
</SelectValidator>
</Grid>
<Grid item xs={4}>
<SelectValidator
name="enum_format"
label="Enum Format"
value={data.enum_format}
fullWidth
variant="outlined"
onChange={handleValueChange('enum_format')}
margin="normal"
>
<MenuItem value={1}>Text</MenuItem>
<MenuItem value={2}>Number</MenuItem>
</SelectValidator>
</Grid>
<Grid item xs={4}>
<SelectValidator
name="dallas_format"
label="Dallas Sensor Format"
value={data.dallas_format}
fullWidth
variant="outlined"
onChange={handleValueChange('dallas_format')}
margin="normal"
>
<MenuItem value={1}>ID</MenuItem>
<MenuItem value={2}>Number</MenuItem>
<MenuItem value={3}>Name</MenuItem>
</SelectValidator>
</Grid>
</Grid>
<br></br>
<Typography variant="h6" color="primary">
Syslog
@@ -451,7 +526,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.syslog_enabled}
onChange={handleValueChange("syslog_enabled")}
onChange={handleValueChange('syslog_enabled')}
value="syslog_enabled"
/>
}
@@ -468,30 +543,30 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
>
<Grid item xs={5}>
<TextValidator
validators={["isOptionalIP"]}
errorMessages={["Not a valid IP address"]}
validators={['isOptionalIPorHost']}
errorMessages={['Not a valid IPv4 address or hostname']}
name="syslog_host"
label="IP"
label="Host"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={handleValueChange("syslog_host")}
onChange={handleValueChange('syslog_host')}
margin="normal"
/>
</Grid>
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Port is required",
"Must be a number",
"Must be greater than 0 ",
"Max value is 65535",
'Port is required',
'Must be a number',
'Must be greater than 0 ',
'Max value is 65535'
]}
name="syslog_port"
label="Port"
@@ -499,7 +574,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.syslog_port}
type="number"
onChange={handleValueChange("syslog_port")}
onChange={handleValueChange('syslog_port')}
margin="normal"
/>
</Grid>
@@ -510,7 +585,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
value={data.syslog_level}
fullWidth
variant="outlined"
onChange={handleValueChange("syslog_level")}
onChange={handleValueChange('syslog_level')}
margin="normal"
>
<MenuItem value={-1}>OFF</MenuItem>
@@ -524,24 +599,24 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Syslog Mark is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 10",
'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)"
label="Mark Interval (seconds, 0=off)"
fullWidth
variant="outlined"
value={data.syslog_mark_interval}
type="number"
onChange={handleValueChange("syslog_mark_interval")}
onChange={handleValueChange('syslog_mark_interval')}
margin="normal"
/>
</Grid>
@@ -549,11 +624,11 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.trace_raw}
onChange={handleValueChange("trace_raw")}
onChange={handleValueChange('trace_raw')}
value="trace_raw"
/>
}
label="Output EMS telegrams in raw format"
label="Output EMS telegrams as hexadecimal bytes"
/>
</Grid>
)}

View File

@@ -1,10 +1,10 @@
import { Theme } from '@material-ui/core';
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes';
export const isConnected = ({ status }: EMSESPStatus) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
export const isConnected = ({ status }: EMSESPStatus) =>
status !== busConnectionStatus.BUS_STATUS_OFFLINE;
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main;
@@ -15,26 +15,25 @@ export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
default:
return theme.palette.warning.main;
}
}
};
export const busStatus = ({ status }: EMSESPStatus) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return "Connected";
return 'Connected';
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return "Tx Errors";
return 'Tx Errors';
case busConnectionStatus.BUS_STATUS_OFFLINE:
return "Disconnected";
return 'Disconnected';
default:
return "Unknown";
return 'Unknown';
}
}
};
export const qualityHighlight = ( value: number, theme: Theme) => {
export const qualityHighlight = (value: number, theme: Theme) => {
if (value >= 95) {
return theme.palette.success.main;
}
return theme.palette.error.main;
}
};

View File

@@ -1,29 +1,33 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { ENDPOINT_ROOT } from '../api';
import EMSESPStatusForm from './EMSESPStatusForm';
import { EMSESPStatus } from './EMSESPtypes';
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + "emsespStatus";
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'emsespStatus';
type EMSESPStatusControllerProps = RestControllerProps<EMSESPStatus>;
class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
<SectionContent title="EMS Status">
<SectionContent title="EMS Status" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <EMSESPStatusForm {...formProps} />}
render={(formProps) => <EMSESPStatusForm {...formProps} />}
/>
</SectionContent>
)
);
}
}

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