Merge branch 'origin/dev'

This commit is contained in:
Proddy
2023-02-06 21:58:27 +01:00
348 changed files with 24702 additions and 25402 deletions

View File

@@ -35,6 +35,8 @@ jobs:
run: |
cd interface
npm ci
npx typesafe-i18n --no-watch
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
npm run build
- name: Build firmware

View File

@@ -24,12 +24,14 @@ jobs:
python -m pip install --upgrade pip
pip install -U platformio
platformio upgrade
platformio update
pio pkg update
- name: Build WebUI
run: |
cd interface
npm ci
npx typesafe-i18n --no-watch
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
npm run build
- name: Build firmware

12
.gitignore vendored
View File

@@ -13,7 +13,6 @@ debug.log
# platformio
.pio
pio_local.ini
/.VSCodeCounter
# OS specific
.DS_Store
@@ -31,7 +30,18 @@ test.sh
scripts/__pycache__
.temp
# i18n generated files
interface/src/i18n/i18n-react.tsx
interface/src/i18n/i18n-types.ts
interface/src/i18n/i18n-util.ts
interface/src/i18n/i18n-util.sync.ts
interface/src/i18n/i18n-util.async.ts
# sonar
.scannerwork/
sonar/
build_wrapper_output_directory/
# other build files
dump_entities.csv
dump_entities.xls*

View File

@@ -5,6 +5,68 @@ 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.5.0]
## **IMPORTANT! BREAKING CHANGES**
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
- Support for multiple EMS-ESPs [#759] has been added as an optional setting for MQTT. When enabled, which is now the default, all MQTT Discovery Entity IDs will include the MQTT base name and the shortname of the EMS-ESP device entity. For example what was previously `sensor.boiler_actual_boiler_temperature` will now become `sensor.ems_esp_boiler_boiltemp`. If you still want to use the old format and retain the history and script compatibility in Home Assistant then set this back to the old format.
## Added
- Translations in Web UI and all device entity names (DE, NL, SV, PL, NO, FR) [#22](https://github.com/emsesp/EMS-ESP32/issues/22)
- Add support for Lolin C3 mini [#620](https://github.com/emsesp/EMS-ESP32/pull/620)
- Add support for ESP32-S2 [#667](https://github.com/emsesp/EMS-ESP32/pull/667)
- Add devices: Greenstar 30Ri boiler, Junkers FW500 thermostat, Buderus BC30 controller
- Add program memory info
- Add mqtt queue and connection infos
- Adapt min/max if ems-value is not in this range
- Add heat pump settings for inputs and limits [#600](https://github.com/emsesp/EMS-ESP32/issues/600)
- Add hybrid heatpump [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
- Add translated tags
- Add min/max to customization table [#686](https://github.com/emsesp/EMS-ESP32/issues/686)
- Add MD5 check [#637](https://github.com/emsesp/EMS-ESP32/issues/637)
- Add more bus-ids [#673](https://github.com/emsesp/EMS-ESP32/issues/673)
- Use HA connectivity device class for Status, added boot time [#751](https://github.com/emsesp/EMS-ESP32/issues/751)
- Add commands for analog sensors outputs
- Support for multiple EMS-ESPs with MQTT and HA [[#759](https://github.com/emsesp/EMS-ESP32/issues/759)]
- Settings for heatpump silent mode and additional heater [[#802](https://github.com/emsesp/EMS-ESP32/issues/802)] [[#803](https://github.com/emsesp/EMS-ESP32/issues/803)]
- Zone module MZ100 [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
- Default MQTT hostname is blank [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
- wwCurFlow for ems+ devices [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
- Add Rego 3000, TR120RF thermostats [#917](https://github.com/emsesp/EMS-ESP32/issues/917)
- Add config for ESP32-S3
- Add heatpump silent mode and other entities [#896](https://github.com/emsesp/EMS-ESP32/issues/896)
- Allow reboot to other partition (factory or asymetric OTA)
- Blacklist entities to remove from memory [#891](https://github.com/emsesp/EMS-ESP32/issues/891)
- Add boiler pump operating mode [#944](https://github.com/emsesp/EMS-ESP32/issues/944)
## Fixed
- Factory Reset not working [#628](https://github.com/emsesp/EMS-ESP32/issues/628)
- Valid 4 byte values [#820](https://github.com/emsesp/EMS-ESP32/issues/820)
- Commands for multiple thermostats [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
- API queries for multiple devices [#865](https://github.com/emsesp/EMS-ESP32/issues/865)
- Console crash when using call with command `hcx` only. [#841](https://github.com/emsesp/EMS-ESP32/issues/841)
- `heatingPump2Mod` was wrong, changed to absBurnPow [[#908](https://github.com/emsesp/EMS-ESP32/issues/908)
- Rounding of web input values
- Analog sensor with single gpio number [#915](https://github.com/emsesp/EMS-ESP32/issues/915)
- HA dallas and analog configs: remove/rebuild on change [#888](https://github.com/emsesp/EMS-ESP32/issues/888)
- Modes and set seltemp for RC30 and RC20 [#932](https://github.com/emsesp/EMS-ESP32/issues/932)
## Changed
- Discovery in HomeAssistant don't work with custom base topic. [#596](https://github.com/emsesp/EMS-ESP32/issues/596) Base topic containing `/` are changed to `_`
- RF room temperature sensor are shown as thermostat
- Render mqtt float json values with trailing zero
- Removed flash strings, to increase available heap memory
- Reload page after restart button is pressed
- Analog/dallas values command as list like ems-devices
- Analog/dallas HA-entities based on id
- MQTT Base is a mandatory field. Removed MQTT topic length from settings
- HA duration class for time entities [[#822](https://github.com/emsesp/EMS-ESP32/issues/822)
- AM200 alternative heatsource as class heatsource [[#857](https://github.com/emsesp/EMS-ESP32/issues/857)
# [3.4.4]
## Fixed
@@ -41,7 +103,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Changed
- Shorten "friendly names" in Home Assistant [#555](https://github.com/emsesp/EMS-ESP32/issues/555)
- platformio 2.3.0 (IDF 4, Arduino 2)
- remove master-thermostat, support multiple thermostats
- merge up- and download in webui [#577](https://github.com/emsesp/EMS-ESP32/issues/577)

View File

@@ -1,7 +1,7 @@
#
# GNUMakefile for EMS-ESP
# (c) 2020 Paul Derbyshire
#
NUMJOBS=${NUMJOBS:-" -j4 "}
MAKEFLAGS+="j "
#----------------------------------------------------------------------
@@ -17,23 +17,30 @@ MAKEFLAGS+="j "
#TARGET := $(notdir $(CURDIR))
TARGET := emsesp
BUILD := build
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
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton lib/semver
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/semver lib/* src/devices
LIBRARIES :=
CPPCHECK = cppcheck
# CHECKFLAGS = -q --force --std=c++17
CHECKFLAGS = -q --force --std=c++11
#----------------------------------------------------------------------
# Languages Standard
#----------------------------------------------------------------------
# C_STANDARD := -std=c17
# CXX_STANDARD := -std=c++17
C_STANDARD := -std=c11
CXX_STANDARD := -std=c++11
#----------------------------------------------------------------------
# Defined Symbols
#----------------------------------------------------------------------
DEFINES += -DFACTORY_WIFI_HOSTNAME=\"ems-esp\" -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0 -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_USE_SERIAL -DEMSESP_DEFAULT_BOARD_PROFILE=\"LOLIN\"
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_USE_SERIAL
DEFINES += $(ARGS)
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.5.0b11\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
#----------------------------------------------------------------------
# Sources & Files
@@ -66,7 +73,7 @@ CXX := /usr/bin/g++
# CXXFLAGS C++ Compiler Flags
# LDFLAGS Linker Flags
#----------------------------------------------------------------------
CPPFLAGS += $(DEFINES) $(INCLUDE)
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
CPPFLAGS += -ggdb
CPPFLAGS += -g3
CPPFLAGS += -Os
@@ -114,6 +121,8 @@ COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
# Targets
#----------------------------------------------------------------------
.PHONY: all
.SILENT: $(OUTPUT)
all: $(OUTPUT)
$(OUTPUT): $(OBJS)

131
README.md
View File

@@ -1,17 +1,5 @@
# ![logo](media/EMS-ESP_logo_dark.png)
**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 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)
@@ -20,34 +8,54 @@ This project is the specifically for the ESP32. Compared with the previous ESP82
[![downloads](https://img.shields.io/github/downloads/emsesp/EMS-ESP32/total.svg)](https://github.com/emsesp/EMS-ESP32/releases)
[![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!
[![GitHub stars](https://img.shields.io/github/stars/emsesp/EMS-ESP32.svg?style=social&label=Star)](https://github.com/emsesp/EMS-ESP32/stargazers)
[![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> or contact the contributors that can provide the schematic and designs.
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger. It requires a small gateway circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
<img src="media/gateway-integration.jpg" width=40%>
## **Features**
---
# **Features**
- A multi-user secure web interface to change settings and monitor incoming data
- A multi-user, multi-language secure web interface to change settings and monitor incoming data
- A console, accessible via Serial and Telnet for more advanced monitoring
- Native support for Home Assistant and Domoticz via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
- Native support for Home Assistant, Domoticz and openHAB 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 [100 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
- Support for more than [110 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways, switches, heat sources)
## **Documentation**
For the complete documentation on how to install, configure and get support visit the [EMS-ESP Wiki](https://emsesp.github.io/docs).
## **Support**
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
If you like **EMS-ESP**, please give it a star, or fork it and contribute or offer a small donation!
## **Demo**
See a demo [here](https://ems-esp.derbyshire.nl). Log in with any username/password.
For a live demo of the Web UI click [here](https://ems-esp.derbyshire.nl) and log in with any username/password.
# **Screenshots**
## **Contributors ✨**
## Web Interface
EMS-ESP is a project owned and maintained by [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP).
## **Libraries used**
- [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 all the 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**
This program is licensed under GPL-3.0
## **Screenshots**
### Web Interface
| | |
| ---------------------------------- | -------------------------------- |
@@ -55,75 +63,10 @@ See a demo [here](https://ems-esp.derbyshire.nl). Log in with any username/passw
| <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
### Telnet Console
<img src="media/console.png" width=80% height=80%>
<img src="media/console0.png" width=80% height=80%>
## In Home Assistant
### 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)
# **Support Information**
If you're looking for support on **EMS-ESP** there are some options available:
## 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)
## 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
## 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
# **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
- 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)
# **Libraries used**
- [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**
This program is licensed under GPL-3.0

6
esp32_partition_16M.csv Normal file
View File

@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, , 0x2000,
app0, app, ota_0, , 0x7F0000,
app1, app, ota_1, , 0x7F0000,
spiffs, data, spiffs, , 64K,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0x2000
4 app0 app ota_0 0x7F0000
5 app1 app ota_1 0x7F0000
6 spiffs data spiffs 64K

6
esp32_partition_4M.csv Normal file
View File

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

View File

@@ -1,6 +0,0 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x10000, 0x1F0000,
app1, app, ota_1, 0x200000, 0x1F0000,
spiffs, data, spiffs, 0x3F0000, 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 0x1F0000
5 app1 app ota_1 0x200000 0x1F0000
6 spiffs data spiffs 0x3F0000 0x10000

View File

@@ -7,8 +7,8 @@ build_flags =
; Access point settings
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
-D FACTORY_AP_SSID=\"ems-esp\" ; 1-64 characters
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\" ; 8-64 characters
-D FACTORY_AP_SSID=\"ems-esp\"
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\"
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
@@ -28,11 +28,11 @@ build_flags =
; OTA settings
-D FACTORY_OTA_PORT=8266
-D FACTORY_OTA_PASSWORD=\"ems-esp-neo\"
-D FACTORY_OTA_ENABLED=true
-D FACTORY_OTA_ENABLED=false
; MQTT settings
-D FACTORY_MQTT_ENABLED=false
-D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
-D FACTORY_MQTT_HOST=\"\"
-D FACTORY_MQTT_PORT=1883
-D FACTORY_MQTT_USERNAME=\"\"
-D FACTORY_MQTT_PASSWORD=\"\"

View File

@@ -0,0 +1,5 @@
{
"adapter": "react",
"baseLocale": "pl",
"$schema": "https://unpkg.com/typesafe-i18n@5.24.0/schema/typesafe-i18n.json"
}

15257
interface/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,35 @@
{
"name": "EMS-ESP",
"version": "3.4.0",
"version": "3.5.0",
"private": true,
"proxy": "http://localhost:3080",
"dependencies": {
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@msgpack/msgpack": "^2.8.0",
"@mui/icons-material": "^5.10.3",
"@mui/material": "^5.10.5",
"@table-library/react-table-library": "4.0.18",
"@types/lodash": "^4.14.185",
"@types/node": "^18.7.18",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"@mui/icons-material": "^5.11.0",
"@mui/material": "^5.11.7",
"@table-library/react-table-library": "4.0.24",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.19",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-router-dom": "^5.3.3",
"async-validator": "^4.2.5",
"axios": "^0.27.2",
"http-proxy-middleware": "^2.0.6",
"axios": "^1.3.2",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"notistack": "^2.0.5",
"parse-ms": "^3.0.0",
"notistack": "^2.0.8",
"react": "^18.2.0",
"react-app-rewired": "^2.2.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.2",
"react-icons": "^4.4.0",
"react-router-dom": "^6.4.0",
"react-dropzone": "^14.2.3",
"react-icons": "^4.7.1",
"react-router-dom": "^6.8.1",
"react-scripts": "5.0.1",
"sockette": "^2.0.6",
"typescript": "^4.8.3"
"typesafe-i18n": "^5.24.0",
"typescript": "^4.9.5"
},
"scripts": {
"start": "react-app-rewired start",
@@ -41,8 +40,9 @@
"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",
"standalone": "npm-run-all -p start mock-api",
"lint": "eslint . --ext .ts,.tsx"
"standalone": "npm-run-all -p start typesafe-i18n mock-api",
"lint": "eslint . --ext .ts,.tsx",
"typesafe-i18n": "typesafe-i18n"
},
"eslintConfig": {
"extends": [
@@ -78,7 +78,7 @@
"max-len": [
1,
{
"code": 200
"code": 220
}
],
"arrow-parens": 1
@@ -98,6 +98,7 @@
},
"devDependencies": {
"nodemon": "^2.0.20",
"npm-run-all": "^4.1.5"
"npm-run-all": "^4.1.5",
"http-proxy-middleware": "^2.0.6"
}
}

View File

@@ -10,8 +10,9 @@
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+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+0131, U+0141-0144, U+0152-0153, U+015A-015B, U+0179-017C,
U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}
@font-face {
@@ -19,6 +20,7 @@
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/md.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+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+0131, U+0141-0144, U+0152-0153, U+015A-015B, U+0179-017C,
U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,4 @@
import { FC, createRef, createContext, useContext, RefObject } from 'react';
import { FC, createRef, createContext, useContext, useEffect, useState, RefObject } from 'react';
import { SnackbarProvider } from 'notistack';
import { IconButton } from '@mui/material';
@@ -9,6 +9,13 @@ import { FeaturesLoader } from './contexts/features';
import CustomTheme from './CustomTheme';
import AppRouting from './AppRouting';
import { localStorageDetector } from 'typesafe-i18n/detectors';
import TypesafeI18n from './i18n/i18n-react';
import { detectLocale } from './i18n/i18n-util';
import { loadLocaleAsync } from './i18n/i18n-util.async';
const detectedLocale = detectLocale(localStorageDetector);
const App: FC = () => {
const notistackRef: RefObject<any> = createRef();
@@ -20,8 +27,17 @@ const App: FC = () => {
const colorMode = useContext(ColorModeContext);
const [wasLoaded, setWasLoaded] = useState(false);
useEffect(() => {
loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
}, []);
if (!wasLoaded) return null;
return (
<ColorModeContext.Provider value={colorMode}>
<TypesafeI18n locale={detectedLocale}>
<CustomTheme>
<SnackbarProvider
maxSnack={3}
@@ -38,6 +54,7 @@ const App: FC = () => {
</FeaturesLoader>
</SnackbarProvider>
</CustomTheme>
</TypesafeI18n>
</ColorModeContext.Provider>
);
};

View File

@@ -2,6 +2,8 @@ import { FC, useContext, useEffect } from 'react';
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
import { useSnackbar, VariantType } from 'notistack';
import { useI18nContext } from './i18n/i18n-react';
import { Authentication, AuthenticationContext } from './contexts/authentication';
import { FeaturesContext } from './contexts/features';
import { RequireAuthenticated, RequireUnauthenticated } from './components';
@@ -41,13 +43,14 @@ export const RemoveTrailingSlashes = () => {
const AppRouting: FC = () => {
const { features } = useContext(FeaturesContext);
const { LL } = useI18nContext();
return (
<Authentication>
<RemoveTrailingSlashes />
<Routes>
<Route path="/unauthorized" element={<RootRedirect message="Please sign in to continue" signOut />} />
<Route path="/fileUpdated" element={<RootRedirect message="Upload successful" variant="success" />} />
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
{features.security && (
<Route
path="/"

View File

@@ -2,20 +2,30 @@ import { FC, useContext, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { useSnackbar } from 'notistack';
import { Box, Fab, Paper, Typography } from '@mui/material';
import { Box, Fab, Paper, Typography, Button } from '@mui/material';
import ForwardIcon from '@mui/icons-material/Forward';
import * as AuthenticationApi from './api/authentication';
import { PROJECT_NAME } from './api/env';
import { AuthenticationContext } from './contexts/authentication';
import { AxiosError } from 'axios';
import { extractErrorMessage, onEnterCallback, updateValue } from './utils';
import { SignInRequest } from './types';
import { ValidatedTextField } from './components';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from './validators';
import { I18nContext } from './i18n/i18n-react';
import type { Locales } from './i18n/i18n-types';
import { loadLocaleAsync } from './i18n/i18n-util.async';
import { ReactComponent as NLflag } from './i18n/NL.svg';
import { ReactComponent as DEflag } from './i18n/DE.svg';
import { ReactComponent as GBflag } from './i18n/GB.svg';
import { ReactComponent as SVflag } from './i18n/SV.svg';
import { ReactComponent as PLflag } from './i18n/PL.svg';
import { ReactComponent as NOflag } from './i18n/NO.svg';
import { ReactComponent as FRflag } from './i18n/FR.svg';
const SignIn: FC = () => {
const authenticationContext = useContext(AuthenticationContext);
const { enqueueSnackbar } = useSnackbar();
@@ -31,6 +41,9 @@ const SignIn: FC = () => {
const validateAndSignIn = async () => {
setProcessing(true);
SIGN_IN_REQUEST_VALIDATOR.messages({
required: LL.IS_REQUIRED('%s')
});
try {
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
signIn();
@@ -44,13 +57,13 @@ const SignIn: FC = () => {
try {
const { data: loginResponse } = await AuthenticationApi.signIn(signInRequest);
authenticationContext.signIn(loginResponse.access_token);
} catch (error: unknown) {
if (error instanceof AxiosError) {
} catch (error) {
if (error.response) {
if (error.response?.status === 401) {
enqueueSnackbar('Invalid login details', { variant: 'warning' });
enqueueSnackbar(LL.INVALID_LOGIN(), { variant: 'warning' });
}
} else {
enqueueSnackbar(extractErrorMessage(error, 'Unexpected error, please try again'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.ERROR()), { variant: 'error' });
}
setProcessing(false);
}
@@ -58,6 +71,14 @@ const SignIn: FC = () => {
const submitOnEnter = onEnterCallback(signIn);
const { LL, setLocale, locale } = useContext(I18nContext);
const selectLocale = async (loc: Locales) => {
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
return (
<Box
display="flex"
@@ -81,11 +102,49 @@ const SignIn: FC = () => {
})}
>
<Typography variant="h4">{PROJECT_NAME}</Typography>
<Box
sx={{
'& button, & a, & .MuiCard-root': {
mt: 0.5,
mx: 0.5
}
}}
>
<Button size="small" variant={locale === 'en' ? 'contained' : 'outlined'} onClick={() => selectLocale('en')}>
<GBflag style={{ width: 24 }} />
&nbsp;EN
</Button>
<Button size="small" variant={locale === 'de' ? 'contained' : 'outlined'} onClick={() => selectLocale('de')}>
<DEflag style={{ width: 24 }} />
&nbsp;DE
</Button>
<Button size="small" variant={locale === 'fr' ? 'contained' : 'outlined'} onClick={() => selectLocale('fr')}>
<FRflag style={{ width: 24 }} />
&nbsp;FR
</Button>
<Button size="small" variant={locale === 'nl' ? 'contained' : 'outlined'} onClick={() => selectLocale('nl')}>
<NLflag style={{ width: 24 }} />
&nbsp;NL
</Button>
<Button size="small" variant={locale === 'no' ? 'contained' : 'outlined'} onClick={() => selectLocale('no')}>
<NOflag style={{ width: 24 }} />
&nbsp;NO
</Button>
<Button size="small" variant={locale === 'pl' ? 'contained' : 'outlined'} onClick={() => selectLocale('pl')}>
<PLflag style={{ width: 24 }} />
&nbsp;PL
</Button>
<Button size="small" variant={locale === 'sv' ? 'contained' : 'outlined'} onClick={() => selectLocale('sv')}>
<SVflag style={{ width: 24 }} />
&nbsp;SV
</Button>
</Box>
<ValidatedTextField
fieldErrors={fieldErrors}
disabled={processing}
name="username"
label="Username"
label={LL.USERNAME(0)}
value={signInRequest.username}
onChange={updateLoginRequestValue}
margin="normal"
@@ -97,7 +156,7 @@ const SignIn: FC = () => {
disabled={processing}
type="password"
name="password"
label="Password"
label={LL.PASSWORD()}
value={signInRequest.password}
onChange={updateLoginRequestValue}
onKeyDown={submitOnEnter}
@@ -107,7 +166,7 @@ const SignIn: FC = () => {
/>
<Fab variant="extended" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
<ForwardIcon sx={{ mr: 1 }} />
Sign In
{LL.SIGN_IN()}
</Fab>
</Paper>
</Box>

View File

@@ -1,4 +1,4 @@
import axios, { AxiosPromise, CancelToken } from 'axios';
import axios, { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios';
import { decode } from '@msgpack/msgpack';
@@ -89,7 +89,7 @@ function calculateEventSourceRoot(endpointPath: string) {
export interface FileUploadConfig {
cancelToken?: CancelToken;
onUploadProgress?: (progressEvent: ProgressEvent) => void;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
}
export const startUploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise<void> => {

View File

@@ -11,6 +11,6 @@ export function readMqttSettings(): AxiosPromise<MqttSettings> {
return AXIOS.get('/mqttSettings');
}
export function updateMqttSettings(ntpSettings: MqttSettings): AxiosPromise<MqttSettings> {
return AXIOS.post('/mqttSettings', ntpSettings);
export function updateMqttSettings(mqttSettings: MqttSettings): AxiosPromise<MqttSettings> {
return AXIOS.post('/mqttSettings', mqttSettings);
}

View File

@@ -12,6 +12,10 @@ export function restart(): AxiosPromise<void> {
return AXIOS.post('/restart');
}
export function partition(): AxiosPromise<void> {
return AXIOS.post('/partition');
}
export function factoryReset(): AxiosPromise<void> {
return AXIOS.post('/factoryReset');
}
@@ -38,4 +42,3 @@ export function updateLogSettings(logSettings: LogSettings): AxiosPromise<LogSet
export function readLogEntries(): AxiosPromise<LogEntries> {
return AXIOS_BIN.get('/fetchLog');
}

View File

@@ -1,12 +1,36 @@
import { FC, useState, useContext } from 'react';
import { FC, useState, useContext, ChangeEventHandler } from 'react';
import { Box, Button, Divider, IconButton, Popover, Typography, Avatar, styled, TypographyProps } from '@mui/material';
import {
Box,
Button,
Divider,
IconButton,
Popover,
Typography,
Avatar,
styled,
TypographyProps,
MenuItem,
TextField
} from '@mui/material';
import PersonIcon from '@mui/icons-material/Person';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import { AuthenticatedContext } from '../../contexts/authentication';
import { I18nContext } from '../../i18n/i18n-react';
import type { Locales } from '../../i18n/i18n-types';
import { loadLocaleAsync } from '../../i18n/i18n-util.async';
import { ReactComponent as NLflag } from '../../i18n/NL.svg';
import { ReactComponent as DEflag } from '../../i18n/DE.svg';
import { ReactComponent as GBflag } from '../../i18n/GB.svg';
import { ReactComponent as SVflag } from '../../i18n/SV.svg';
import { ReactComponent as PLflag } from '../../i18n/PL.svg';
import { ReactComponent as NOflag } from '../../i18n/NO.svg';
import { ReactComponent as FRflag } from '../../i18n/FR.svg';
const ItemTypography = styled(Typography)<TypographyProps>({
maxWidth: '250px',
whiteSpace: 'nowrap',
@@ -23,6 +47,15 @@ const LayoutAuthMenu: FC = () => {
setAnchorEl(event.currentTarget);
};
const { locale, LL, setLocale } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
const handleClose = () => {
setAnchorEl(null);
};
@@ -32,7 +65,53 @@ const LayoutAuthMenu: FC = () => {
return (
<>
<IconButton id="open-auth-menu" sx={{ padding: 0 }} aria-describedby={id} color="inherit" onClick={handleClick}>
<TextField
name="locale"
InputProps={{ style: { fontSize: 10 } }}
variant="outlined"
value={locale}
onChange={onLocaleSelected}
size="small"
select
>
<MenuItem key="en" value="en">
<GBflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;EN
</MenuItem>
<Divider />
<MenuItem key="de" value="de">
<DEflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;DE
</MenuItem>
<MenuItem key="fr" value="fr">
<FRflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;FR
</MenuItem>
<MenuItem key="nl" value="nl">
<NLflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NL
</MenuItem>
<MenuItem key="no" value="no">
<NOflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NO
</MenuItem>
<MenuItem key="pl" value="pl">
<PLflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;PL
</MenuItem>
<MenuItem key="sv" value="sv">
<SVflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SV
</MenuItem>
</TextField>
<IconButton
id="open-auth-menu"
sx={{ ml: 1, padding: 0 }}
aria-describedby={id}
color="inherit"
onClick={handleClick}
>
<AccountCircleIcon />
</IconButton>
<Popover
@@ -56,13 +135,15 @@ const LayoutAuthMenu: FC = () => {
</Avatar>
<Box pl={2}>
<ItemTypography variant="h6">{me.username}</ItemTypography>
<ItemTypography variant="body1">{me.admin ? 'Admin User' : 'Guest User'}</ItemTypography>
<ItemTypography variant="body1">
{me.admin ? LL.ADMIN() : LL.GUEST()}&nbsp;{LL.USER(2)}
</ItemTypography>
</Box>
</Box>
<Divider />
<Box p={1.5}>
<Button variant="outlined" fullWidth color="primary" onClick={() => signOut(true)}>
Sign Out
{LL.SIGN_OUT()}
</Button>
</Box>
</Popover>

View File

@@ -15,9 +15,12 @@ import ProjectMenu from '../../project/ProjectMenu';
import LayoutMenuItem from './LayoutMenuItem';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
const LayoutMenu: FC = () => {
const { features } = useContext(FeaturesContext);
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
return (
<>
@@ -28,12 +31,17 @@ const LayoutMenu: FC = () => {
</List>
)}
<List disablePadding component="nav">
<LayoutMenuItem icon={SettingsEthernetIcon} label="Network Connection" to="/network" />
<LayoutMenuItem icon={SettingsInputAntennaIcon} label="Access Point" to="/ap" />
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="Network Time" to="/ntp" />}
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />}
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
<LayoutMenuItem icon={LockIcon} label="Security" to="/security" disabled={!authenticatedContext.me.admin} />
<LayoutMenuItem icon={SettingsIcon} label="System" to="/system" />
<LayoutMenuItem
icon={LockIcon}
label={LL.SECURITY(0)}
to="/security"
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={SettingsIcon} label={LL.SYSTEM(0)} to="/system" />
</List>
</>
);

View File

@@ -5,6 +5,8 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import { MessageBox } from '..';
import { useI18nContext } from '../../i18n/i18n-react';
interface FormLoaderProps {
message?: string;
errorMessage?: string;
@@ -12,12 +14,14 @@ interface FormLoaderProps {
}
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, onRetry, message = 'Loading…' }) => {
const { LL } = useI18nContext();
if (errorMessage) {
return (
<MessageBox my={2} level="error" message={errorMessage}>
{onRetry && (
<Button startIcon={<RefreshIcon />} variant="contained" color="error" onClick={onRetry}>
Retry
{LL.RETRY()}
</Button>
)}
</MessageBox>

View File

@@ -2,11 +2,16 @@ import { FC } from 'react';
import { CircularProgress, Box, Typography, Theme } from '@mui/material';
import { useI18nContext } from '../../i18n/i18n-react';
interface LoadingSpinnerProps {
height?: number | string;
}
const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => (
const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => {
const { LL } = useI18nContext();
return (
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column" padding={2} height={height}>
<CircularProgress
sx={(theme: Theme) => ({
@@ -16,9 +21,10 @@ const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => (
size={100}
/>
<Typography variant="h4" color="textSecondary">
Loading&hellip;
{LL.LOADING()}&hellip;
</Typography>
</Box>
);
);
};
export default LoadingSpinner;

View File

@@ -1,12 +1,14 @@
import { FC, Fragment } from 'react';
import { useDropzone, DropzoneState } from 'react-dropzone';
import { AxiosProgressEvent } from 'axios';
import { Box, Button, LinearProgress, Theme, Typography, useTheme } from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CancelIcon from '@mui/icons-material/Cancel';
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
import { useI18nContext } from '../../i18n/i18n-react';
const getBorderColor = (theme: Theme, props: DropzoneState) => {
if (props.isDragAccept) {
@@ -25,7 +27,7 @@ export interface SingleUploadProps {
onDrop: (acceptedFiles: File[]) => void;
onCancel: () => void;
uploading: boolean;
progress?: ProgressEvent;
progress?: AxiosProgressEvent;
}
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, progress }) => {
@@ -33,7 +35,8 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
onDrop,
accept: {
'application/octet-stream': ['.bin'],
'application/json': ['.json']
'application/json': ['.json'],
'text/plain': ['.md5']
},
disabled: uploading,
multiple: false
@@ -41,14 +44,16 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
const { getRootProps, getInputProps } = dropzoneState;
const theme = useTheme();
const { LL } = useI18nContext();
const progressText = () => {
if (uploading) {
if (progress?.lengthComputable) {
return `Uploading: ${progressPercentage(progress)}%`;
if (progress?.total) {
return LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%';
}
return 'Uploading\u2026';
return LL.UPLOADING() + `\u2026`;
}
return 'Drop file or click here';
return LL.UPLOAD_DROP_TEXT();
};
return (
@@ -60,7 +65,7 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
borderWidth: 2,
borderRadius: 2,
borderStyle: 'dashed',
color: theme.palette.grey[700],
color: theme.palette.grey[400],
transition: 'border .24s ease-in-out',
width: '100%',
cursor: uploading ? 'default' : 'pointer',
@@ -76,12 +81,12 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
<Fragment>
<Box width="100%" p={2}>
<LinearProgress
variant={!progress || progress.lengthComputable ? 'determinate' : 'indeterminate'}
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
variant={!progress || progress.total ? 'determinate' : 'indeterminate'}
value={!progress ? 0 : progress.total ? Math.round((progress.loaded * 100) / progress.total) : 0}
/>
</Box>
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>
Cancel
{LL.CANCEL()}
</Button>
</Fragment>
)}

View File

@@ -1,24 +1,30 @@
import { useCallback, useEffect, useState } from 'react';
import axios, { AxiosPromise, CancelTokenSource } from 'axios';
import axios, { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios';
import { useSnackbar } from 'notistack';
import { extractErrorMessage } from '../../utils';
import { FileUploadConfig } from '../../api/endpoints';
import { useI18nContext } from '../../i18n/i18n-react';
interface MediaUploadOptions {
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
}
const useFileUpload = ({ upload }: MediaUploadOptions) => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [uploading, setUploading] = useState<boolean>(false);
const [uploadProgress, setUploadProgress] = useState<ProgressEvent>();
const [md5, setMd5] = useState<string>('');
const [uploadProgress, setUploadProgress] = useState<AxiosProgressEvent>();
const [uploadCancelToken, setUploadCancelToken] = useState<CancelTokenSource>();
const resetUploadingStates = () => {
setUploading(false);
setUploadProgress(undefined);
setUploadCancelToken(undefined);
setMd5('');
};
const cancelUpload = useCallback(() => {
@@ -37,23 +43,28 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => {
const cancelToken = axios.CancelToken.source();
setUploadCancelToken(cancelToken);
setUploading(true);
await upload(images[0], {
const response = await upload(images[0], {
onUploadProgress: setUploadProgress,
cancelToken: cancelToken.token
});
resetUploadingStates();
enqueueSnackbar('File uploaded', { variant: 'success' });
} catch (error: unknown) {
if (response.status === 200) {
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.SUCCESSFUL(), { variant: 'success' });
} else if (response.status === 201) {
setMd5(String(response.data));
enqueueSnackbar(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL(), { variant: 'success' });
}
} catch (error) {
if (axios.isCancel(error)) {
enqueueSnackbar('Upload aborted', { variant: 'warning' });
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.ABORTED(), { variant: 'warning' });
} else {
resetUploadingStates();
enqueueSnackbar(extractErrorMessage(error, 'Upload failed'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED()), { variant: 'error' });
}
}
};
return [uploadFile, cancelUpload, uploading, uploadProgress] as const;
return [uploadFile, cancelUpload, uploading, uploadProgress, md5] as const;
};
export default useFileUpload;

View File

@@ -2,6 +2,8 @@ import { FC, useCallback, useContext, useEffect, useState } from 'react';
import { useSnackbar } from 'notistack';
import { useNavigate } from 'react-router-dom';
import { useI18nContext } from '../../i18n/i18n-react';
import * as AuthenticationApi from '../../api/authentication';
import { ACCESS_TOKEN } from '../../api/endpoints';
import { RequiredChildrenProps } from '../../utils';
@@ -12,6 +14,8 @@ import { AuthenticationContext } from './context';
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const { features } = useContext(FeaturesContext);
const { LL } = useI18nContext();
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
@@ -23,8 +27,8 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
setMe(decodedMe);
enqueueSnackbar(`Logged in as ${decodedMe.username}`, { variant: 'success' });
} catch (error: unknown) {
enqueueSnackbar(LL.LOGGED_IN({ name: decodedMe.username }), { variant: 'success' });
} catch (error) {
setMe(undefined);
throw new Error('Failed to parse JWT');
}
@@ -50,7 +54,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
await AuthenticationApi.verifyAuthorization();
setMe(AuthenticationApi.decodeMeJWT(accessToken));
setInitialized(true);
} catch (error: unknown) {
} catch (error) {
setMe(undefined);
setInitialized(true);
}

View File

@@ -16,7 +16,7 @@ const FeaturesLoader: FC<RequiredChildrenProps> = (props) => {
try {
const response = await FeaturesApi.readFeatures();
setFeatures(response.data);
} catch (error: unknown) {
} catch (error) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch application details.'));
}
}, []);

View File

@@ -19,6 +19,8 @@ import { APProvisionMode, APSettings } from '../../types';
import { numberValue, updateValue, useRest } from '../../utils';
import * as APApi from '../../api/ap';
import { useI18nContext } from '../../i18n/i18n-react';
export const isAPEnabled = ({ provision_mode }: APSettings) => {
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
};
@@ -29,6 +31,8 @@ const APSettingsForm: FC = () => {
update: APApi.updateAPSettings
});
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValue(setData);
@@ -53,7 +57,7 @@ const APSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="provision_mode"
label="Provide Access Point&hellip;"
label={LL.AP_PROVIDE() + '...'}
value={data.provision_mode}
fullWidth
select
@@ -61,16 +65,16 @@ const APSettingsForm: FC = () => {
onChange={updateFormValue}
margin="normal"
>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>{LL.AP_PROVIDE_TEXT_1()}</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>{LL.AP_PROVIDE_TEXT_2()}</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>{LL.AP_PROVIDE_TEXT_3()}</MenuItem>
</ValidatedTextField>
{isAPEnabled(data) && (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="ssid"
label="Access Point SSID"
label={LL.ACCESS_POINT(2) + ' SSID'}
fullWidth
variant="outlined"
value={data.ssid}
@@ -80,7 +84,7 @@ const APSettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Access Point Password"
label={LL.ACCESS_POINT(2) + ' ' + LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -90,7 +94,7 @@ const APSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="channel"
label="Preferred Channel"
label={LL.AP_PREFERRED_CHANNEL()}
value={numberValue(data.channel)}
fullWidth
select
@@ -107,12 +111,12 @@ const APSettingsForm: FC = () => {
</ValidatedTextField>
<BlockFormControlLabel
control={<Checkbox name="ssid_hidden" checked={data.ssid_hidden} onChange={updateFormValue} />}
label="Hide SSID"
label={LL.AP_HIDE_SSID()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="max_clients"
label="Max Clients"
label={LL.AP_MAX_CLIENTS()}
value={numberValue(data.max_clients)}
fullWidth
select
@@ -130,7 +134,7 @@ const APSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="local_ip"
label="Local IP"
label={LL.AP_LOCAL_IP()}
fullWidth
variant="outlined"
value={data.local_ip}
@@ -140,7 +144,7 @@ const APSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="gateway_ip"
label="Gateway"
label={LL.NETWORK_GATEWAY()}
fullWidth
variant="outlined"
value={data.gateway_ip}
@@ -150,7 +154,7 @@ const APSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="subnet_mask"
label="Subnet"
label={LL.NETWORK_SUBNET()}
fullWidth
variant="outlined"
value={data.subnet_mask}
@@ -168,7 +172,7 @@ const APSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -176,7 +180,7 @@ const APSettingsForm: FC = () => {
};
return (
<SectionContent title="Access Point Settings" titleGutter>
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -11,6 +11,8 @@ import { APNetworkStatus, APStatus } from '../../types';
import { ButtonRow, FormLoader, SectionContent } from '../../components';
import { useRest } from '../../utils';
import { useI18nContext } from '../../i18n/i18n-react';
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
switch (status) {
case APNetworkStatus.ACTIVE:
@@ -24,24 +26,26 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
}
};
export const apStatus = ({ status }: APStatus) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return 'Active';
case APNetworkStatus.INACTIVE:
return 'Inactive';
case APNetworkStatus.LINGERING:
return 'Lingering until idle';
default:
return 'Unknown';
}
};
const APStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const apStatus = ({ status }: APStatus) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return LL.ACTIVE();
case APNetworkStatus.INACTIVE:
return LL.INACTIVE(0);
case APNetworkStatus.LINGERING:
return 'Lingering until idle';
default:
return LL.UNKNOWN();
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -56,14 +60,14 @@ const APStatusForm: FC = () => {
<SettingsInputAntennaIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={apStatus(data)} />
<ListItemText primary={LL.STATUS_OF('')} secondary={apStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>IP</Avatar>
</ListItemAvatar>
<ListItemText primary="IP Address" secondary={data.ip_address} />
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={data.ip_address} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -72,7 +76,7 @@ const APStatusForm: FC = () => {
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="MAC Address" secondary={data.mac_address} />
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -81,13 +85,13 @@ const APStatusForm: FC = () => {
<ComputerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="AP Clients" secondary={data.station_num} />
<ListItemText primary={LL.AP_CLIENTS()} secondary={data.station_num} />
</ListItem>
<Divider variant="inset" component="li" />
</List>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
@@ -95,7 +99,7 @@ const APStatusForm: FC = () => {
};
return (
<SectionContent title="Access Point Status" titleGutter>
<SectionContent title={LL.STATUS_OF(LL.ACCESS_POINT(1))} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -8,8 +8,12 @@ import APStatusForm from './APStatusForm';
import APSettingsForm from './APSettingsForm';
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
import { useI18nContext } from '../../i18n/i18n-react';
const AccessPoint: FC = () => {
useLayoutTitle('Access Point');
const { LL } = useI18nContext();
useLayoutTitle(LL.ACCESS_POINT(0));
const authenticatedContext = useContext(AuthenticatedContext);
@@ -18,8 +22,8 @@ const AccessPoint: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="Access Point Status" />
<Tab value="settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} />
<Tab value="settings" label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<APStatusForm />} />

View File

@@ -9,7 +9,11 @@ import { AuthenticatedContext } from '../../contexts/authentication';
import MqttStatusForm from './MqttStatusForm';
import MqttSettingsForm from './MqttSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const Mqtt: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle('MQTT');
const authenticatedContext = useContext(AuthenticatedContext);
@@ -18,8 +22,8 @@ const Mqtt: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="MQTT Status" />
<Tab value="settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={LL.STATUS_OF('MQTT')} />
<Tab value="settings" label={LL.SETTINGS_OF('MQTT')} disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<MqttStatusForm />} />

View File

@@ -1,10 +1,10 @@
import { FC, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { Button, Checkbox, MenuItem, Grid, Typography } from '@mui/material';
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import { MQTT_SETTINGS_VALIDATOR, validate } from '../../validators';
import { createMqttSettingsValidator, validate } from '../../validators';
import {
BlockFormControlLabel,
ButtonRow,
@@ -17,12 +17,16 @@ import { MqttSettings } from '../../types';
import { numberValue, updateValue, useRest } from '../../utils';
import * as MqttApi from '../../api/mqtt';
import { useI18nContext } from '../../i18n/i18n-react';
const MqttSettingsForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<MqttSettings>({
read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings
});
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValue(setData);
@@ -35,7 +39,7 @@ const MqttSettingsForm: FC = () => {
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(MQTT_SETTINGS_VALIDATOR, data);
await validate(createMqttSettingsValidator(data), data);
saveData();
} catch (errors: any) {
setFieldErrors(errors);
@@ -46,14 +50,14 @@ const MqttSettingsForm: FC = () => {
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
label="Enable MQTT"
label={LL.ENABLE_MQTT()}
/>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="host"
label="Host"
label={LL.ADDRESS_OF(LL.BROKER())}
fullWidth
variant="outlined"
value={data.host}
@@ -80,7 +84,7 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="base"
label="Base"
label={LL.BASE_TOPIC()}
fullWidth
variant="outlined"
value={data.base}
@@ -91,7 +95,7 @@ const MqttSettingsForm: FC = () => {
<Grid item xs={6}>
<ValidatedTextField
name="client_id"
label="Client ID (optional)"
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
fullWidth
variant="outlined"
value={data.client_id}
@@ -104,7 +108,7 @@ const MqttSettingsForm: FC = () => {
<Grid item xs={6}>
<ValidatedTextField
name="username"
label="Username"
label={LL.USERNAME(0)}
fullWidth
variant="outlined"
value={data.username}
@@ -115,7 +119,7 @@ const MqttSettingsForm: FC = () => {
<Grid item xs={6}>
<ValidatedPasswordField
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -129,7 +133,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="keep_alive"
label="Keep Alive (seconds)"
label="Keep Alive"
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.keep_alive)}
@@ -149,7 +156,7 @@ const MqttSettingsForm: FC = () => {
margin="normal"
select
>
<MenuItem value={0}>0 (default)</MenuItem>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
</ValidatedTextField>
@@ -157,18 +164,19 @@ const MqttSettingsForm: FC = () => {
</Grid>
<BlockFormControlLabel
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />}
label="Set Clean Session"
label={LL.MQTT_CLEAN_SESSION()}
/>
<BlockFormControlLabel
control={<Checkbox name="mqtt_retain" checked={data.mqtt_retain} onChange={updateFormValue} />}
label="Always use Retain Flag"
label={LL.MQTT_RETAIN_FLAG()}
/>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Formatting
{LL.FORMATTING()}
</Typography>
<ValidatedTextField
name="nested_format"
label="Topic/Payload Format"
label={LL.MQTT_FORMAT()}
value={data.nested_format}
fullWidth
variant="outlined"
@@ -176,19 +184,19 @@ const MqttSettingsForm: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>Nested in a single topic</MenuItem>
<MenuItem value={2}>As individual topics</MenuItem>
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
</ValidatedTextField>
<BlockFormControlLabel
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
label="Publish command output to a 'response' topic"
label={LL.MQTT_RESPONSE()}
/>
{!data.ha_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<BlockFormControlLabel
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
label="Publish single value topics on change"
label={LL.MQTT_PUBLISH_TEXT_1()}
/>
</Grid>
{data.publish_single && (
@@ -197,7 +205,7 @@ const MqttSettingsForm: FC = () => {
control={
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} />
}
label="Publish to command topics (ioBroker)"
label={LL.MQTT_PUBLISH_TEXT_2()}
/>
</Grid>
)}
@@ -207,15 +215,18 @@ const MqttSettingsForm: FC = () => {
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<BlockFormControlLabel
sx={{ pb: 1 }}
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
label="Enable MQTT Discovery (Home Assistant, Domoticz)"
label={LL.MQTT_PUBLISH_TEXT_3()}
/>
</Grid>
{data.ha_enabled && (
<Grid item xs={6}>
<>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<ValidatedTextField
name="discovery_prefix"
label="Prefix for the Discovery topics"
label={LL.MQTT_PUBLISH_TEXT_4()}
fullWidth
variant="outlined"
value={data.discovery_prefix}
@@ -223,18 +234,55 @@ const MqttSettingsForm: FC = () => {
margin="normal"
/>
</Grid>
<Grid item>
<ValidatedTextField
name="entity_format"
label={LL.MQTT_ENTITY_FORMAT()}
value={data.entity_format}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
<MenuItem value={1}>{LL.MQTT_ENTITY_FORMAT_1()}</MenuItem>
<MenuItem value={2}>{LL.MQTT_ENTITY_FORMAT_2()}</MenuItem>
</ValidatedTextField>
</Grid>
</Grid>
</>
)}
</Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Publish Intervals (in seconds, 0=automatic)
{LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto)
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_heartbeat"
label={LL.MQTT_INT_HEARTBEAT()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_heartbeat)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_boiler"
label="Boilers and Heat Pumps"
label={LL.MQTT_INT_BOILER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_boiler)}
@@ -243,11 +291,14 @@ const MqttSettingsForm: FC = () => {
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_thermostat"
label="Thermostats"
label={LL.MQTT_INT_THERMOSTATS()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_thermostat)}
@@ -256,11 +307,14 @@ const MqttSettingsForm: FC = () => {
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_solar"
label="Solar Modules"
label={LL.MQTT_INT_SOLAR()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_solar)}
@@ -269,11 +323,14 @@ const MqttSettingsForm: FC = () => {
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_mixer"
label="Mixer Modules"
label={LL.MQTT_INT_MIXER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_mixer)}
@@ -282,11 +339,14 @@ const MqttSettingsForm: FC = () => {
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_sensor"
label="Temperature Sensors"
label={LL.TEMP_SENSORS()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_sensor)}
@@ -295,11 +355,14 @@ const MqttSettingsForm: FC = () => {
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_other"
label="Default"
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
label={LL.DEFAULT(0)}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_other)}
@@ -318,7 +381,7 @@ const MqttSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -326,7 +389,7 @@ const MqttSettingsForm: FC = () => {
};
return (
<SectionContent title="MQTT Settings" titleGutter>
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -5,12 +5,15 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import RefreshIcon from '@mui/icons-material/Refresh';
import ReportIcon from '@mui/icons-material/Report';
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
import { ButtonRow, FormLoader, SectionContent } from '../../components';
import { MqttStatus, MqttDisconnectReason } from '../../types';
import * as MqttApi from '../../api/mqtt';
import { useRest } from '../../utils';
import { useI18nContext } from '../../i18n/i18n-react';
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
if (!enabled) {
return theme.palette.info.main;
@@ -29,17 +32,30 @@ export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) =
return theme.palette.error.main;
};
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
if (!enabled) {
return 'Not enabled';
}
if (connected) {
return 'Connected';
}
return 'Disconnected';
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatus, theme: Theme) => {
if (mqtt_queued <= 1) return theme.palette.success.main;
return theme.palette.warning.main;
};
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
const MqttStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<MqttStatus>({ read: MqttApi.readMqttStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatus) => {
if (!enabled) {
return LL.NOT_ENABLED();
}
if (connected) {
return LL.CONNECTED(0) + (connect_count > 1 ? ' (' + connect_count + ')' : '');
}
return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : '');
};
const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
switch (disconnect_reason) {
case MqttDisconnectReason.TCP_DISCONNECTED:
return 'TCP disconnected';
@@ -60,12 +76,7 @@ export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
default:
return 'Unknown';
}
};
const MqttStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<MqttStatus>({ read: MqttApi.readMqttStatus });
const theme = useTheme();
};
const content = () => {
if (!data) {
@@ -73,14 +84,35 @@ const MqttStatusForm: FC = () => {
}
const renderConnectionStatus = () => {
if (data.connected) {
return (
<>
{!data.connected && (
<>
<ListItem>
<ListItemAvatar>
<Avatar>
<ReportIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.DISCONNECT_REASON()} secondary={disconnectReason(data)} />
</ListItem>
<Divider variant="inset" component="li" />
</>
)}
<ListItem>
<ListItemAvatar>
<Avatar>#</Avatar>
</ListItemAvatar>
<ListItemText primary="Client ID" secondary={data.client_id} />
<ListItemText primary={LL.ID_OF(LL.CLIENT())} secondary={data.client_id} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: mqttQueueHighlight(data, theme) }}>
<AutoAwesomeMotionIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.MQTT_QUEUE()} secondary={data.mqtt_queued} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -89,20 +121,7 @@ const MqttStatusForm: FC = () => {
<SpeakerNotesOffIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="MQTT Publish Errors" secondary={data.mqtt_fails} />
</ListItem>
</>
);
}
return (
<>
<ListItem>
<ListItemAvatar>
<Avatar>
<ReportIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
<ListItemText primary={LL.ERRORS_OF('MQTT')} secondary={data.mqtt_fails} />
</ListItem>
<Divider variant="inset" component="li" />
</>
@@ -118,14 +137,14 @@ const MqttStatusForm: FC = () => {
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={mqttStatus(data)} />
<ListItemText primary={LL.STATUS_OF('')} secondary={mqttStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{data.enabled && renderConnectionStatus()}
</List>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
@@ -133,7 +152,7 @@ const MqttStatusForm: FC = () => {
};
return (
<SectionContent title="MQTT Status" titleGutter>
<SectionContent title={LL.STATUS_OF('MQTT')} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -11,12 +11,16 @@ import NetworkStatusForm from './NetworkStatusForm';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import NetworkSettingsForm from './NetworkSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const NetworkConnection: FC = () => {
useLayoutTitle('Network Connection');
const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK(0));
const { routerTab } = useRouterTab();
const authenticatedContext = useContext(AuthenticatedContext);
const navigate = useNavigate();
const { routerTab } = useRouterTab();
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
@@ -41,9 +45,9 @@ const NetworkConnection: FC = () => {
}}
>
<RouterTabs value={routerTab}>
<Tab value="status" label="Network Status" />
<Tab value="scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
<Tab value="settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={LL.STATUS_OF(LL.NETWORK(1))} />
<Tab value="scan" label={LL.NETWORK_SCAN()} disabled={!authenticatedContext.me.admin} />
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<NetworkStatusForm />} />

View File

@@ -1,4 +1,5 @@
import { FC, useContext, useEffect, useState } from 'react';
import { useSnackbar } from 'notistack';
import {
Avatar,
@@ -10,13 +11,15 @@ import {
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
Typography
Typography,
InputAdornment
} from '@mui/material';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import DeleteIcon from '@mui/icons-material/Delete';
import SaveIcon from '@mui/icons-material/Save';
import LockIcon from '@mui/icons-material/Lock';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import {
BlockFormControlLabel,
@@ -24,11 +27,13 @@ import {
FormLoader,
SectionContent,
ValidatedPasswordField,
ValidatedTextField
ValidatedTextField,
MessageBox
} from '../../components';
import { NetworkSettings } from '../../types';
import * as NetworkApi from '../../api/network';
import { numberValue, updateValue, useRest } from '../../utils';
import * as EMSESP from '../../project/api';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
@@ -36,11 +41,18 @@ import { ValidateFieldsError } from 'async-validator';
import { validate } from '../../validators';
import { createNetworkSettingsValidator } from '../../validators/network';
import { useI18nContext } from '../../i18n/i18n-react';
import RestartMonitor from '../system/RestartMonitor';
const WiFiSettingsForm: FC = () => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
const [initialized, setInitialized] = useState(false);
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NetworkSettings>({
const [restarting, setRestarting] = useState(false);
const { loadData, saving, data, setData, saveData, errorMessage, restartNeeded } = useRest<NetworkSettings>({
read: NetworkApi.readNetworkSettings,
update: NetworkApi.updateNetworkSettings
});
@@ -57,7 +69,9 @@ const WiFiSettingsForm: FC = () => {
bandwidth20: false,
tx_power: 20,
nosleep: false,
enableMDNS: true
enableMDNS: true,
enableCORS: false,
CORSOrigin: '*'
});
}
setInitialized(true);
@@ -85,6 +99,15 @@ const WiFiSettingsForm: FC = () => {
}
};
const restart = async () => {
try {
await EMSESP.restart();
setRestarting(true);
} catch (error) {
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
}
};
return (
<>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
@@ -111,7 +134,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="ssid"
label="SSID (leave blank to disable WiFi)"
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
fullWidth
variant="outlined"
value={data.ssid}
@@ -123,7 +146,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -135,7 +158,10 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="tx_power"
label="WiFi Tx Power (dBm)"
label={LL.TX_POWER()}
InputProps={{
endAdornment: <InputAdornment position="end">dBm</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.tx_power)}
@@ -146,27 +172,22 @@ const WiFiSettingsForm: FC = () => {
<BlockFormControlLabel
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
label="Disable WiFi Sleep Mode"
label={LL.NETWORK_DISABLE_SLEEP()}
/>
<BlockFormControlLabel
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
label="Use Lower WiFi Bandwidth"
/>
<BlockFormControlLabel
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
label="Enable mDNS Service"
label={LL.NETWORK_LOW_BAND()}
/>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
General
{LL.GENERAL_OPTIONS()}
</Typography>
<ValidatedTextField
fieldErrors={fieldErrors}
name="hostname"
label="Hostname"
label={LL.HOSTNAME()}
fullWidth
variant="outlined"
value={data.hostname}
@@ -174,21 +195,43 @@ const WiFiSettingsForm: FC = () => {
margin="normal"
/>
<BlockFormControlLabel
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
label={LL.NETWORK_USE_DNS()}
/>
<BlockFormControlLabel
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
label={LL.NETWORK_ENABLE_CORS()}
/>
{data.enableCORS && (
<ValidatedTextField
fieldErrors={fieldErrors}
name="CORSOrigin"
label={LL.NETWORK_CORS_ORIGIN()}
fullWidth
variant="outlined"
value={data.CORSOrigin}
onChange={updateFormValue}
margin="normal"
/>
)}
<BlockFormControlLabel
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
label="Enable IPv6 support"
label={LL.NETWORK_ENABLE_IPV6()}
/>
<BlockFormControlLabel
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />}
label="Use Fixed IP address"
label={LL.NETWORK_FIXED_IP()}
/>
{data.static_ip_config && (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="local_ip"
label="Local IP"
label={LL.AP_LOCAL_IP()}
fullWidth
variant="outlined"
value={data.local_ip}
@@ -198,7 +241,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="gateway_ip"
label="Gateway"
label={LL.NETWORK_GATEWAY()}
fullWidth
variant="outlined"
value={data.gateway_ip}
@@ -208,7 +251,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="subnet_mask"
label="Subnet"
label={LL.NETWORK_SUBNET()}
fullWidth
variant="outlined"
value={data.subnet_mask}
@@ -218,7 +261,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="dns_ip_1"
label="DNS IP #1"
label="DNS #1"
fullWidth
variant="outlined"
value={data.dns_ip_1}
@@ -228,7 +271,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="dns_ip_2"
label="DNS IP #2"
label="DNS #2"
fullWidth
variant="outlined"
value={data.dns_ip_2}
@@ -237,6 +280,14 @@ const WiFiSettingsForm: FC = () => {
/>
</>
)}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
{LL.RESTART()}
</Button>
</MessageBox>
)}
{!restartNeeded && (
<ButtonRow>
<Button
startIcon={<SaveIcon />}
@@ -246,16 +297,17 @@ const WiFiSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
)}
</>
);
};
return (
<SectionContent title="Network Settings" titleGutter>
{content()}
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};

View File

@@ -14,6 +14,8 @@ import { NetworkConnectionStatus, NetworkStatus } from '../../types';
import * as NetworkApi from '../../api/network';
import { useRest } from '../../utils';
import { useI18nContext } from '../../i18n/i18n-react';
const isConnected = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
@@ -35,29 +37,6 @@ const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
}
};
const networkStatus = ({ status }: NetworkStatus) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return 'Inactive';
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
return 'Idle';
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return 'No SSID Available';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
return 'Connected (WiFi)';
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return 'Connected (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return 'Connection Failed';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return 'Connection Lost';
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
return 'Disconnected';
default:
return 'Unknown';
}
};
export const isWiFi = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
export const isEthernet = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
@@ -65,7 +44,7 @@ const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatus) => {
if (!dns_ip_1) {
return 'none';
}
return dns_ip_1 + (dns_ip_2 === '0.0.0.0' ? '' : ',' + dns_ip_2);
return dns_ip_1 + (!dns_ip_2 || dns_ip_2 === '0.0.0.0' ? '' : ',' + dns_ip_2);
};
const IPs = (status: NetworkStatus) => {
@@ -81,8 +60,33 @@ const IPs = (status: NetworkStatus) => {
const NetworkStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<NetworkStatus>({ read: NetworkApi.readNetworkStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const networkStatus = ({ status }: NetworkStatus) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return LL.INACTIVE(1);
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
return LL.IDLE();
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return 'No SSID Available';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (WiFi)';
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return LL.CONNECTED(1) + ' ' + LL.FAILED();
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return LL.CONNECTED(1) + ' ' + LL.LOST();
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
return LL.DISCONNECTED();
default:
return LL.UNKNOWN();
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -120,7 +124,7 @@ const NetworkStatusForm: FC = () => {
<ListItemAvatar>
<Avatar>IP</Avatar>
</ListItemAvatar>
<ListItemText primary="IP Address" secondary={IPs(data)} />
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={IPs(data)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -129,14 +133,14 @@ const NetworkStatusForm: FC = () => {
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="MAC Address" secondary={data.mac_address} />
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>#</Avatar>
</ListItemAvatar>
<ListItemText primary="Subnet Mask" secondary={data.subnet_mask} />
<ListItemText primary={LL.NETWORK_SUBNET()} secondary={data.subnet_mask} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -145,7 +149,7 @@ const NetworkStatusForm: FC = () => {
<SettingsInputComponentIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || 'none'} />
<ListItemText primary={LL.NETWORK_GATEWAY()} secondary={data.gateway_ip || 'none'} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -154,7 +158,7 @@ const NetworkStatusForm: FC = () => {
<DnsIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="DNS Server IP" secondary={dnsServers(data)} />
<ListItemText primary={LL.NETWORK_DNS()} secondary={dnsServers(data)} />
</ListItem>
<Divider variant="inset" component="li" />
</>
@@ -162,7 +166,7 @@ const NetworkStatusForm: FC = () => {
</List>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
@@ -170,7 +174,7 @@ const NetworkStatusForm: FC = () => {
};
return (
<SectionContent title="Network Status" titleGutter>
<SectionContent title={LL.STATUS_OF(LL.NETWORK(1))} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -1,8 +1,6 @@
import { useEffect, FC, useState, useCallback, useRef } from 'react';
import { useSnackbar } from 'notistack';
import { AxiosError } from 'axios';
import { Button } from '@mui/material';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
@@ -12,6 +10,8 @@ import { ButtonRow, FormLoader, SectionContent } from '../../components';
import WiFiNetworkSelector from './WiFiNetworkSelector';
import { useI18nContext } from '../../i18n/i18n-react';
const NUM_POLLS = 10;
const POLLING_FREQUENCY = 500;
@@ -22,6 +22,8 @@ const compareNetworks = (network1: WiFiNetwork, network2: WiFiNetwork) => {
};
const WiFiNetworkScanner: FC = () => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const pollCount = useRef(0);
@@ -46,21 +48,21 @@ const WiFiNetworkScanner: FC = () => {
pollCount.current = completedPollCount;
setTimeout(pollNetworkList, POLLING_FREQUENCY);
} else {
finishedWithError('Device did not return network list in timely manner');
finishedWithError(LL.PROBLEM_LOADING());
}
} else {
const newNetworkList = response.data;
newNetworkList.networks.sort(compareNetworks);
setNetworkList(newNetworkList);
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
finishedWithError('Problem listing WiFi networks ' + error.response?.data.message);
} catch (error) {
if (error.response) {
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
} else {
finishedWithError('Problem listing WiFi networks');
finishedWithError(LL.PROBLEM_LOADING());
}
}
}, [finishedWithError]);
}, [finishedWithError, LL]);
const startNetworkScan = useCallback(async () => {
pollCount.current = 0;
@@ -69,14 +71,14 @@ const WiFiNetworkScanner: FC = () => {
try {
await NetworkApi.scanNetworks();
setTimeout(pollNetworkList, POLLING_FREQUENCY);
} catch (error: unknown) {
if (error instanceof AxiosError) {
finishedWithError('Problem scanning for WiFi networks ' + error.response?.data.message);
} catch (error) {
if (error.response) {
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
} else {
finishedWithError('Problem scanning for WiFi networks');
finishedWithError(LL.PROBLEM_LOADING());
}
}
}, [finishedWithError, pollNetworkList]);
}, [finishedWithError, pollNetworkList, LL]);
useEffect(() => {
startNetworkScan();
@@ -84,13 +86,13 @@ const WiFiNetworkScanner: FC = () => {
const renderNetworkScanner = () => {
if (!networkList) {
return <FormLoader message="Scanning&hellip;" errorMessage={errorMessage} />;
return <FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />;
}
return <WiFiNetworkSelector networkList={networkList} />;
};
return (
<SectionContent title="Network Scanner">
<SectionContent title={LL.NETWORK_SCANNER()}>
{renderNetworkScanner()}
<ButtonRow>
<Button
@@ -100,7 +102,7 @@ const WiFiNetworkScanner: FC = () => {
onClick={startNetworkScan}
disabled={!errorMessage && !networkList}
>
Scan again&hellip;
{LL.SCAN_AGAIN()}&hellip;
</Button>
</ButtonRow>
</SectionContent>

View File

@@ -12,6 +12,8 @@ import { WiFiEncryptionType, WiFiNetwork, WiFiNetworkList } from '../../types';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { useI18nContext } from '../../i18n/i18n-react';
interface WiFiNetworkSelectorProps {
networkList: WiFiNetworkList;
}
@@ -39,6 +41,8 @@ export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
};
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
const { LL } = useI18nContext();
const wifiConnectionContext = useContext(WiFiConnectionContext);
const renderNetwork = (network: WiFiNetwork) => {
@@ -61,7 +65,7 @@ const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
};
if (networkList.networks.length === 0) {
return <MessageBox mt={2} mb={1} message="No WiFi networks found" level="info" />;
return <MessageBox mt={2} mb={1} message={LL.NETWORK_NO_WIFI()} level="info" />;
}
return <List>{networkList.networks.map(renderNetwork)}</List>;

View File

@@ -12,12 +12,16 @@ import * as NTPApi from '../../api/ntp';
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp';
import { useI18nContext } from '../../i18n/i18n-react';
const NTPSettingsForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NTPSettings>({
read: NTPApi.readNTPSettings,
update: NTPApi.updateNTPSettings
});
const { LL } = useI18nContext();
const updateFormValue = updateValue(setData);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -49,12 +53,12 @@ const NTPSettingsForm: FC = () => {
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
label="Enable NTP"
label={LL.ENABLE_NTP()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="server"
label="Server"
label={LL.NTP_SERVER()}
fullWidth
variant="outlined"
value={data.server}
@@ -64,7 +68,7 @@ const NTPSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="tz_label"
label="Time zone"
label={LL.TIME_ZONE()}
fullWidth
variant="outlined"
value={selectedTimeZone(data.tz_label, data.tz_format)}
@@ -72,7 +76,7 @@ const NTPSettingsForm: FC = () => {
margin="normal"
select
>
<MenuItem disabled>Time zone...</MenuItem>
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
{timeZoneSelectItems()}
</ValidatedTextField>
<ButtonRow>
@@ -84,7 +88,7 @@ const NTPSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -92,7 +96,7 @@ const NTPSettingsForm: FC = () => {
};
return (
<SectionContent title="NTP Settings" titleGutter>
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -16,7 +16,8 @@ import {
ListItemText,
TextField,
Theme,
useTheme
useTheme,
Typography
} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
@@ -31,6 +32,8 @@ import { ButtonRow, FormLoader, SectionContent } from '../../components';
import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
@@ -47,19 +50,6 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
}
};
export const ntpStatus = ({ status }: NTPStatus) => {
switch (status) {
case NTPSyncStatus.NTP_DISABLED:
return 'Disabled';
case NTPSyncStatus.NTP_INACTIVE:
return 'Inactive';
case NTPSyncStatus.NTP_ACTIVE:
return 'Active';
default:
return 'Unknown';
}
};
const NTPStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<NTPStatus>({ read: NTPApi.readNTPStatus });
const [localTime, setLocalTime] = useState<string>('');
@@ -68,6 +58,8 @@ const NTPStatusForm: FC = () => {
const { enqueueSnackbar } = useSnackbar();
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
const openSetTime = () => {
@@ -77,35 +69,48 @@ const NTPStatusForm: FC = () => {
const theme = useTheme();
const ntpStatus = ({ status }: NTPStatus) => {
switch (status) {
case NTPSyncStatus.NTP_DISABLED:
return LL.DISABLED(0);
case NTPSyncStatus.NTP_INACTIVE:
return LL.INACTIVE(0);
case NTPSyncStatus.NTP_ACTIVE:
return LL.ACTIVE();
default:
return LL.UNKNOWN();
}
};
const configureTime = async () => {
setProcessing(true);
try {
await NTPApi.updateTime({
local_time: formatLocalDateTime(new Date(localTime))
});
enqueueSnackbar('Time set', { variant: 'success' });
enqueueSnackbar(LL.TIME_SET(), { variant: 'success' });
setSettingTime(false);
loadData();
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating time'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setProcessing(false);
}
};
const renderSetTimeDialog = () => {
return (
const renderSetTimeDialog = () => (
<Dialog open={settingTime} onClose={() => setSettingTime(false)}>
<DialogTitle>Set Time</DialogTitle>
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
<DialogContent dividers>
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
</Box>
<TextField
label="Local Time"
label={LL.LOCAL_TIME()}
type="datetime-local"
value={localTime}
onChange={updateLocalTime}
disabled={processing}
variant="outlined"
fullWidth
InputLabelProps={{
shrink: true
@@ -114,7 +119,7 @@ const NTPStatusForm: FC = () => {
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary">
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<AccessTimeIcon />}
@@ -124,12 +129,11 @@ const NTPStatusForm: FC = () => {
color="primary"
autoFocus
>
Set Time
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
);
};
const content = () => {
if (!data) {
@@ -145,7 +149,7 @@ const NTPStatusForm: FC = () => {
<UpdateIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={ntpStatus(data)} />
<ListItemText primary={LL.STATUS_OF('')} secondary={ntpStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{isNtpEnabled(data) && (
@@ -156,7 +160,7 @@ const NTPStatusForm: FC = () => {
<DnsIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="NTP Server" secondary={data.server} />
<ListItemText primary={LL.NTP_SERVER()} secondary={data.server} />
</ListItem>
<Divider variant="inset" component="li" />
</>
@@ -167,7 +171,7 @@ const NTPStatusForm: FC = () => {
<AccessTimeIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
<ListItemText primary={LL.LOCAL_TIME()} secondary={formatDateTime(data.local_time)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -176,7 +180,7 @@ const NTPStatusForm: FC = () => {
<SwapVerticalCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
<ListItemText primary={LL.UTC_TIME()} secondary={formatDateTime(data.utc_time)} />
</ListItem>
<Divider variant="inset" component="li" />
</List>
@@ -184,7 +188,7 @@ const NTPStatusForm: FC = () => {
<Box flexGrow={1}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
@@ -192,7 +196,7 @@ const NTPStatusForm: FC = () => {
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}>
Set Time
{LL.SET_TIME(0)}
</Button>
</ButtonRow>
</Box>
@@ -204,7 +208,7 @@ const NTPStatusForm: FC = () => {
};
return (
<SectionContent title="NTP Status" titleGutter>
<SectionContent title={LL.STATUS_OF('NTP')} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -9,8 +9,11 @@ import { AuthenticatedContext } from '../../contexts/authentication';
import NTPStatusForm from './NTPStatusForm';
import NTPSettingsForm from './NTPSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const NetworkTime: FC = () => {
useLayoutTitle('Network Time');
const { LL } = useI18nContext();
useLayoutTitle('NTP');
const authenticatedContext = useContext(AuthenticatedContext);
const { routerTab } = useRouterTab();
@@ -18,8 +21,8 @@ const NetworkTime: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="NTP Status" />
<Tab value="settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={LL.STATUS_OF('NTP')} />
<Tab value="settings" label={LL.SETTINGS_OF('NTP')} disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<NTPStatusForm />} />

View File

@@ -19,6 +19,8 @@ import { MessageBox } from '../../components';
import * as SecurityApi from '../../api/security';
import { Token } from '../../types';
import { useI18nContext } from '../../i18n/i18n-react';
interface GenerateTokenProps {
username?: string;
onClose: () => void;
@@ -28,15 +30,17 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const [token, setToken] = useState<Token>();
const open = !!username;
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const getToken = useCallback(async () => {
try {
setToken((await SecurityApi.generateToken(username)).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem generating token'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
}, [username, enqueueSnackbar]);
}, [username, enqueueSnackbar, LL]);
useEffect(() => {
if (open) {
@@ -46,16 +50,11 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open={!!username} fullWidth maxWidth="sm">
<DialogTitle id="generate-token-dialog-title">Access Token for {username}</DialogTitle>
<DialogTitle id="generate-token-dialog-title">{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
<DialogContent dividers>
{token ? (
<>
<MessageBox
message="The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the
'Authorization' header or in the 'access_token' URL query parameter."
level="info"
my={2}
/>
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
<Box mt={2} mb={2}>
<TextField label="Token" multiline value={token.token} fullWidth contentEditable={false} />
</Box>
@@ -63,13 +62,13 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
) : (
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">Generating token&hellip;</Typography>
<Typography variant="h6">{LL.GENERATING_TOKEN()}&hellip;</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
<Button startIcon={<CloseIcon />} variant="outlined" onClick={onClose} color="secondary">
Close
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>

View File

@@ -20,6 +20,8 @@ import { createUserValidator } from '../../validators';
import { useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
import GenerateToken from './GenerateToken';
import UserForm from './UserForm';
@@ -34,9 +36,11 @@ const ManageUsersForm: FC = () => {
const [generatingToken, setGeneratingToken] = useState<string>();
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const table_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 120px;
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
`,
BaseRow: `
font-size: 14px;
@@ -136,8 +140,8 @@ const ManageUsersForm: FC = () => {
<>
<Header>
<HeaderRow>
<HeaderCell resize>USERNAME</HeaderCell>
<HeaderCell stiff>IS ADMIN</HeaderCell>
<HeaderCell resize>{LL.USERNAME(1)}</HeaderCell>
<HeaderCell stiff>{LL.IS_ADMIN(0)}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -169,9 +173,7 @@ const ManageUsersForm: FC = () => {
)}
</Table>
{noAdminConfigured() && (
<MessageBox level="warning" message="You must have at least one admin user configured" my={2} />
)}
{noAdminConfigured() && <MessageBox level="warning" message={LL.USER_WARNING()} my={2} />}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
@@ -183,14 +185,14 @@ const ManageUsersForm: FC = () => {
type="submit"
onClick={onSubmit}
>
Save
{LL.SAVE()}
</Button>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<PersonAddIcon />} variant="outlined" color="secondary" onClick={createUser}>
Add
{LL.ADD(0)}
</Button>
</ButtonRow>
</Box>
@@ -210,7 +212,7 @@ const ManageUsersForm: FC = () => {
};
return (
<SectionContent title="Manage Users" titleGutter>
<SectionContent title={LL.MANAGE_USERS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -8,16 +8,19 @@ import { RouterTabs, useRouterTab, useLayoutTitle } from '../../components';
import SecuritySettingsForm from './SecuritySettingsForm';
import ManageUsersForm from './ManageUsersForm';
import { useI18nContext } from '../../i18n/i18n-react';
const Security: FC = () => {
useLayoutTitle('Security');
const { LL } = useI18nContext();
useLayoutTitle(LL.SECURITY(0));
const { routerTab } = useRouterTab();
return (
<>
<RouterTabs value={routerTab}>
<Tab value="users" label="Manage Users" />
<Tab value="settings" label="Security Settings" />
<Tab value="users" label={LL.MANAGE_USERS()} />
<Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
</RouterTabs>
<Routes>
<Route path="users" element={<ManageUsersForm />} />

View File

@@ -11,7 +11,11 @@ import { SECURITY_SETTINGS_VALIDATOR, validate } from '../../validators';
import { updateValue, useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
const SecuritySettingsForm: FC = () => {
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings,
@@ -42,18 +46,14 @@ const SecuritySettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="jwt_secret"
label="su Password"
label={LL.SU_PASSWORD()}
fullWidth
variant="outlined"
value={data.jwt_secret}
onChange={updateFormValue}
margin="normal"
/>
<MessageBox
level="info"
message="The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console."
mt={1}
/>
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
<ButtonRow>
<Button
startIcon={<SaveIcon />}
@@ -63,7 +63,7 @@ const SecuritySettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -71,7 +71,7 @@ const SecuritySettingsForm: FC = () => {
};
return (
<SectionContent title="Security Settings" titleGutter>
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -3,6 +3,7 @@ import Schema, { ValidateFieldsError } from 'async-validator';
import CancelIcon from '@mui/icons-material/Cancel';
import PersonAddIcon from '@mui/icons-material/PersonAdd';
import SaveIcon from '@mui/icons-material/Save';
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
@@ -11,6 +12,8 @@ import { User } from '../../types';
import { updateValue } from '../../utils';
import { validate } from '../../validators';
import { useI18nContext } from '../../i18n/i18n-react';
interface UserFormProps {
creating: boolean;
validator: Schema;
@@ -23,6 +26,8 @@ interface UserFormProps {
}
const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
const { LL } = useI18nContext();
const updateFormValue = updateValue(setUser);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const open = !!user;
@@ -49,12 +54,14 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
<Dialog onClose={onCancelEditing} open={!!user} fullWidth maxWidth="sm">
{user && (
<>
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
<DialogTitle id="user-form-dialog-title">
{creating ? LL.ADD(1) : LL.MODIFY()}&nbsp;{LL.USER(1)}
</DialogTitle>
<DialogContent dividers>
<ValidatedTextField
fieldErrors={fieldErrors}
name="username"
label="Username"
label={LL.USERNAME(1)}
fullWidth
variant="outlined"
value={user.username}
@@ -65,7 +72,7 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={user.password}
@@ -74,21 +81,21 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
/>
<BlockFormControlLabel
control={<Checkbox name="admin" checked={user.admin} onChange={updateFormValue} />}
label="is Admin?"
label={LL.IS_ADMIN(1)}
/>
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onCancelEditing} color="secondary">
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<PersonAddIcon />}
startIcon={creating ? <PersonAddIcon /> : <SaveIcon />}
variant="outlined"
onClick={validateAndDone}
color="primary"
autoFocus
>
Add
{creating ? LL.ADD(0) : LL.SAVE()}
</Button>
</DialogActions>
</>

View File

@@ -4,6 +4,7 @@ import { AxiosPromise } from 'axios';
import { Typography, Button, Box } from '@mui/material';
import { FileUploadConfig } from '../../api/endpoints';
import { SingleUpload, useFileUpload } from '../../components';
import DownloadIcon from '@mui/icons-material/GetApp';
@@ -14,15 +15,19 @@ import { extractErrorMessage } from '../../utils';
import * as EMSESP from '../../project/api';
import { useI18nContext } from '../../i18n/i18n-react';
interface UploadFileProps {
uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
}
const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
const [uploadFile, cancelUpload, uploading, uploadProgress] = useFileUpload({ upload: uploadGeneralFile });
const [uploadFile, cancelUpload, uploading, uploadProgress, md5] = useFileUpload({ upload: uploadGeneralFile });
const { enqueueSnackbar } = useSnackbar();
const { LL } = useI18nContext();
const saveFile = (json: any, endpoint: string) => {
const a = document.createElement('a');
const filename = 'emsesp_' + endpoint + '.json';
@@ -35,19 +40,19 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
enqueueSnackbar('File downloaded', { variant: 'info' });
enqueueSnackbar(LL.DOWNLOAD_SUCCESSFUL(), { variant: 'info' });
};
const downloadSettings = async () => {
try {
const response = await EMSESP.getSettings();
if (response.status !== 200) {
enqueueSnackbar('Unable to get settings', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
} else {
saveFile(response.data, 'settings');
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
@@ -55,47 +60,50 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
try {
const response = await EMSESP.getCustomizations();
if (response.status !== 200) {
enqueueSnackbar('Unable to get customizations', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
} else {
saveFile(response.data, 'customizations');
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
return (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
Upload
</Typography>
{!uploading && (
<Box mb={2} color="warning.main">
<Typography variant="body2">
Upload a new firmware (.bin) file, settings or customizations (.json) file below.
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()}
</Typography>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.UPLOAD_TEXT()} </Typography>
</Box>
</>
)}
{md5 !== '' && (
<Box mb={2}>
<Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box>
)}
<SingleUpload onDrop={uploadFile} onCancel={cancelUpload} uploading={uploading} progress={uploadProgress} />
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
Download
</Typography>
{!uploading && (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
Download the application settings. Be careful when sharing your settings as this file contains passwords
and other sensitive system information.
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => downloadSettings()}>
settings
{LL.SETTINGS_OF('')}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
Download the entity customizations.
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '}
</Typography>
</Box>
<Button
@@ -104,7 +112,7 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
color="primary"
onClick={() => downloadCustomizations()}
>
customizations
{LL.CUSTOMIZATION()}
</Button>
</>
)}

View File

@@ -12,6 +12,7 @@ import {
ValidatedPasswordField,
ValidatedTextField
} from '../../components';
import { OTASettings } from '../../types';
import { numberValue, updateValue, useRest } from '../../utils';
@@ -19,12 +20,16 @@ import { ValidateFieldsError } from 'async-validator';
import { validate } from '../../validators';
import { OTA_SETTINGS_VALIDATOR } from '../../validators/system';
import { useI18nContext } from '../../i18n/i18n-react';
const OTASettingsForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<OTASettings>({
read: SystemApi.readOTASettings,
update: SystemApi.updateOTASettings
});
const { LL } = useI18nContext();
const updateFormValue = updateValue(setData);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -48,7 +53,7 @@ const OTASettingsForm: FC = () => {
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
label="Enable OTA Updates"
label={LL.ENABLE_OTA()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -64,7 +69,7 @@ const OTASettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -80,7 +85,7 @@ const OTASettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -88,7 +93,7 @@ const OTASettingsForm: FC = () => {
};
return (
<SectionContent title="OTA Settings" titleGutter>
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -4,6 +4,8 @@ import { FC, useRef, useState } from 'react';
import * as SystemApi from '../../api/system';
import { FormLoader } from '../../components';
import { useI18nContext } from '../../i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000;
const POLL_TIMEOUT = 2000;
const POLL_INTERVAL = 5000;
@@ -12,12 +14,14 @@ const RestartMonitor: FC = () => {
const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const { LL } = useI18nContext();
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
const poll = useRef(async () => {
try {
await SystemApi.readSystemStatus(POLL_TIMEOUT);
document.location.href = '/fileUpdated';
} catch (error: unknown) {
} catch (error) {
if (new Date().getTime() < timeoutAt.current) {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
} else {
@@ -32,12 +36,7 @@ const RestartMonitor: FC = () => {
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
return (
<FormLoader
message="EMS-ESP is restarting, please wait&hellip;"
errorMessage={failed ? 'Timed out waiting for device to restart.' : undefined}
/>
);
return <FormLoader message={LL.APPLICATION_RESTARTING() + '...'} errorMessage={failed ? 'Timed out' : undefined} />;
};
export default RestartMonitor;

View File

@@ -12,8 +12,12 @@ import OTASettingsForm from './OTASettingsForm';
import SystemLog from './SystemLog';
import { useI18nContext } from '../../i18n/i18n-react';
const System: FC = () => {
useLayoutTitle('System');
const { LL } = useI18nContext();
useLayoutTitle(LL.SYSTEM(0));
const { me } = useContext(AuthenticatedContext);
const { features } = useContext(FeaturesContext);
@@ -22,11 +26,11 @@ const System: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="System Status" />
<Tab value="log" label="System Log" />
<Tab value="status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
{features.ota && <Tab value="ota" label="OTA Settings" disabled={!me.admin} />}
{features.upload_firmware && <Tab value="upload" label="Upload/Download" disabled={!me.admin} />}
{features.ota && <Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />}
{features.upload_firmware && <Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />}
</RouterTabs>
<Routes>
<Route path="status" element={<SystemStatusForm />} />

View File

@@ -15,6 +15,9 @@ import DownloadIcon from '@mui/icons-material/GetApp';
import { useSnackbar } from 'notistack';
import { EVENT_SOURCE_ROOT } from '../../api/endpoints';
import { useI18nContext } from '../../i18n/i18n-react';
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
const useWindowSize = () => {
@@ -63,6 +66,8 @@ const levelLabel = (level: LogLevel) => {
const SystemLog: FC = () => {
useWindowSize();
const { LL } = useI18nContext();
const { loadData, data, setData } = useRest<LogSettings>({
read: SystemApi.readLogSettings
});
@@ -104,10 +109,10 @@ const SystemLog: FC = () => {
compact: data.compact
});
if (response.status !== 200) {
enqueueSnackbar('Problem applying log settings', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem applying log settings'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
}
};
@@ -158,10 +163,10 @@ const SystemLog: FC = () => {
const fetchLog = useCallback(async () => {
try {
setLogEntries((await SystemApi.readLogEntries()).data);
} catch (error: unknown) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch log'));
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, []);
}, [LL]);
useEffect(() => {
fetchLog();
@@ -197,7 +202,7 @@ const SystemLog: FC = () => {
<Grid item xs={4}>
<ValidatedTextField
name="level"
label="Log Level"
label={LL.LOG_LEVEL()}
value={data.level}
fullWidth
variant="outlined"
@@ -205,6 +210,7 @@ const SystemLog: FC = () => {
margin="normal"
select
>
<MenuItem value={-1}>OFF</MenuItem>
<MenuItem value={3}>ERROR</MenuItem>
<MenuItem value={4}>WARNING</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
@@ -214,7 +220,7 @@ const SystemLog: FC = () => {
</ValidatedTextField>
</Grid>
<Grid item xs={3}>
<FormLabel>Buffer size</FormLabel>
<FormLabel>{LL.BUFFER_SIZE()}</FormLabel>
<Slider
value={data.max_messages}
valueLabelDisplay="auto"
@@ -235,12 +241,12 @@ const SystemLog: FC = () => {
<Grid item>
<BlockFormControlLabel
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />}
label="Compact"
label={LL.COMPACT()}
/>
</Grid>
<Grid item>
<Button startIcon={<DownloadIcon />} variant="outlined" color="secondary" onClick={onDownload}>
Export
{LL.EXPORT()}
</Button>
</Grid>
</Grid>
@@ -273,7 +279,7 @@ const SystemLog: FC = () => {
};
return (
<SectionContent title="System Log" titleGutter id="log-window">
<SectionContent title={LL.LOG_OF(LL.SYSTEM(2))} titleGutter id="log-window">
{content()}
</SectionContent>
);

View File

@@ -22,6 +22,7 @@ import ShowChartIcon from '@mui/icons-material/ShowChart';
import MemoryIcon from '@mui/icons-material/Memory';
import AppsIcon from '@mui/icons-material/Apps';
import SdStorageIcon from '@mui/icons-material/SdStorage';
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
import FolderIcon from '@mui/icons-material/Folder';
import RefreshIcon from '@mui/icons-material/Refresh';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
@@ -31,13 +32,16 @@ import TimerIcon from '@mui/icons-material/Timer';
import CancelIcon from '@mui/icons-material/Cancel';
import { ButtonRow, FormLoader, SectionContent, MessageBox } from '../../components';
import { EspPlatform, SystemStatus, Version } from '../../types';
import { SystemStatus, Version } from '../../types';
import * as SystemApi from '../../api/system';
import { extractErrorMessage, useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import axios from 'axios';
import RestartMonitor from './RestartMonitor';
import { useI18nContext } from '../../i18n/i18n-react';
export const VERSIONCHECK_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/latest';
export const VERSIONCHECK_DEV_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/latest';
@@ -48,6 +52,9 @@ function formatNumber(num: number) {
}
const SystemStatusForm: FC = () => {
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>();
const { loadData, data, errorMessage } = useRest<SystemStatus>({ read: SystemApi.readSystemStatus });
const { me } = useContext(AuthenticatedContext);
@@ -64,7 +71,7 @@ const SystemStatusForm: FC = () => {
setLatestVersion({
version: response.data.name,
url: response.data.assets[1].browser_download_url,
changelog: response.data.html_url
changelog: response.data.assets[0].browser_download_url
});
});
axios.get(VERSIONCHECK_DEV_ENDPOINT).then((response) => {
@@ -79,10 +86,25 @@ const SystemStatusForm: FC = () => {
const restart = async () => {
setProcessing(true);
try {
await SystemApi.restart();
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
const response = await SystemApi.restart();
if (response.status === 200) {
setRestarting(true);
}
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
} finally {
setConfirmRestart(false);
setProcessing(false);
}
};
const partition = async () => {
setProcessing(true);
try {
await SystemApi.partition();
setRestarting(true);
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
} finally {
setConfirmRestart(false);
setProcessing(false);
@@ -91,16 +113,17 @@ const SystemStatusForm: FC = () => {
const renderRestartDialog = () => (
<Dialog open={confirmRestart} onClose={() => setConfirmRestart(false)}>
<DialogTitle>Restart</DialogTitle>
<DialogContent dividers>Are you sure you want to restart EMS-ESP?</DialogContent>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmRestart(false)}
disabled={processing}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
@@ -110,8 +133,19 @@ const SystemStatusForm: FC = () => {
color="primary"
autoFocus
>
Restart
{LL.RESTART()}
</Button>
{data?.has_loader && (
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={partition}
disabled={processing}
color="primary"
>
EMS-ESP-Loader
</Button>
)}
</DialogActions>
</Dialog>
);
@@ -119,22 +153,19 @@ const SystemStatusForm: FC = () => {
const renderVersionDialog = () => {
return (
<Dialog open={showingVersion} onClose={() => setShowingVersion(false)}>
<DialogTitle>Version Check</DialogTitle>
<DialogTitle>{LL.VERSION_CHECK(1)}</DialogTitle>
<DialogContent dividers>
<MessageBox
my={0}
level="info"
message={'You are currently running EMS-ESP version ' + data?.emsesp_version}
/>
<MessageBox my={0} level="info" message={LL.SYSTEM_VERSION_RUNNING() + ' ' + data?.emsesp_version} />
{latestVersion && (
<Box mt={2} mb={2}>
The latest <u>official</u> version is <b>{latestVersion.version}</b>&nbsp;(
{LL.THE_LATEST()}&nbsp;<u>{LL.OFFICIAL()}</u>&nbsp;{LL.VERSION_IS()}&nbsp;<b>{latestVersion.version}</b>
&nbsp;(
<Link target="_blank" href={latestVersion.changelog} color="primary">
{'release notes'}
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link target="_blank" href={latestVersion.url} color="primary">
{'download'}
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
@@ -142,14 +173,15 @@ const SystemStatusForm: FC = () => {
{latestDevVersion && (
<Box mt={2} mb={2}>
The latest <u>development</u> version is&nbsp;<b>{latestDevVersion.version}</b>
{LL.THE_LATEST()}&nbsp;<u>{LL.DEVELOPMENT()}</u>&nbsp;{LL.VERSION_IS()}&nbsp;
<b>{latestDevVersion.version}</b>
&nbsp;(
<Link target="_blank" href={latestDevVersion.changelog} color="primary">
{'release notes'}
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link target="_blank" href={latestDevVersion.url} color="primary">
{'download'}
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
@@ -157,17 +189,17 @@ const SystemStatusForm: FC = () => {
<Box color="warning.main" p={0} pl={0} pr={0} mt={4} mb={0}>
<Typography variant="body2">
Use&nbsp;
<Link target="_blank" href={uploadURL} color="primary">
{'UPLOAD'}
{LL.USE()}&nbsp;
<Link href={uploadURL} color="primary">
{LL.UPLOAD()}
</Link>
&nbsp;to apply the new firmware
&nbsp;{LL.SYSTEM_APPLY_FIRMWARE()}
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setShowingVersion(false)} color="secondary">
Close
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
@@ -178,9 +210,9 @@ const SystemStatusForm: FC = () => {
setProcessing(true);
try {
await SystemApi.factoryReset();
enqueueSnackbar('Device has been factory reset and will now restart', { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem factory resetting the device'), { variant: 'error' });
setRestarting(true);
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setConfirmFactoryReset(false);
setProcessing(false);
@@ -189,16 +221,17 @@ const SystemStatusForm: FC = () => {
const renderFactoryResetDialog = () => (
<Dialog open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
<DialogTitle>Factory Reset</DialogTitle>
<DialogContent dividers>Are you sure you want to reset the device to its factory defaults?</DialogContent>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmFactoryReset(false)}
disabled={processing}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
@@ -208,7 +241,7 @@ const SystemStatusForm: FC = () => {
autoFocus
color="error"
>
Factory Reset
{LL.FACTORY_RESET()}
</Button>
</DialogActions>
</Dialog>
@@ -228,10 +261,10 @@ const SystemStatusForm: FC = () => {
<BuildIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="EMS-ESP Version" secondary={'v' + data.emsesp_version} />
<ListItemText primary={LL.EMS_ESP_VER()} secondary={'v' + data.emsesp_version} />
{latestVersion && (
<Button color="primary" onClick={() => setShowingVersion(true)}>
Version Check
{LL.VERSION_CHECK(0)}
</Button>
)}
</ListItem>
@@ -242,7 +275,7 @@ const SystemStatusForm: FC = () => {
<DevicesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Device (Platform / SDK)" secondary={data.esp_platform + ' / ' + data.sdk_version} />
<ListItemText primary={LL.PLATFORM()} secondary={data.esp_platform + ' / ' + data.sdk_version} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -251,7 +284,7 @@ const SystemStatusForm: FC = () => {
<TimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="System Uptime" secondary={data.uptime} />
<ListItemText primary={LL.UPTIME()} secondary={data.uptime} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -260,7 +293,7 @@ const SystemStatusForm: FC = () => {
<ShowChartIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="CPU Frequency" secondary={data.cpu_freq_mhz + ' MHz'} />
<ListItemText primary={LL.CPU_FREQ()} secondary={data.cpu_freq_mhz + ' MHz'} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -270,17 +303,11 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Heap (Free / Max Alloc)"
secondary={
formatNumber(data.free_heap) +
' / ' +
formatNumber(data.max_alloc_heap) +
' bytes ' +
(data.esp_platform === EspPlatform.ESP8266 ? '(' + data.heap_fragmentation + '% fragmentation)' : '')
}
primary={LL.HEAP()}
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '}
/>
</ListItem>
{data.esp_platform === EspPlatform.ESP32 && data.psram_size > 0 && (
{data.psram_size !== undefined && data.free_psram !== undefined && (
<>
<Divider variant="inset" component="li" />
<ListItem>
@@ -290,8 +317,8 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="PSRAM (Size / Free)"
secondary={formatNumber(data.psram_size) + ' / ' + formatNumber(data.free_psram) + ' bytes'}
primary={LL.PSRAM()}
secondary={formatNumber(data.psram_size) + ' KB / ' + formatNumber(data.free_psram) + ' KB'}
/>
</ListItem>
</>
@@ -304,13 +331,25 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Flash Chip (Size / Speed)"
primary={LL.FLASH()}
secondary={
formatNumber(data.flash_chip_size) + ' bytes / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'
formatNumber(data.flash_chip_size) + ' KB / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'
}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<SdCardAlertIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.APPSIZE()}
secondary={formatNumber(data.app_used) + ' KB / ' + formatNumber(data.app_free) + ' KB'}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
@@ -318,15 +357,8 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="File System (Used / Total)"
secondary={
formatNumber(data.fs_used) +
' / ' +
formatNumber(data.fs_total) +
' bytes (' +
formatNumber(data.fs_total - data.fs_used) +
'\xa0bytes free)'
}
primary={LL.FILESYSTEM()}
secondary={formatNumber(data.fs_used) + ' KB / ' + formatNumber(data.fs_free) + ' KB'}
/>
</ListItem>
<Divider variant="inset" component="li" />
@@ -335,7 +367,7 @@ const SystemStatusForm: FC = () => {
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
@@ -348,7 +380,7 @@ const SystemStatusForm: FC = () => {
color="primary"
onClick={() => setConfirmRestart(true)}
>
Restart
{LL.RESTART()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
@@ -356,7 +388,7 @@ const SystemStatusForm: FC = () => {
onClick={() => setConfirmFactoryReset(true)}
color="error"
>
Factory reset
{LL.FACTORY_RESET()}
</Button>
</ButtonRow>
</Box>
@@ -370,8 +402,8 @@ const SystemStatusForm: FC = () => {
};
return (
<SectionContent title="System Status" titleGutter>
{content()}
<SectionContent title={LL.STATUS_OF(LL.SYSTEM(1))} titleGutter>
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};

View File

@@ -7,17 +7,23 @@ import { FileUploadConfig } from '../../api/endpoints';
import GeneralFileUpload from './GeneralFileUpload';
import RestartMonitor from './RestartMonitor';
import { useI18nContext } from '../../i18n/i18n-react';
const UploadFileForm: FC = () => {
const [restarting, setRestarting] = useState<boolean>();
const { LL } = useI18nContext();
const uploadFile = useRef(async (file: File, config?: FileUploadConfig) => {
const response = await SystemApi.uploadFile(file, config);
if (response.status === 200) {
setRestarting(true);
}
return response;
});
return (
<SectionContent title="Upload/Download" titleGutter>
<SectionContent title={LL.UPLOAD_DOWNLOAD()} titleGutter>
{restarting ? <RestartMonitor /> : <GeneralFileUpload uploadGeneralFile={uploadFile.current} />}
</SectionContent>
);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.331h512v341.337H0z"/><path d="M0 85.331h512v113.775H0z"/><path fill="#FFDA44" d="M0 312.882h512v113.775H0z"/></svg>

After

Width:  |  Height:  |  Size: 216 B

1
interface/src/i18n/FR.svg Executable file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512V426.67H0z"/><path fill="#D80027" d="M288 85.33h-64v138.666H0v64h224v138.666h64V287.996h224v-64H288z"/><g fill="#0052B4"><path d="M393.785 315.358 512 381.034v-65.676zM311.652 315.358 512 426.662v-31.474l-143.693-79.83zM458.634 426.662l-146.982-81.664v81.664z"/></g><path fill="#FFF" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><path fill="#D80027" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><g fill="#0052B4"><path d="M90.341 315.356 0 365.546v-50.19zM200.348 329.51v97.151H25.491z"/></g><path fill="#D80027" d="M143.693 315.358 0 395.188v31.474l200.348-111.304z"/><g fill="#0052B4"><path d="M118.215 196.634 0 130.958v65.676zM200.348 196.634 0 85.33v31.474l143.693 79.83zM53.366 85.33l146.982 81.664V85.33z"/></g><path fill="#FFF" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><path fill="#D80027" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><g fill="#0052B4"><path d="M421.659 196.636 512 146.446v50.19zM311.652 182.482V85.331h174.857z"/></g><path fill="#D80027" d="M368.307 196.634 512 116.804V85.33L311.652 196.634z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.5 513 342"><path fill="#FFF" d="M0 85.5h513v342H0z"/><path fill="#cd1f2a" d="M0 85.5h513v114H0z"/><path fill="#1d4185" d="M0 312h513v114H0z"/></svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.334h512v341.337H0z"/><path fill="#FFF" d="M512 295.883H202.195v130.783H122.435V295.883H0V216.111h122.435V85.329H202.195v130.782H512V277.329z"/><path fill="#2E52B2" d="M512 234.666v42.663H183.652v149.337h-42.674V277.329H0v-42.663h140.978V85.329h42.674v149.337z"/></svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><g fill="#FFF"><path d="M0 85.337h512v341.326H0z"/><path d="M0 85.337h512V256H0z"/></g><path fill="#D80027" d="M0 256h512v170.663H0z"/></svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#0052B4" d="M0 85.333h512V426.67H0z"/><path fill="#FFDA44" d="M192 85.33h-64v138.666H0v64h128v138.666h64V287.996h320v-64H192z"/></svg>

After

Width:  |  Height:  |  Size: 217 B

View File

@@ -0,0 +1,309 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const de: Translation = {
LANGUAGE: 'Sprache',
RETRY: 'Neuer Versuch',
LOADING: 'Laden',
IS_REQUIRED: '{0} ist erforderlich',
SIGN_IN: 'Einloggen',
SIGN_OUT: 'Ausloggen',
USERNAME: 'Nutzername',
PASSWORD: 'Passwort',
SU_PASSWORD: 'su Passwort',
DASHBOARD: 'Kontrollzentrum',
SETTINGS_OF: '{0} Einstellungen',
SAVED: 'gespeichert',
HELP_OF: '{0} Hilfe',
LOGGED_IN: 'Eingeloggt als {name}',
PLEASE_SIGNIN: 'Bitte einloggen, um fortzufahren',
UPLOAD_SUCCESSFUL: 'Hochladen erfolgreich',
DOWNLOAD_SUCCESSFUL: 'Herunterladen erfolgreich',
INVALID_LOGIN: 'Ungültige Login Daten',
NETWORK: 'Netzwerk',
SECURITY: 'Sicherheit',
ONOFF_CAP: 'AN/AUS',
ONOFF: 'an/aus',
TYPE: 'Typ',
DESCRIPTION: 'Bezeichnung',
ENTITIES: 'Entitäten',
REFRESH: 'Aktualisieren',
EXPORT: 'Exportieren',
DEVICE_DETAILS: 'Geräte Details',
BRAND: 'Marke',
ID_OF: '{0} ID',
DEVICE: 'Geräte',
PRODUCT: 'Produkt',
VERSION: 'Version',
ENTITY_NAME: 'Entitätsname',
VALUE: '{{Wert|wert}}',
SHOW_FAV: 'nur Favoriten anzeigen',
DEVICE_SENSOR_DATA: 'Geräte- und Sensordaten',
DEVICES_SENSORS: 'Geräte & Sensoren',
ATTACHED_SENSORS: 'Angeschlossene EMS-ESP Sensoren',
RUN_COMMAND: 'Befehl ausführen',
CHANGE_VALUE: 'Wert ändern',
CANCEL: 'Abbrechen',
RESET: 'Zurücksetzen',
SEND: 'Senden',
SAVE: 'Speichern',
REMOVE: 'Entfernen',
PROBLEM_UPDATING: 'Problem beim Aktualisieren',
PROBLEM_LOADING: 'Problem beim Laden',
ACCESS_DENIED: 'Zugriff abgelehnt',
ANALOG_SENSOR: 'Analogsensor',
ANALOG_SENSORS: 'Analogsensoren',
UPDATED_OF: '{0} Aktualisiert',
UPDATE_OF: '{0} Aktualisieren',
REMOVED_OF: '{0} Entfernt',
DELETION_OF: '{0} Löschung',
OFFSET: 'Addition',
FACTOR: 'Faktor',
FREQ: 'Frequenz',
DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM',
STARTVALUE: 'Startwert',
WARN_GPIO: 'Warnung: Vorsicht bei der korrekten Wahl des GPIO!',
EDIT: 'Editiere',
SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperatursensoren',
WRITE_CMD_SENT: 'Befehl schreiben wurde gesendet',
WRITE_CMD_FAILED: 'Befehl schreiben failed', // TODO translate
EMS_BUS_WARNING: 'EMS-Bus getrennt. Wenn diese Warnung nach einigen Sekunden immer noch besteht, überprüfen Sie bitte die Einstellungen und das Board-Profil',
EMS_BUS_SCANNING: 'Suche nach EMS Geräten...',
CONNECTED: 'Verbunden',
TX_ISSUES: 'Tx-Probleme - versuchen Sie einen anderen Tx-Modus',
DISCONNECTED: 'Getrennt',
EMS_SCAN: 'Möchten Sie wirklich eine vollständige Gerätesuche des EMS-Busses starten?',
EMS_BUS_STATUS: 'EMS-Busstatus',
ACTIVE_DEVICES: 'Aktive Geräte und Sensoren',
EMS_DEVICE: 'EMS Gerät',
SUCCESS: 'ERFOLG',
FAIL: 'FEHLER',
QUALITY: 'QUALITÄT',
SCAN_DEVICES: 'Nach neuen Geräten suchen',
EMS_BUS_STATUS_TITLE: 'EMS-Bus- und Aktivitätsstatus',
SCAN: 'Suche',
STATUS_NAMES: [
'EMS-Telegramme empfangen (Rx)',
'EMS-Telegramme gelesen (Tx)',
'EMS-Telegramme geschrieben (Tx)',
'Temperatursensoren gelesen',
'Analogsensoren gelesen',
'MQTT-Nachrichten gesendet',
'API-Aufrufe',
'Syslog-Mitteilungen'
],
NUM_DEVICES: '{num} Gerät{{e}}',
NUM_TEMP_SENSORS: '{num} Temperatursensor{{en}}',
NUM_ANALOG_SENSORS: '{num} Analogsensor{{en}}',
NUM_DAYS: '{num} Tag{{e}}',
NUM_SECONDS: '{num} Sekunde{{n}}',
NUM_HOURS: '{num} Stunde{{n}}',
NUM_MINUTES: '{num} Minute{{n}}',
APPLICATION_SETTINGS: 'Anwendungseinstellungen',
CUSTOMIZATION: 'Anpassungen',
APPLICATION_RESTARTING: 'EMS-ESP startet neu',
INTERFACE_BOARD_PROFILE: 'Interface Platinenprofil',
BOARD_PROFILE_TEXT: 'Wählen Sie ein vorkonfiguriertes Platinenprofil aus der Liste unten aus oder wählen Sie "Custom", um Ihre eigenen Hardwareeinstellungen zu konfigurieren',
BOARD_PROFILE: 'Platinenprofil',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
BUTTON: 'Taste',
TEMPERATURE: 'Temperatur',
PHY_TYPE: 'Eth PHY Typ',
DISABLED: 'deaktiviert',
TX_MODE: 'Tx Modus',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Allgemeine Optionen',
LANGUAGE_ENTITIES: 'Sprache (für Geräteentitäten)',
HIDE_LED: 'LED ausblenden',
ENABLE_TELNET: 'Aktiviere Telnet Konsole',
ENABLE_ANALOG: 'Aktiviere Analogsensoren',
CONVERT_FAHRENHEIT: 'Konvertiere Temperaturwerte in Fahrenheit',
BYPASS_TOKEN: 'Zugriffstoken-Autorisierung bei API-Aufrufen umgehen',
READONLY: 'Nur-Lese-Modus aktivieren (blockiert alle ausgehenden EMS Tx Write-Befehle)',
UNDERCLOCK_CPU: 'CPU-Geschwindigkeit untertakten',
ENABLE_SHOWER_TIMER: 'Duschtimer aktivieren',
ENABLE_SHOWER_ALERT: 'Duschalarm aktivieren',
TRIGGER_TIME: 'Auslösezeit',
COLD_SHOT_DURATION: 'Kaltschussdauer',
FORMATTING_OPTIONS: 'Formatierungsoptionen',
BOOLEAN_FORMAT_DASHBOARD: 'Boolsches Format für Web',
BOOLEAN_FORMAT_API: 'Boolesches Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Parasitäre Stomversorgung',
LOGGING: 'Protokollierung',
LOG_HEX: 'EMS-Telegramme hexadezimal protokollieren',
ENABLE_SYSLOG: 'Syslog aktivieren',
LOG_LEVEL: 'Log Level',
MARK_INTERVAL: 'Intervallmarke',
SECONDS: 'Sekunden',
MINUTES: 'Minuten',
HOURS: 'Stunden',
RESTART: 'Neu starten',
RESTART_TEXT: 'EMS-ESP muss neu gestartet werden, um geänderte Systemeinstellungen zu übernehmen',
RESTART_CONFIRM: 'EMS-ESP wirklich neu starten?',
COMMAND: 'Befehl',
CUSTOMIZATIONS_RESTART: 'Alle Anpassungen wurden entfernt. Neustart...',
CUSTOMIZATIONS_FULL: 'Ausgewählte Entitäten haben das Limit überschritten. Bitte stapelweise speichern',
CUSTOMIZATIONS_SAVED: 'Anpassungen gespeichert',
CUSTOMIZATIONS_HELP_1: 'Wählen Sie ein Gerät aus und passen Sie die Entitäten mithilfe der Optionen an',
CUSTOMIZATIONS_HELP_2: 'als Favorit markieren',
CUSTOMIZATIONS_HELP_3: 'Schreibaktion deaktivieren',
CUSTOMIZATIONS_HELP_4: 'von MQTT und API ausschließen',
CUSTOMIZATIONS_HELP_5: 'Aus dem Kontrollzentrum ausblenden',
CUSTOMIZATIONS_HELP_6: 'Aus dem Speicher löschen',
SELECT_DEVICE: 'Wählen Sie ein Gerät aus',
SET_ALL: 'setzen Sie alle',
OPTIONS: 'Optionen',
NAME: 'Name',
CUSTOMIZATIONS_RESET: 'Möchten Sie wirklich alle Anpassungen entfernen, einschließlich der benutzerdefinierten Einstellungen der Temperatur- und Analogsensoren?',
DEVICE_ENTITIES: 'Geräteentitäten',
USER_CUSTOMIZATION: 'Benutzeranpassung',
SUPPORT_INFORMATION: 'Unterstützende Informationen',
CLICK_HERE: 'Hier klicken',
HELP_INFORMATION_1: 'EMS-ESP Konfigurationsanweisungen und mehr finden Sie im Online-Wiki',
HELP_INFORMATION_2: 'Für einen Live-Community-Chat besuchen Sie unseren Discord-Server',
HELP_INFORMATION_3: 'Um neue Funktionen anzufragen oder Fehler zu melden, eröffnen Sie ein Issue auf Github',
HELP_INFORMATION_4: 'Bitte laden Sie die System-Details und hängen Sie sie an das Support-Issue an. ',
HELP_INFORMATION_5: 'EMS-ESP ist ein freies Open-Source Projekt. Bitte unterstützen Sie die zukünftige Entwicklung mit einem "Star" auf Github!',
SUPPORT_INFO: 'Support Info',
UPLOAD_OF: '{0} Hochladen',
UPLOAD: 'Hochladen',
DOWNLOAD: 'Herunterladen',
ABORTED: 'abgebrochen',
FAILED: 'gescheitert',
SUCCESSFUL: 'erfolgreich',
SYSTEM: 'System',
LOG_OF: '{0} Log',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Hoch-/Herunterladen',
SYSTEM_VERSION_RUNNING: 'Sie verwenden die Version',
SYSTEM_APPLY_FIRMWARE: 'um die neue Firmware anzuwenden',
CLOSE: 'Schließen',
USE: 'Verwenden Sie',
FACTORY_RESET: 'Werkseinstellung',
SYSTEM_FACTORY_TEXT: 'EMS-ESP wurde auf Werkseinstellung gesetzt und startet als Zugangspunkt neu',
SYSTEM_FACTORY_TEXT_DIALOG: 'Sind Sie sicher alle Einstellungen auf Werkseinstellung zu setzen?',
VERSION_CHECK: 'Versionsprüfung',
THE_LATEST: 'Die neueste',
OFFICIAL: 'offizielle',
DEVELOPMENT: 'Entwicklungs',
VERSION_IS: 'Version ist',
RELEASE_NOTES: 'Versionshinweise',
EMS_ESP_VER: 'EMS-ESP Version',
PLATFORM: 'Platform (Platform / SDK)',
UPTIME: 'System Betriebszeit',
CPU_FREQ: 'CPU Frequenz',
HEAP: 'freier RAM Speicher (Gesamt / max. Block)',
PSRAM: 'PSRAM (Größe / Frei)',
FLASH: 'Flash Speicher (Größe / Geschwindigkeit)',
APPSIZE: 'Programm (Genutzt / Frei)',
FILESYSTEM: 'Dateisystem (Genutzt / Frei)',
BUFFER_SIZE: 'max. Puffergröße',
COMPACT: 'Kompakte Darstellung',
ENABLE_OTA: 'OTA Updates verwenden',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Herunterladen der individuellen Entitätsanpassungen',
DOWNLOAD_SETTINGS_TEXT: 'Herunterladen der Anwendungseinstellungen. Vorsicht beim Teilen der Einstellungen, da sie Passwörter und andere sensitive Einstellungen enthalten',
UPLOAD_TEXT: 'Hochladen von neuer Firmware (.bin), Geräte- oder Entitätseinstellungen (.json), zur optionalen Validitätsprüfung zuerst die (.md5) Datei hochladen',
UPLOADING: 'Hochladen',
UPLOAD_DROP_TEXT: 'Klicken Sie hier, oder ziehen eine Datei hierher',
ERROR: 'Unerwarteter Fehler, bitter versuchen Sie es erneut',
TIME_SET: 'Zeit gesetzt',
MANAGE_USERS: 'Nutzerverwaltung',
IS_ADMIN: 'ist Admin',
USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren',
ADD: 'Hinzufügen',
ACCESS_TOKEN_FOR: 'Zugangs-Token für',
ACCESS_TOKEN_TEXT: 'Dieses Token ist für REST API Aufrufe bestimmt, die eine Authentifizierung benötigen. Es kann entweder als Bearer Token im `Authorization-Header` oder in der Access_Token URL verwendet werden.',
GENERATING_TOKEN: 'Erzeuge Token',
USER: 'Nutzer',
MODIFY: 'Ändern',
SU_TEXT: 'Das su (super user) Passwort wird zum Signieren der Authentifikations-Tokens verwendet und ermöglicht Admin-Berechtigung in der Konsole.',
NOT_ENABLED: 'Nicht aktiviert',
ERRORS_OF: '{0} Fehler',
DISCONNECT_REASON: 'Grund der Verbindungsunterbrechung',
ENABLE_MQTT: 'MQTT aktivieren',
BROKER: 'Broker',
CLIENT: 'Client',
BASE_TOPIC: 'Base',
OPTIONAL: 'Optional',
FORMATTING: 'Formattierung',
MQTT_FORMAT: 'Topic/Payload Format',
MQTT_NEST_1: 'Eingebettet in einem Gesamttopic',
MQTT_NEST_2: 'Als einzelne Topics',
MQTT_RESPONSE: 'Veröffentliche die Kommandoantwort als `response` Topic',
MQTT_PUBLISH_TEXT_1: 'Veröffentliche einzelne Werte bei Veränderung als eigene Topics',
MQTT_PUBLISH_TEXT_2: 'Veröffentliche als Kommando-Topic (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Aktiviere `MQTT Discovery` (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix für die `Discovery`-Topics',
MQTT_PUBLISH_INTERVALS: 'Veröffentlichungs-Intervalle',
MQTT_INT_BOILER: 'Boiler und Wärmepumpen',
MQTT_INT_THERMOSTATS: 'Thermostate',
MQTT_INT_SOLAR: 'Solarmodule',
MQTT_INT_MIXER: 'Mischermodule',
MQTT_INT_HEARTBEAT: 'Heartbeat',
MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Standard',
MQTT_ENTITY_FORMAT: 'Entitäts-ID Format',
MQTT_ENTITY_FORMAT_0: 'Einzelinstanz, Langname (v3.4)',
MQTT_ENTITY_FORMAT_1: 'Einzelinstanz, MQTT-Namen',
MQTT_ENTITY_FORMAT_2: 'Mehrfachinstanzen, MQTT-Namen',
MQTT_CLEAN_SESSION: 'Setze `Clean Session`',
MQTT_RETAIN_FLAG: 'Setze `Retain flag` immer',
INACTIVE: 'Inaktiv',
ACTIVE: 'Aktiv',
UNKNOWN: 'Unbekannt',
SET_TIME: 'Zeiteinstellung',
SET_TIME_TEXT: 'Geben Sie das lokale Datum und die Zeit ein',
LOCAL_TIME: 'Lokalzeit',
UTC_TIME: 'UTC Zeit',
ENABLE_NTP: 'Aktiviere NTP',
NTP_SERVER: 'NTP Server',
TIME_ZONE: 'Zeitzone',
ACCESS_POINT: 'Zugangspunkt',
AP_PROVIDE: 'Aktiviere Zugangspunkt',
AP_PROVIDE_TEXT_1: 'Immer',
AP_PROVIDE_TEXT_2: 'Wenn WiFi nicht verbunden',
AP_PROVIDE_TEXT_3: 'Niemals',
AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal',
AP_HIDE_SSID: 'Verstecke SSID',
AP_CLIENTS: 'AP-Klienten',
AP_MAX_CLIENTS: 'Max Anzahl AP-Klienten',
AP_LOCAL_IP: 'Lokale IP',
NETWORK_SCAN: 'Suche nach WiFi Netzwerken',
IDLE: 'Leerlauf',
LOST: 'Verloren',
SCANNING: 'Suche',
SCAN_AGAIN: 'Erneute Suche',
NETWORK_SCANNER: 'Netzwerk Suche',
NETWORK_NO_WIFI: 'Keine WiFi Netzwerke gefunden',
NETWORK_BLANK_SSID: 'Freilassen um WiFi zu deaktivieren',
TX_POWER: 'Tx Leistung',
HOSTNAME: 'Hostname',
NETWORK_DISABLE_SLEEP: 'Deaktiviere WiFi Schlafmodus',
NETWORK_LOW_BAND: 'Verwende niedrige WiFi Bandbreite',
NETWORK_USE_DNS: 'Aktiviere mDNS Service',
NETWORK_ENABLE_CORS: 'Aktiviere CORS',
NETWORK_CORS_ORIGIN: 'CORS origin',
NETWORK_ENABLE_IPV6: 'Aktiviere IPv6 Unterstützung',
NETWORK_FIXED_IP: 'Feste IP Adresse',
NETWORK_GATEWAY: 'Gateway',
NETWORK_SUBNET: 'Subnetz Maske',
NETWORK_DNS: 'DNS Server',
ADDRESS_OF: '{0} Adresse',
ADMIN: 'Administrator',
GUEST: 'Gast',
NEW: 'Neuer',
NEW_NAME_OF: 'Ändere {0}',
ENTITY: 'Entität',
MIN: 'min',
MAX: 'max'
};
export default de;

View File

@@ -0,0 +1,309 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const en: Translation = {
LANGUAGE: 'Language',
RETRY: 'Retry',
LOADING: 'Loading',
IS_REQUIRED: '{0} is required',
SIGN_IN: 'Sign In',
SIGN_OUT: 'Sign Out',
USERNAME: 'Username',
PASSWORD: 'Password',
SU_PASSWORD: 'su Password',
DASHBOARD: 'Dashboard',
SETTINGS_OF: '{0} Settings',
SAVED: 'saved',
HELP_OF: '{0} Help',
LOGGED_IN: 'Logged in as {name}',
PLEASE_SIGNIN: 'Please sign in to continue',
UPLOAD_SUCCESSFUL: 'Upload finished',
DOWNLOAD_SUCCESSFUL: 'Download finished',
INVALID_LOGIN: 'Invalid login details',
NETWORK: 'Network',
SECURITY: 'Security',
ONOFF_CAP: 'ON/OFF',
ONOFF: 'on/off',
TYPE: 'Type',
DESCRIPTION: 'Description',
ENTITIES: 'Entities',
REFRESH: 'Refresh',
EXPORT: 'Export',
DEVICE_DETAILS: 'Device Details',
ID_OF: '{0} ID',
DEVICE: 'Device',
PRODUCT: 'Product',
VERSION: 'Version',
BRAND: 'Brand',
ENTITY_NAME: 'Entity Name',
VALUE: '{{Value|value}}',
SHOW_FAV: 'only show favorites',
DEVICE_SENSOR_DATA: 'Device and Sensor Data',
DEVICES_SENSORS: 'Devices & Sensors',
ATTACHED_SENSORS: 'Attached EMS-ESP Sensors',
RUN_COMMAND: 'Call Command',
CHANGE_VALUE: 'Change Value',
CANCEL: 'Cancel',
RESET: 'Reset',
SEND: 'Send',
SAVE: 'Save',
REMOVE: 'Remove',
PROBLEM_UPDATING: 'Problem updating',
PROBLEM_LOADING: 'Problem loading',
ACCESS_DENIED: 'Access Denied',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analog Sensors',
UPDATED_OF: '{0} Updated',
UPDATE_OF: '{0} Update',
REMOVED_OF: '{0} Removed',
DELETION_OF: '{0} Deletion',
OFFSET: 'Offset',
FACTOR: 'Factor',
FREQ: 'Frequency',
DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM',
STARTVALUE: 'Start value',
WARN_GPIO: 'Warning: be careful when assigning a GPIO!',
EDIT: 'Edit',
SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperature Sensor',
TEMP_SENSORS: 'Temperature Sensors',
WRITE_CMD_SENT: 'Write command has been sent',
WRITE_CMD_FAILED: 'Write command failed',
EMS_BUS_WARNING: 'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
EMS_BUS_SCANNING: 'Scanning for EMS devices...',
CONNECTED: 'Connected',
TX_ISSUES: 'Tx issues - try a different Tx Mode',
DISCONNECTED: 'Disconnected',
EMS_SCAN: 'Are you sure you want to initiate a full device scan of the EMS bus?',
EMS_BUS_STATUS: 'EMS Bus Status',
ACTIVE_DEVICES: 'Active Devices & Sensors',
EMS_DEVICE: 'EMS Device',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
QUALITY: 'QUALITY',
SCAN_DEVICES: 'Scan for new devices',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activity Status',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrams Received (Rx)',
'EMS Reads (Tx)',
'EMS Writes (Tx)',
'Temperature Sensor Reads',
'Analog Sensor Reads',
'MQTT Publishes',
'API Calls',
'Syslog Messages'
],
NUM_DEVICES: '{num} Device{{s}}',
NUM_TEMP_SENSORS: '{num} Temperature Sensor{{s}}',
NUM_ANALOG_SENSORS: '{num} Analog Sensor{{s}}',
NUM_DAYS: '{num} day{{s}}',
NUM_SECONDS: '{num} second{{s}}',
NUM_HOURS: '{num} hour{{s}}',
NUM_MINUTES: '{num} minute{{s}}',
APPLICATION_SETTINGS: 'Application Settings',
CUSTOMIZATION: 'Customization',
APPLICATION_RESTARTING: 'EMS-ESP is restarting',
INTERFACE_BOARD_PROFILE: 'Interface Board Profile',
BOARD_PROFILE_TEXT: 'Select a pre-configured interface board profile from the list below or choose Custom to configure your own hardware settings',
BOARD_PROFILE: 'Board Profile',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
BUTTON: 'Button',
TEMPERATURE: 'Temperature',
PHY_TYPE: 'Eth PHY Type',
DISABLED: 'disabled',
TX_MODE: 'Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'General Options',
LANGUAGE_ENTITIES: 'Language (for device entities)',
HIDE_LED: 'Hide LED',
ENABLE_TELNET: 'Enable Telnet Console',
ENABLE_ANALOG: 'Enable Analog Sensors',
CONVERT_FAHRENHEIT: 'Convert temperature values to Fahrenheit',
BYPASS_TOKEN: 'Bypass Access Token authorization on API calls',
READONLY: 'Enable read-only mode (blocks all outgoing EMS Tx Write commands)',
UNDERCLOCK_CPU: 'Underclock CPU speed',
ENABLE_SHOWER_TIMER: 'Enable Shower Timer',
ENABLE_SHOWER_ALERT: 'Enable Shower Alert',
TRIGGER_TIME: 'Trigger Time',
COLD_SHOT_DURATION: 'Cold Shot Duration',
FORMATTING_OPTIONS: 'Formatting Options',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean Format Dashboard',
BOOLEAN_FORMAT_API: 'Boolean Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Enable parasite power',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrams in hexadecimal',
ENABLE_SYSLOG: 'Enable Syslog',
LOG_LEVEL: 'Log Level',
MARK_INTERVAL: 'Mark Interval',
SECONDS: 'seconds',
MINUTES: 'minutes',
HOURS: 'hours',
RESTART: 'Restart',
RESTART_TEXT: 'EMS-ESP needs to be restarted to apply changed system settings',
RESTART_CONFIRM: 'Are you sure you want to restart EMS-ESP?',
COMMAND: 'Command',
CUSTOMIZATIONS_RESTART: 'All customizations have been removed. Restarting...',
CUSTOMIZATIONS_FULL: 'Selected entities exceeded limit. Please save in batches',
CUSTOMIZATIONS_SAVED: 'Customizations saved',
CUSTOMIZATIONS_HELP_1: 'Select a device and customize the entities options or click to rename',
CUSTOMIZATIONS_HELP_2: 'mark as favorite',
CUSTOMIZATIONS_HELP_3: 'disable write action',
CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API',
CUSTOMIZATIONS_HELP_5: 'hide from Dashboard',
CUSTOMIZATIONS_HELP_6: 'remove from memory',
SELECT_DEVICE: 'Select a device',
SET_ALL: 'set all',
OPTIONS: 'Options',
NAME: 'Name',
CUSTOMIZATIONS_RESET: 'Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors?',
DEVICE_ENTITIES: 'Device Entities',
USER_CUSTOMIZATION: 'User Customization',
SUPPORT_INFORMATION: 'Support Information',
CLICK_HERE: 'Click Here',
HELP_INFORMATION_1: 'Visit the online wiki to get instructions on how to configure EMS-ESP',
HELP_INFORMATION_2: 'For live community chat join our Discord server',
HELP_INFORMATION_3: 'To request a feature or report a bug',
HELP_INFORMATION_4: 'remember to download and attach your system information for a faster response when reporting an issue',
HELP_INFORMATION_5: 'EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!',
SUPPORT_INFO: 'Support Info',
UPLOAD_OF: '{0} Upload',
UPLOAD: 'Upload',
DOWNLOAD: 'Download',
ABORTED: 'aborted',
FAILED: 'failed',
SUCCESSFUL: 'successful',
SYSTEM: 'System',
LOG_OF: '{0} Log',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
SYSTEM_VERSION_RUNNING: 'You are currently running version',
SYSTEM_APPLY_FIRMWARE: 'to apply the new firmware',
CLOSE: 'Close',
USE: 'Use',
FACTORY_RESET: 'Factory Reset',
SYSTEM_FACTORY_TEXT: 'Device has been factory reset and will now restart',
SYSTEM_FACTORY_TEXT_DIALOG: 'Are you sure you want to reset the device to its factory defaults?',
VERSION_CHECK: 'Version Check',
THE_LATEST: 'The latest',
OFFICIAL: 'official',
DEVELOPMENT: 'development',
VERSION_IS: 'version is',
RELEASE_NOTES: 'release notes',
EMS_ESP_VER: 'EMS-ESP Version',
PLATFORM: 'Device (Platform / SDK)',
UPTIME: 'System Uptime',
CPU_FREQ: 'CPU Frequency',
HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size / Speed)',
APPSIZE: 'Application (Used / Free)',
FILESYSTEM: 'File System (Used / Free)',
BUFFER_SIZE: 'Max Buffer Size',
COMPACT: 'Compact',
ENABLE_OTA: 'Enable OTA Updates',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download the entity customizations',
DOWNLOAD_SETTINGS_TEXT: 'Download the application settings. Be careful when sharing your settings as this file contains passwords and other sensitive system information',
UPLOAD_TEXT: 'Upload a new firmware (.bin) file, settings or customizations (.json) file below, for optional validation upload (.md5) first',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Drop file or click here',
ERROR: 'Unexpected Error, please try again',
TIME_SET: 'Time set',
MANAGE_USERS: 'Manage Users',
IS_ADMIN: 'is Admin',
USER_WARNING: 'You must have at least one admin user configured',
ADD: 'Add',
ACCESS_TOKEN_FOR: 'Access Token for',
ACCESS_TOKEN_TEXT: 'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
GENERATING_TOKEN: 'Generating token',
USER: 'User',
MODIFY: 'Modify',
SU_TEXT: 'The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console.',
NOT_ENABLED: 'Not enabled',
ERRORS_OF: '{0} Errors',
DISCONNECT_REASON: 'Disconnect Reason',
ENABLE_MQTT: 'Enable MQTT',
BROKER: 'Broker',
CLIENT: 'Client',
BASE_TOPIC: 'Base',
OPTIONAL: 'Optional',
FORMATTING: 'Formatting',
MQTT_FORMAT: 'Topic/Payload Format',
MQTT_NEST_1: 'Nested in a single topic',
MQTT_NEST_2: 'As individual topics',
MQTT_RESPONSE: 'Publish command output to a `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publish single value topics on change',
MQTT_PUBLISH_TEXT_2: 'Publish to command topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Enable MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix for the Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publish Intervals',
MQTT_INT_BOILER: 'Boilers and Heat Pumps',
MQTT_INT_THERMOSTATS: 'Thermostats',
MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules',
MQTT_INT_HEARTBEAT: 'Heartbeat',
MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Default',
MQTT_ENTITY_FORMAT: 'Entity ID format',
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)',
MQTT_ENTITY_FORMAT_1: 'Single instance, short name',
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name',
MQTT_CLEAN_SESSION: 'Set Clean Session',
MQTT_RETAIN_FLAG: 'Always set Retain flag',
INACTIVE: 'Inactive',
ACTIVE: 'Active',
UNKNOWN: 'Unknown',
SET_TIME: 'Set Time',
SET_TIME_TEXT: 'Enter local date and time below to set the time',
LOCAL_TIME: 'Local Time',
UTC_TIME: 'UTC Time',
ENABLE_NTP: 'Enable NTP',
NTP_SERVER: 'NTP Server',
TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'always',
AP_PROVIDE_TEXT_2: 'when WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'never',
AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID',
AP_CLIENTS: 'AP Clients',
AP_MAX_CLIENTS: 'Max Clients',
AP_LOCAL_IP: 'Local IP',
NETWORK_SCAN: 'Scan WiFi Networks',
IDLE: 'Idle',
LOST: 'Lost',
SCANNING: 'Scanning',
SCAN_AGAIN: 'Scan again',
NETWORK_SCANNER: 'Network Scanner',
NETWORK_NO_WIFI: 'No WiFi networks found',
NETWORK_BLANK_SSID: 'leave blank to disable WiFi',
TX_POWER: 'Tx Power',
HOSTNAME: 'Hostname',
NETWORK_DISABLE_SLEEP: 'Disable WiFi Sleep Mode',
NETWORK_LOW_BAND: 'Use Lower WiFi Bandwidth',
NETWORK_USE_DNS: 'Enable mDNS Service',
NETWORK_ENABLE_CORS: 'Enable CORS',
NETWORK_CORS_ORIGIN: 'CORS origin',
NETWORK_ENABLE_IPV6: 'Enable IPv6 support',
NETWORK_FIXED_IP: 'Use Fixed IP address',
NETWORK_GATEWAY: 'Gateway',
NETWORK_SUBNET: 'Subnet Mask',
NETWORK_DNS: 'DNS Servers',
ADDRESS_OF: '{0} Address',
ADMIN: 'Admin',
GUEST: 'Guest',
NEW: 'New',
NEW_NAME_OF: 'New {0} name',
ENTITY: 'entity',
MIN: 'min',
MAX: 'max'
};
export default en;

View File

@@ -0,0 +1,10 @@
import type { FormattersInitializer } from 'typesafe-i18n';
import type { Locales, Formatters } from './i18n-types';
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
const formatters: Formatters = {
// add your formatter functions here
};
return formatters;
};

View File

@@ -0,0 +1,309 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const fr: Translation = {
LANGUAGE: 'Langue',
RETRY: 'Réessayer',
LOADING: 'Chargement',
IS_REQUIRED: '{0} est requis',
SIGN_IN: 'Se connecter',
SIGN_OUT: 'Se déconnecter',
USERNAME: 'Nom d\'utilisateur',
PASSWORD: 'Mot de passe',
SU_PASSWORD: 'Mot de passe su',
DASHBOARD: 'Tableau de bord',
SETTINGS_OF: 'Paramètres {0}',
SAVED: 'sauvegardé',
HELP_OF: 'Aide {0}',
LOGGED_IN: 'Connecté en tant que {name}',
PLEASE_SIGNIN: 'Veuillez vous connecter pour continuer',
UPLOAD_SUCCESSFUL: 'Upload terminée',
DOWNLOAD_SUCCESSFUL: 'Téléchargement terminé',
INVALID_LOGIN: 'Informations de connexion invalides',
NETWORK: 'Réseau',
SECURITY: 'Sécurité',
ONOFF_CAP: 'ON/OFF',
ONOFF: 'on/off',
TYPE: 'Type',
DESCRIPTION: 'Description',
ENTITIES: 'Entités',
REFRESH: 'Rafraîchir',
EXPORT: 'Exporter',
DEVICE_DETAILS: 'Détails de l\'appareil',
ID_OF: 'ID {0}',
DEVICE: 'Appareil',
PRODUCT: 'Produit',
VERSION: 'Version',
BRAND: 'Marque',
ENTITY_NAME: 'Nom de l\'entité',
VALUE: 'Valeur',
SHOW_FAV: 'ne montrer que les favoris',
DEVICE_SENSOR_DATA: 'Données des appareils et capteurs',
DEVICES_SENSORS: 'Appareils et capteurs',
ATTACHED_SENSORS: 'Capteurs EMS-ESP connectés',
RUN_COMMAND: 'Lancer une commande',
CHANGE_VALUE: 'Changer la valeur',
CANCEL: 'Annuler',
RESET: 'Réinitialiser',
SEND: 'Envoyer',
SAVE: 'Sauvegarder',
REMOVE: 'Enlever',
PROBLEM_UPDATING: 'Problème lors de la mise à jour',
PROBLEM_LOADING: 'Problème lors du chargement',
ACCESS_DENIED: 'Accès refusé',
ANALOG_SENSOR: 'Capteur analogique',
ANALOG_SENSORS: 'Capteurs analogiques',
UPDATED_OF: '{0} mis à jour',
UPDATE_OF: 'Mise à jour de {0}',
REMOVED_OF: '{0} enlevé',
DELETION_OF: '{0} supprimé',
OFFSET: 'Offset',
FACTOR: 'Facteur',
FREQ: 'Fréquence',
DUTY_CYCLE: 'Cycle de fonctionnement',
UNIT: 'Unité',
STARTVALUE: 'Valeur de départ',
WARN_GPIO: 'Attention: soyez vigilant en choisissant un GPIO!',
EDIT: 'Éditer',
SENSOR: 'Capteur',
TEMP_SENSOR: 'Capteur de température',
TEMP_SENSORS: 'Capteurs de température',
WRITE_CMD_SENT: 'Envoyer la commande sent', // TODO translate
WRITE_CMD_FAILED: 'Envoyer la commande failed', // TODO translate
EMS_BUS_WARNING: 'Bus EMS déconnecté. Si ce message persiste après quelques secondes, vérifiez les paramètres et la configuration de la carte.',
EMS_BUS_SCANNING: 'Scan des appareils EMS...',
CONNECTED: 'Connecté',
TX_ISSUES: 'Problèmes de transmission (Tx) - Essayez un autre mode Tx',
DISCONNECTED: 'Déconnecté',
EMS_SCAN: 'Etes-vous sûr de vouloir lancer un scan complet du bus EMS ?',
EMS_BUS_STATUS: 'Statut du bus EMS',
ACTIVE_DEVICES: 'Appareils et capteurs actifs',
EMS_DEVICE: 'Appareils EMS',
SUCCESS: 'SUCCÈS',
FAIL: 'ÉCHEC',
QUALITY: 'QUALITÉ',
SCAN_DEVICES: 'Rechercher de nouveaux appareils',
EMS_BUS_STATUS_TITLE: 'Statut du bus et de l\'activité EMS',
SCAN: 'Scan',
STATUS_NAMES: [
'Télégrammes EMS reçus (Rx)',
'Lectures EMS (Tx)',
'Écritures EMS (Tx)',
'Lectures capteurs de température',
'Lectures capteurs analogiques',
'Publications MQTT',
'Appels à l\'API',
'Messages Syslog'
],
NUM_DEVICES: '{num} Appareil{{s}}',
NUM_TEMP_SENSORS: '{num} Capteur{{s}} de température',
NUM_ANALOG_SENSORS: '{num} Capteur{{s}} analogique{{s}}',
NUM_DAYS: '{num} jour{{s}}',
NUM_SECONDS: '{num} seconde{{s}}',
NUM_HOURS: '{num} heure{{s}}',
NUM_MINUTES: '{num} minute{{s}}',
APPLICATION_SETTINGS: 'Paramètres de l\'application',
CUSTOMIZATION: 'Personnalisation',
APPLICATION_RESTARTING: 'EMS-ESP redémarre',
INTERFACE_BOARD_PROFILE: 'Profile de carte d\'interface',
BOARD_PROFILE_TEXT: 'Sélectionnez un profil de carte d\'interface préconfiguré dans la liste ci-dessous ou choisissez Personnalisé pour configurer vos propres paramètres matériels',
BOARD_PROFILE: 'Profil de carte',
CUSTOM: 'Personnalisé',
GPIO_OF: 'GPIO {0}',
BUTTON: 'Bouton',
TEMPERATURE: 'Température',
PHY_TYPE: 'Eth PHY Type',
DISABLED: 'désactivé',
TX_MODE: 'Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Options générales',
LANGUAGE_ENTITIES: 'Langue (pour les entités du matériel)',
HIDE_LED: 'Cacher la LED',
ENABLE_TELNET: 'Activer la console Telnet',
ENABLE_ANALOG: 'Activer les capteurs analogiques',
CONVERT_FAHRENHEIT: 'Convertir les températures en Fahrenheit',
BYPASS_TOKEN: 'Contourner l\'autorisation du jeton d\'accès sur les appels API',
READONLY: 'Activer le mode lecture uniquement (bloque toutes les commandes EMS sortantes en écriture Tx)',
UNDERCLOCK_CPU: 'Underclock du CPU',
ENABLE_SHOWER_TIMER: 'Activer la minuterie de la douche',
ENABLE_SHOWER_ALERT: 'Activer les alertes de durée de douche',
TRIGGER_TIME: 'Durée avant déclenchement',
COLD_SHOT_DURATION: 'Durée du coup d\'eau froide',
FORMATTING_OPTIONS: 'Options de mise en forme',
BOOLEAN_FORMAT_DASHBOARD: 'Tableau de bord du format booléen',
BOOLEAN_FORMAT_API: 'Format booléen API/MQTT',
ENUM_FORMAT: 'Format enum API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Activer la puissance parasite',
LOGGING: 'Journal',
LOG_HEX: 'Enregistrer les télégrammes EMS en hexadécimal',
ENABLE_SYSLOG: 'Activer les logs système',
LOG_LEVEL: 'Niveau de log',
MARK_INTERVAL: 'Intervalle de marquage',
SECONDS: 'secondes',
MINUTES: 'minutes',
HOURS: 'heures',
RESTART: 'Redémarrer',
RESTART_TEXT: 'EMS-ESP a besoin de redémarrer pour appliquer les changements de paramètres du système',
RESTART_CONFIRM: 'Etes-vous sûr de vouloir redémarrer EMS-ESP ?',
COMMAND: 'Commande',
CUSTOMIZATIONS_RESTART: 'Toutes les personnalisations ont été supprimées. Redémarrage...',
CUSTOMIZATIONS_FULL: 'Les entités sélectionnées ont dépassé la limite. Veuillez sauvegarder par lots',
CUSTOMIZATIONS_SAVED: 'Personnalisations enregistrées',
CUSTOMIZATIONS_HELP_1: 'Sélectionnez un appareil et personnalisez les options des entités ou cliquez pour renommer',
CUSTOMIZATIONS_HELP_2: 'marquer comme favori',
CUSTOMIZATIONS_HELP_3: 'désactiver l\'action d\'écriture',
CUSTOMIZATIONS_HELP_4: 'exclure de MQTT et de l\'API',
CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord',
CUSTOMIZATIONS_HELP_6: 'remove from memory',
SELECT_DEVICE: 'Sélectionnez un appareil',
SET_ALL: 'tout régler',
OPTIONS: 'Options',
NAME: 'Nom',
CUSTOMIZATIONS_RESET: 'Êtes-vous sûr de vouloir supprimer toutes les personnalisations, y compris les paramètres personnalisés des capteurs de température et analogiques ?',
DEVICE_ENTITIES: 'Entités de l\'appareil',
USER_CUSTOMIZATION: 'Personnalisation de l\'utilisateur',
SUPPORT_INFORMATION: 'Information de support',
CLICK_HERE: 'Cliquez ici',
HELP_INFORMATION_1: 'Visitez le wiki en ligne pour obtenir des instructions sur la façon de configurer EMS-ESP.',
HELP_INFORMATION_2: 'Pour une discussion en direct avec la communauté, rejoignez notre serveur Discord',
HELP_INFORMATION_3: 'Pour demander une fonctionnalité ou signaler un problème',
HELP_INFORMATION_4: 'n\'oubliez pas de télécharger et de joindre les informations relatives à votre système pour obtenir une réponse plus rapide lorsque vous signalez un problème',
HELP_INFORMATION_5: 'EMS-ESP est un projet libre et open-source. Merci de soutenir son développement futur en lui donnant une étoile sur Github !',
SUPPORT_INFO: 'Information de support',
UPLOAD_OF: 'Upload de {0}',
UPLOAD: 'Upload',
DOWNLOAD: 'Download',
ABORTED: 'annulé',
FAILED: 'échoué',
SUCCESSFUL: 'réussi',
SYSTEM: 'Système',
LOG_OF: '{0} Log',
STATUS_OF: 'Statut {0}',
UPLOAD_DOWNLOAD: 'Upload/Download',
SYSTEM_VERSION_RUNNING: 'Vous utilisez actuellement la version',
SYSTEM_APPLY_FIRMWARE: 'pour appliquer le nouveau firmware',
CLOSE: 'Fermer',
USE: 'Utiliser',
FACTORY_RESET: 'Réinitialisation',
SYSTEM_FACTORY_TEXT: 'L\'appareil a été réinitialisé et va maintenant redémarrer',
SYSTEM_FACTORY_TEXT_DIALOG: 'Êtes-vous sûr de vouloir réinitialiser l\'appareil à ses paramètres d\'usine ?',
VERSION_CHECK: 'Vérification de la version',
THE_LATEST: 'La dernière',
OFFICIAL: 'officielle',
DEVELOPMENT: 'développement',
VERSION_IS: 'version est',
RELEASE_NOTES: 'notes de version',
EMS_ESP_VER: 'Version EMS-ESP',
PLATFORM: 'Appareil (Plateforme / SDK)',
UPTIME: 'Durée de fonctionnement du système',
CPU_FREQ: 'Fréquence du CPU',
HEAP: 'Heap (Libre / Max Allouée)',
PSRAM: 'PSRAM (Taille / Libre)',
FLASH: 'Flash Chip (Taille / Vitesse)',
APPSIZE: 'Application (Utilisée / Libre)',
FILESYSTEM: 'File System (Utilisée / Libre)',
BUFFER_SIZE: 'Max taille du buffer',
COMPACT: 'Compact',
ENABLE_OTA: 'Activer les updates OTA',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Télécharger les personnalisations d\'entités',
DOWNLOAD_SETTINGS_TEXT: 'Téléchargez les paramètres de l\'application. Soyez prudent lorsque vous partagez vos paramètres car ce fichier contient des mots de passe et d\'autres informations système sensibles.',
UPLOAD_TEXT: 'Téléchargez un nouveau fichier de firmware (.bin), un fichier de paramètres ou de personnalisations (.json) ci-dessous, pour une validation optionnelle téléchargez d\'abord un fichier (.md5)',
UPLOADING: 'Téléchargement',
UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici',
ERROR: 'Erreur inattendue, veuillez réessayer',
TIME_SET: 'Time set',
MANAGE_USERS: 'Gérer les utilisateurs',
IS_ADMIN: 'admin',
USER_WARNING: 'Vous devez avoir au moins un utilisateur admin configuré',
ADD: 'Ajouter',
ACCESS_TOKEN_FOR: 'Jeton d\'accès pour',
ACCESS_TOKEN_TEXT: 'Le jeton ci-dessous est utilisé avec les appels d\'API REST qui nécessitent une autorisation. Il peut être passé soit en tant que jeton Bearer dans l\'en-tête Authorization, soit dans le paramètre de requête URL access_token.',
GENERATING_TOKEN: 'Génération de jeton',
USER: 'Utilisateur',
MODIFY: 'Modifier',
SU_TEXT: 'Le mot de passe su (super utilisateur) est utilisé pour signer les jetons d\'authentification et activer les privilèges d\'administrateur dans la console.',
NOT_ENABLED: 'Non activé',
ERRORS_OF: 'Erreurs {0}',
DISCONNECT_REASON: 'Raison de la déconnexion',
ENABLE_MQTT: 'Activer le MQTT',
BROKER: 'Broker',
CLIENT: 'Client',
BASE_TOPIC: 'Base',
OPTIONAL: 'Optionnel',
FORMATTING: 'Mise en forme',
MQTT_FORMAT: 'Format du Topic/Payload',
MQTT_NEST_1: 'Englobé dans un topic unique',
MQTT_NEST_2: 'En tant que topics individuels',
MQTT_RESPONSE: 'Publier le résultat des commandes dans un topic `response`',
MQTT_PUBLISH_TEXT_1: 'Publier des topics à valeur unique sur changement',
MQTT_PUBLISH_TEXT_2: 'Publier vers des topics de commande (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Activer la découverte MQTT (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Préfixe pour les topics découverte',
MQTT_PUBLISH_INTERVALS: 'Intervalles de publication',
MQTT_INT_BOILER: 'Chaudières et pompes à chaleur',
MQTT_INT_THERMOSTATS: 'Thermostats',
MQTT_INT_SOLAR: 'Modules solaires',
MQTT_INT_MIXER: 'Modules mélangeurs',
MQTT_INT_HEARTBEAT: 'Battements',
MQTT_QUEUE: 'Queue MQTT',
DEFAULT: 'Défaut',
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)',// TODO translate
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
MQTT_CLEAN_SESSION: 'Flag Clean Session',
MQTT_RETAIN_FLAG: 'Toujours activer le Retain Flag',
INACTIVE: 'Inactif',
ACTIVE: 'Actif',
UNKNOWN: 'Inconnu',
SET_TIME: 'Définir l\'heure',
SET_TIME_TEXT: 'Entrer la date et l\'heure locale ci-dessous pour régler l\'heure',
LOCAL_TIME: 'Heure locale',
UTC_TIME: 'Heure UTC',
ENABLE_NTP: 'Activer le NTP',
NTP_SERVER: 'Serveur NTP',
TIME_ZONE: 'Fuseau horaire',
ACCESS_POINT: 'Point d\'accès',
AP_PROVIDE: 'Activer le Point d\'Accès',
AP_PROVIDE_TEXT_1: 'toujours',
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
AP_PROVIDE_TEXT_3: 'jamais',
AP_PREFERRED_CHANNEL: 'Canal préféré',
AP_HIDE_SSID: 'Cacher le SSID',
AP_CLIENTS: 'AP Clients',
AP_MAX_CLIENTS: 'Max Clients',
AP_LOCAL_IP: 'IP locale',
NETWORK_SCAN: 'Scanner les réseaux WiFi',
IDLE: 'Inactif',
LOST: 'Perdu',
SCANNING: 'Scan en cours',
SCAN_AGAIN: 'Rescanner',
NETWORK_SCANNER: 'Scan réseau',
NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé',
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi',
TX_POWER: 'Puissance Tx',
HOSTNAME: 'Nom d\'hôte',
NETWORK_DISABLE_SLEEP: 'Désactiver le mode veille du WiFi',
NETWORK_LOW_BAND: 'Utiliser une bande passante WiFi plus faible',
NETWORK_USE_DNS: 'Activer le service mDNS',
NETWORK_ENABLE_CORS: 'Activer CORS',
NETWORK_CORS_ORIGIN: 'Origine CORS',
NETWORK_ENABLE_IPV6: 'Activer le support de l\'IPv6',
NETWORK_FIXED_IP: 'Utiliser une adresse IP fixe',
NETWORK_GATEWAY: 'Passerelle',
NETWORK_SUBNET: 'Masque de sous-réseau',
NETWORK_DNS: 'Serveurs DNS',
ADDRESS_OF: 'Adresse de {0}',
ADMIN: 'Admin',
GUEST: 'Invité',
NEW: 'Nouveau',
NEW_NAME_OF: 'Nouveau nom de {0}',
ENTITY: 'entité',
MIN: 'min',
MAX: 'max'
};
export default fr;

View File

@@ -0,0 +1,309 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const nl: Translation = {
LANGUAGE: 'Taal',
RETRY: 'Opnieuw proberen',
LOADING: 'Laden',
IS_REQUIRED: '{0} is verplicht',
SIGN_IN: 'Inloggen',
SIGN_OUT: 'Uitloggen',
USERNAME: 'Gebruikersnaam',
PASSWORD: 'Wachtwoord',
SU_PASSWORD: 'su Wachtwoord',
DASHBOARD: 'Dashboard',
SETTINGS_OF: '{0} Instellingen',
SAVED: 'opgeslagen',
HELP_OF: '{0} Help',
LOGGED_IN: 'Ingelogd als {name}',
PLEASE_SIGNIN: 'Log in om verder te gaan',
UPLOAD_SUCCESSFUL: 'Upload successvol',
DOWNLOAD_SUCCESSFUL: 'Download successvol',
INVALID_LOGIN: 'Logingegevens fout',
NETWORK: 'Netwerk',
SECURITY: 'Beveiliging',
ONOFF_CAP: 'AAN/UIT',
ONOFF: 'aan/uit',
TYPE: 'Type',
DESCRIPTION: 'Beschrijving',
ENTITIES: 'Entiteiten',
REFRESH: 'Ververs',
EXPORT: 'Export',
DEVICE_DETAILS: 'Device Gegevens',
ID_OF: '{0} ID',
DEVICE: 'Apparaat',
PRODUCT: 'Product',
VERSION: 'Versie',
BRAND: 'Merk',
ENTITY_NAME: 'Entiteit',
VALUE: '{{Waarde|waarde}}',
SHOW_FAV: 'alleen favorieten weergeven',
DEVICE_SENSOR_DATA: 'Apparaat en Sensor data',
DEVICES_SENSORS: 'Apparaten & Sensoren',
ATTACHED_SENSORS: 'Aangesloten EMS-ESP sensoren',
RUN_COMMAND: 'Call commando',
CHANGE_VALUE: 'Wijzig waarde',
CANCEL: 'Annuleren',
RESET: 'Reset',
SEND: 'Verzenden',
SAVE: 'Opslaan',
REMOVE: 'Verwijderen',
PROBLEM_UPDATING: 'Probleem met updaten',
PROBLEM_LOADING: 'Probleem met laden',
ACCESS_DENIED: 'Toegang geweigerd',
ANALOG_SENSOR: 'Analoge sensor',
ANALOG_SENSORS: 'Analoge Sensoren',
UPDATED_OF: '{0} Bijgewerkt',
UPDATE_OF: '{0} Bijwerken',
REMOVED_OF: '{0} Verwijderd',
DELETION_OF: '{0} Verwijder',
OFFSET: 'Offset',
FACTOR: 'Factor',
FREQ: 'Frequentie',
DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM',
STARTVALUE: 'Startwaarde',
WARN_GPIO: 'Waarschuwing: let op met het koppelen van de juiste GPIO pin!',
EDIT: 'Wijzigen',
SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatuur sensor',
TEMP_SENSORS: 'Temperatuur Sensoren',
WRITE_CMD_SENT: 'Schrijf commando sent', // TODO translate
WRITE_CMD_FAILED: 'Schrijf commando failed', // TODO translate
EMS_BUS_WARNING: 'EMS bus niet gevonden. Als deze waarschuwing blijft staan na een paar seconden dan loop de instellingen na en in het bijzonder het apparaat type profiel na.',
EMS_BUS_SCANNING: 'Scannen naar EMS apparaten...',
CONNECTED: 'Verbonden',
TX_ISSUES: 'Tx bus probleem. Probeer een andere Tx verzendmodus',
DISCONNECTED: 'Niet verbonden',
EMS_SCAN: 'Weet je zeker dat je een volledige EMS bus scan uit wilt voeren?',
EMS_BUS_STATUS: 'EMS busstatus',
ACTIVE_DEVICES: 'Actieve Apparaten & Sensoren',
EMS_DEVICE: 'EMS Apparaat',
SUCCESS: 'SUCCESS',
FAIL: 'MISLUKT',
QUALITY: 'QUALITEIT',
SCAN_DEVICES: 'Scannen naar nieuwe apparaten',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activiteitenstatus',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrammen ontvangen (Rx)',
'EMS Leesopdrachten (Tx)',
'EMS Schrijfopdrachten (Tx)',
'Temperatuursensoren uitgelezen',
'Analoge sensoren uitgelezen',
'MQTT publicaties',
'API calls',
'Syslog berichten'
],
NUM_DEVICES: '{num} Apparaat{{en}}',
NUM_TEMP_SENSORS: '{num} Temperatuursensor{{en}}',
NUM_ANALOG_SENSORS: '{num} Analoge sensor{{en}}',
NUM_DAYS: '{num} dag{{en}}',
NUM_SECONDS: '{num} second{{en}}',
NUM_HOURS: '{num} {{uur|uren}}',
NUM_MINUTES: '{num} {{minuut|minuten}}',
APPLICATION_SETTINGS: 'Applicatieinstellingen',
CUSTOMIZATION: 'Custom aanpassingen',
APPLICATION_RESTARTING: 'EMS-ESP herstarten',
INTERFACE_BOARD_PROFILE: 'Interface Apparaatprofiel',
BOARD_PROFILE_TEXT: 'Selecteer een vooraf ingesteld apparaat profiel uit de lijst of kies Eigen om zelf uw hardware te configureren',
BOARD_PROFILE: 'Apparaatprofiel',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
BUTTON: 'Toets',
TEMPERATURE: 'Temperatuur',
PHY_TYPE: 'Eth PHY Type',
TX_MODE: 'Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
DISABLED: 'Uitgeschakeld',
GENERAL_OPTIONS: 'Algemene Opties',
LANGUAGE_ENTITIES: 'Taal (voor apparaat entiteiten)',
HIDE_LED: 'Verberg LED',
ENABLE_TELNET: 'Activeer Telnet console',
ENABLE_ANALOG: 'Activeer analoge sensoren',
CONVERT_FAHRENHEIT: 'Converteer temperatuurwaarden naar Fahrenheit',
BYPASS_TOKEN: 'API Access Token authenticatie uitschakelen',
READONLY: 'Activeer read-only modus (blokkeert alle outgaande EMS Tx schrijf commandos)',
UNDERCLOCK_CPU: 'Underclock CPU snelheid',
ENABLE_SHOWER_TIMER: 'Activeer Douche Timer (tijdmeting)',
ENABLE_SHOWER_ALERT: 'Activeer Douchemelding',
TRIGGER_TIME: 'Trigger tijd',
COLD_SHOT_DURATION: 'Tijd Shot koud water',
FORMATTING_OPTIONS: 'Formatteringsopties',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean formaat dashboard',
BOOLEAN_FORMAT_API: 'Boolean formaat API/MQTT',
ENUM_FORMAT: 'Enum formaat API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Activeer Dallas parasitaire modus',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrammen in hexadecimaal',
ENABLE_SYSLOG: 'Activeer Syslog',
LOG_LEVEL: 'Log Level',
MARK_INTERVAL: 'Markeringsinterval',
SECONDS: 'seconden',
MINUTES: 'minuten',
HOURS: 'uren',
RESTART: 'Herstarten',
RESTART_TEXT: 'EMS-ESP dient opnieuw gestart te worden om de wijzingen toe te passen',
RESTART_CONFIRM: 'Weet je zeker dat je EMS-ESP wilt herstarten?',
COMMAND: 'Commando',
CUSTOMIZATIONS_RESTART: 'Alle custom profielen worden verwijderd. Herstarten...',
CUSTOMIZATIONS_FULL: 'Te veel entiteiten geselecteerd. Sla op in delen aub',
CUSTOMIZATIONS_SAVED: 'Custom aanpassingen opgeslagen',
CUSTOMIZATIONS_HELP_1: 'Selecteer een apparaat en pas de entiteiten aan door middel van de opties',
CUSTOMIZATIONS_HELP_2: 'Markeer as favoriet',
CUSTOMIZATIONS_HELP_3: 'Zet schrijfacties uit',
CUSTOMIZATIONS_HELP_4: 'Uitsluiten van MQTT en API',
CUSTOMIZATIONS_HELP_5: 'verberg van het Dashboard',
CUSTOMIZATIONS_HELP_6: 'remove from memory',
SELECT_DEVICE: 'Selecteer een apparaat',
SET_ALL: 'Alles aanzetten',
OPTIONS: 'Opties',
NAME: 'Naam',
CUSTOMIZATIONS_RESET: 'Weet je zeker dat je alle custom aanpassingen wilt verwijderen inclusief de custom instellingen voor analoge temperatuursensoren?',
DEVICE_ENTITIES: 'Apparaat Entiteiten',
USER_CUSTOMIZATION: 'Custom Instellingen',
SUPPORT_INFORMATION: 'Support Informatie',
CLICK_HERE: 'Klik Hier',
HELP_INFORMATION_1: 'Bezoek de online wiki om instructies te vinden om EMS-ESP te configureren',
HELP_INFORMATION_2: 'Voor de live community ga naar de Discord server',
HELP_INFORMATION_3: 'Om een nieuwe feature te vragen of een bug te rapporteren',
HELP_INFORMATION_4: 'zorg dat je ook je systeem details zijn toevoeged voor een sneller antwoord',
HELP_INFORMATION_5: 'EMS-ESP is een gratis en open source project. Steun ons met een Star op Github!',
SUPPORT_INFO: 'Support Info',
UPLOAD_OF: '{0} Upload',
UPLOAD: 'Upload',
DOWNLOAD: 'Download',
ABORTED: 'afgebroken',
FAILED: 'mislukt',
SUCCESSFUL: 'successvol',
SYSTEM: 'Systeem',
LOG_OF: '{0} Log',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
SYSTEM_VERSION_RUNNING: 'op dit moment draai je versie',
SYSTEM_APPLY_FIRMWARE: 'om de nieuwe firmware te activeren',
CLOSE: 'Sluiten',
USE: 'Gebruik',
FACTORY_RESET: 'Fabrieksinstellingen',
SYSTEM_FACTORY_TEXT: 'Gateway is gereset en start nu weer op met fabrieksinstellingen',
SYSTEM_FACTORY_TEXT_DIALOG: 'Weet je zeker dat je een reset naar fabrieksinstellingen uit wilt voeren?',
VERSION_CHECK: 'Versie Check',
THE_LATEST: 'De laatste',
OFFICIAL: 'official',
DEVELOPMENT: 'development',
VERSION_IS: 'versie is',
RELEASE_NOTES: 'release notes',
EMS_ESP_VER: 'EMS-ESP Version',
PLATFORM: 'Apparaat (Platform / SDK)',
UPTIME: 'Systeem Uptime',
CPU_FREQ: 'CPU Frequency',
HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size / Speed)',
APPSIZE: 'Application (Used / Free)',
FILESYSTEM: 'File System (Used / Free)',
BUFFER_SIZE: 'Max Buffer Size',
COMPACT: 'Compact',
ENABLE_OTA: 'Acitveer OTA Updates',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download alle custom instellingen',
DOWNLOAD_SETTINGS_TEXT: 'Download de applicatie settings. Wees voorzichting met het delen van dit bestand want het bevat o.a. de wachtwoorden in plain text',
UPLOAD_TEXT: 'Upload een nieuwe firmware (.bin) file, instellingen of custom instellingen (.json) bestand hieronder',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Sleep bestand hierheen of klik hier',
ERROR: 'Onverwachte fout, probeer opnieuw',
TIME_SET: 'Tijd ingesteld',
MANAGE_USERS: 'Beheer Gebruikers',
IS_ADMIN: 'is Admin',
USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren',
ADD: 'Toevoegen',
ACCESS_TOKEN_FOR: 'Access Token voor',
ACCESS_TOKEN_TEXT: 'Het token hieronder wordt gebruikt voor de REST API calls die authorisatie nodig hebben. Het kan zowel als Bearer token in de Authorization header of in acccess_token URL query parameter gebruikt worden',
GENERATING_TOKEN: 'Token aan het genereren',
USER: 'Gebruiker',
MODIFY: 'Aanpassen',
SU_TEXT: 'Het su (super user) wachtwoord wordt gebruikt om authorisatie tokens te signeren en ook om admin privileges te activeren in de console.',
NOT_ENABLED: 'Niet geactiveerd',
ERRORS_OF: '{0} Foutmeldingen',
DISCONNECT_REASON: 'Verbinding verbroken vanwege',
ENABLE_MQTT: 'Activeer MQTT',
BROKER: 'Broker',
CLIENT: 'Client',
BASE_TOPIC: 'Base',
OPTIONAL: 'Optioneel',
FORMATTING: 'Formatteren',
MQTT_FORMAT: 'Topic/Payload Formattering',
MQTT_NEST_1: 'Genest in 1 topic',
MQTT_NEST_2: 'Als individuele topics',
MQTT_RESPONSE: 'Publiceer commando output naar een `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publiceer enkele waarde topics on change',
MQTT_PUBLISH_TEXT_2: 'Publiceer naar commando topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Activeer MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix voor de Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publicatie intervallen',
MQTT_INT_BOILER: 'CV ketels en warmtepompen',
MQTT_INT_THERMOSTATS: 'Thermostaten',
MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules',
MQTT_INT_HEARTBEAT: 'Heartbeat',
MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Default',
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)', // TODO translate
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
MQTT_CLEAN_SESSION: 'Clean Session aan',
MQTT_RETAIN_FLAG: 'Retain flag aan',
INACTIVE: 'Inactief',
ACTIVE: 'Actief',
UNKNOWN: 'Onbekend',
SET_TIME: 'Tijd instellen',
SET_TIME_TEXT: 'Geef de locale datum en tijd in',
LOCAL_TIME: 'Locale Tijd',
UTC_TIME: 'UTC Tijd',
ENABLE_NTP: 'Activeer NTP',
NTP_SERVER: 'NTP Server',
TIME_ZONE: 'Tijdzone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Activeer Access Point',
AP_PROVIDE_TEXT_1: 'altijd',
AP_PROVIDE_TEXT_2: 'als WiFi niet is verbonden',
AP_PROVIDE_TEXT_3: 'nooit',
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
AP_HIDE_SSID: 'SSID verbergen',
AP_CLIENTS: 'AP Clients',
AP_MAX_CLIENTS: 'Max Clients',
AP_LOCAL_IP: 'Local IP',
NETWORK_SCAN: 'Scan WiFi Networken',
IDLE: 'Idle',
LOST: 'Verloren',
SCANNING: 'Scannen',
SCAN_AGAIN: 'Opnieuw scannen',
NETWORK_SCANNER: 'Netwerk Scanner',
NETWORK_NO_WIFI: 'Geen WiFi networken gevonden',
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
TX_POWER: 'Tx Vermogen',
HOSTNAME: 'Hostname',
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
NETWORK_LOW_BAND: 'Lagere WiFi bandbreedte gebruiken',
NETWORK_USE_DNS: 'Activeer mDNS Service',
NETWORK_ENABLE_CORS: 'Activeer CORS',
NETWORK_CORS_ORIGIN: 'CORS origin',
NETWORK_ENABLE_IPV6: 'Activeer IPv6 support',
NETWORK_FIXED_IP: 'Gebruik vast IP addres',
NETWORK_GATEWAY: 'Gateway',
NETWORK_SUBNET: 'Subnetmasker',
NETWORK_DNS: 'DNS Servers',
ADDRESS_OF: '{0} Address',
ADMIN: 'Admin',
GUEST: 'Gast',
NEW: 'Nieuwe',
NEW_NAME_OF: 'Hernoem {0}',
ENTITY: 'Entiteit',
MIN: 'min',
MAX: 'max'
};
export default nl;

View File

@@ -0,0 +1,309 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const no: Translation = {
LANGUAGE: 'Språk',
RETRY: 'Forsøk igjen',
LOADING: 'Laster',
IS_REQUIRED: '{0} er nødvendig',
SIGN_IN: 'Logg inn',
SIGN_OUT: 'Logg ut',
USERNAME: 'Brukernavn',
PASSWORD: 'Passord',
SU_PASSWORD: 'su Passord',
DASHBOARD: 'Dashboard',
SETTINGS_OF: '{0} Innstillinger',
SAVED: 'lagret',
HELP_OF: '{0} Hjelp',
LOGGED_IN: 'Logget in som {name}',
PLEASE_SIGNIN: 'Venligst logge inn for å fortsetta',
UPLOAD_SUCCESSFUL: 'Opplasting lykkes',
DOWNLOAD_SUCCESSFUL: 'Nedlasting lykkes',
INVALID_LOGIN: 'Ugyldig innlogging',
NETWORK: 'Nettverk',
SECURITY: 'Sikkerhet',
ONOFF_CAP: 'PÅ/AV',
ONOFF: 'på/av',
TYPE: 'Type',
DESCRIPTION: 'Beskrivelse',
ENTITIES: 'Ojekter',
REFRESH: 'Oppdater',
EXPORT: 'Eksport',
DEVICE_DETAILS: 'Enhetsdetaljer',
ID_OF: '{0}-ID',
DEVICE: 'Enhets',
PRODUCT: 'Produkt',
VERSION: 'Versjon',
BRAND: 'Fabrikat',
ENTITY_NAME: 'Objektsnavn',
VALUE: '{{Verdi|verdi}}',
SHOW_FAV: ' Vis kun favoritter',
DEVICE_SENSOR_DATA: 'Enheter og Sensordata',
DEVICES_SENSORS: 'Enheter og Sensorer',
ATTACHED_SENSORS: 'Tilkoblede EMS-ESP Sensorer',
RUN_COMMAND: 'Kjør kommando',
CHANGE_VALUE: 'Endre Verdi',
CANCEL: 'Avbryt',
RESET: 'Nullstill',
SEND: 'Send',
SAVE: 'Lagre',
REMOVE: 'Fjern',
PROBLEM_UPDATING: 'Problem med oppdatering',
PROBLEM_LOADING: 'Problem med opplasting',
ACCESS_DENIED: 'Tilgang nektet',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analoge Sensorer',
UPDATED_OF: '{0} Oppdatert',
UPDATE_OF: '{0} Oppdater',
REMOVED_OF: '{0} Slettet',
DELETION_OF: '{0} Sletting',
OFFSET: 'Kompensering',
FACTOR: 'Faktor',
FREQ: 'Frekvens',
DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM',
STARTVALUE: 'Startverdi',
WARN_GPIO: 'Advarsel: vær forsiktig ved aktivering av GPIO!',
EDIT: 'Endre',
SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperaturesensorer',
WRITE_CMD_SENT: 'Skriv kommando sent', // TODO translate
WRITE_CMD_FAILED: 'Skriv kommando failed', // TODO translate
EMS_BUS_WARNING: 'EMS bussen koblet ned. Hvis denne advarselen fortsetter etter noen f¨sekunder sjekk instillinger og prosessorkort',
EMS_BUS_SCANNING: 'Søker etter EMS enheter...',
CONNECTED: 'Tilkoblet',
TX_ISSUES: 'Tx problemer - prøv en annen Tx Modus',
DISCONNECTED: 'Frakoblet',
EMS_SCAN: 'Er du sikker på du vil starte full søking av EMS bussen?',
EMS_BUS_STATUS: 'EMS Buss Status',
ACTIVE_DEVICES: 'Aktive Enheter og Sensorer',
EMS_DEVICE: 'EMS Enhet',
SUCCESS: 'VELLYKKET',
FAIL: 'MISLYKKET',
QUALITY: 'KVALITET',
SCAN_DEVICES: 'Søk etter nye enheter',
EMS_BUS_STATUS_TITLE: 'EMS Buss & Aktivitet Status',
SCAN: 'Søk',
STATUS_NAMES: [
'EMS Telegrammer Mottatt (Rx)',
'EMS Lest (Tx)',
'EMS Skrevet (Tx)',
'Temperatur Sensor Lest',
'Analog Sensor Lest',
'MQTT Publiseringer',
'API Anrop',
'Syslog Meldinger'
],
NUM_DEVICES: '{num} Enhet{{er}}',
NUM_TEMP_SENSORS: '{num} Temperatursensor{{er}}',
NUM_ANALOG_SENSORS: '{num} Analogsensor{{er}}',
NUM_DAYS: '{num} sag{{er}}',
NUM_SECONDS: '{num} sekund{{er}}',
NUM_HOURS: '{num} time{{r}}',
NUM_MINUTES: '{num} minutt{{er}}',
APPLICATION_SETTINGS: 'Innstillinger',
CUSTOMIZATION: 'Tilpasninger',
APPLICATION_RESTARTING: 'EMS-ESP restarter',
INTERFACE_BOARD_PROFILE: 'Interface Prosessor Profil',
BOARD_PROFILE_TEXT: 'Velg en pre-konfigurert prosessor profil fra listen under eller velg Tilpasset for å konfigurere dine egne innstillinger',
BOARD_PROFILE: 'Prosessor Profil',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
BUTTON: 'Knapp',
TEMPERATURE: 'Temperatur',
PHY_TYPE: 'Eth PHY Type',
DISABLED: 'avslått',
TX_MODE: 'Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Generelle Innstillinger',
LANGUAGE_ENTITIES: 'Språk (for objekter)',
HIDE_LED: 'Skjul LED',
ENABLE_TELNET: 'Aktiver Telnet',
ENABLE_ANALOG: 'Aktiver Analoge Sensorer',
CONVERT_FAHRENHEIT: 'Konverter temperatur til Fahrenheit',
BYPASS_TOKEN: 'Utelat Aksess Token authorisering av API kall',
READONLY: 'Aktiver read-only modus (blokker all EMS Tx Skriving)',
UNDERCLOCK_CPU: 'Underklokking av prosessorhastighet',
ENABLE_SHOWER_TIMER: 'Aktiver Dusjtimer',
ENABLE_SHOWER_ALERT: 'Aktiver Dusj-varsling',
TRIGGER_TIME: 'Aktiveringstid',
COLD_SHOT_DURATION: 'Tid på kaldt vann',
FORMATTING_OPTIONS: 'Formatteringsalternativs',
BOOLEAN_FORMAT_DASHBOARD: 'Bool Format Dashboard',
BOOLEAN_FORMAT_API: 'Bool Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Indeks',
ENABLE_PARASITE: 'Aktiver parasitt strømforsyning',
LOGGING: 'Logging',
LOG_HEX: 'Logg EMS telegrammer i hexadesimal',
ENABLE_SYSLOG: 'Aktiver Syslog',
LOG_LEVEL: 'Log Level',
MARK_INTERVAL: 'Oppdateringsintervall',
SECONDS: 'sekunder',
MINUTES: 'minutter',
HOURS: 'timer',
RESTART: 'Omstart',
RESTART_TEXT: 'EMS-ESP må omstartes for å iverksette endrede systeminstillinger',
RESTART_CONFIRM: 'Er du sikker på at du vil omstarte EMS-ESP?',
COMMAND: 'Kommando',
CUSTOMIZATIONS_RESTART: 'Alle tilpasninger har blitt slettet. Restarter...',
CUSTOMIZATIONS_FULL: 'Antall valgte objekter for høyt. Largre i mindre antall om gangen',
CUSTOMIZATIONS_SAVED: 'Tilpasninger lagret',
CUSTOMIZATIONS_HELP_1: 'Velg en enhet og tilpass underenheter med hjelp av alternativer eller velg å gi nytt navn',
CUSTOMIZATIONS_HELP_2: 'merk som favoritt',
CUSTOMIZATIONS_HELP_3: 'inaktiviser skriving',
CUSTOMIZATIONS_HELP_4: 'ekskludere fra MQTT og API',
CUSTOMIZATIONS_HELP_5: 'gjemme fra Dashboard',
CUSTOMIZATIONS_HELP_6: 'remove from memory',
SELECT_DEVICE: 'Velg en enhet',
SET_ALL: 'sett alle',
OPTIONS: 'Alternativ',
NAME: 'Navn',
CUSTOMIZATIONS_RESET: 'Er du sikker på att du vil fjerne tilpassninger inkludert innstillinger for Temperatur og Analoge sensorer?',
DEVICE_ENTITIES: 'Enhets objekter',
USER_CUSTOMIZATION: 'Brukertilpasninger',
SUPPORT_INFORMATION: 'Supportinformasjon',
CLICK_HERE: 'Klikk her',
HELP_INFORMATION_1: 'Besøk wiki for instruksjoner for å konfigurere EMS-ESP',
HELP_INFORMATION_2: 'For community-support besøk vår Discord-server',
HELP_INFORMATION_3: 'For å be om en ny funksjon eller melde feil',
HELP_INFORMATION_4: 'husk å laste ned og legg ved din systeminformasjon for en raskere respons når du rapporterer et problem',
HELP_INFORMATION_5: 'EMS-ESP er gratis og åpen kildekode. Bidra til utviklingen ved å gi oss en stjerne på GitHub!',
SUPPORT_INFO: 'Supportinfo',
UPLOAD_OF: '{0} Opplasning',
UPLOAD: 'Opplasning',
DOWNLOAD: 'Nedlasting',
ABORTED: 'avbrutt',
FAILED: 'feilet',
SUCCESSFUL: 'vellykket',
SYSTEM: 'System',
LOG_OF: '{0} Logg',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Opp/Nedlasting',
SYSTEM_VERSION_RUNNING: 'Du benytter versjon',
SYSTEM_APPLY_FIRMWARE: 'for å aktivere ny firmware',
CLOSE: 'Steng',
USE: 'Bruk',
FACTORY_RESET: 'Sett tilbake til fabrikkinstilling',
SYSTEM_FACTORY_TEXT: 'Enhet har blitt satt tilbake til fabrikkinstilling og vil restarte',
SYSTEM_FACTORY_TEXT_DIALOG: 'Er du sikker på at du vil resette enheten til fabrikkinstillinger?',
VERSION_CHECK: 'Versjonsjekk',
THE_LATEST: 'Den nyeste',
OFFICIAL: 'official',
DEVELOPMENT: 'development',
VERSION_IS: 'versjonen er',
RELEASE_NOTES: 'release notes',
EMS_ESP_VER: 'EMS-ESP Version',
PLATFORM: 'Enhet (Platform / SDK)',
UPTIME: 'System Oppetid',
CPU_FREQ: 'CPU Frekvens',
HEAP: 'Heap (Ledig / Max Allokert)',
PSRAM: 'PSRAM (Størrelse / Ledig)',
FLASH: 'Flash Chip (Størrelse / Hastighet)',
APPSIZE: 'Applikasjon (Brukt / Ledig)',
FILESYSTEM: 'File System (Brukt / Ledig)',
BUFFER_SIZE: 'Max Buffer Størrelse',
COMPACT: 'Komprimere',
ENABLE_OTA: 'Aktiviser OTA oppdateringer',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Last ned objektstilpasninger',
DOWNLOAD_SETTINGS_TEXT: 'Last ned applikasjonskonfigurasjon. Vær varsom med å dele fila da den inneholder passord og annen sensitiv system informasjon',
UPLOAD_TEXT: 'Last opp en ny firmware (.bin) fil, innstillinger eller tilpassninger (.json) fil nedenfor',
UPLOADING: 'Opplasting',
UPLOAD_DROP_TEXT: 'Slipp fil eller klikk her',
ERROR: 'Ukjent feil, prøv igjen',
TIME_SET: 'Still in tid',
MANAGE_USERS: 'Administrer Brukere',
IS_ADMIN: 'er Admin',
USER_WARNING: 'Du må ha minst en admin bruker konfigurert',
ADD: 'Legg til',
ACCESS_TOKEN_FOR: 'Aksess Token for',
ACCESS_TOKEN_TEXT: 'Token nedenfor benyttes med REST API-kall som krever autorisering. Den kan sendes med enten som en Bearer token i Authorization-headern eller i access_token URL query-parameter.',
GENERATING_TOKEN: 'Generer token',
USER: 'Bruker',
MODIFY: 'Endre',
SU_TEXT: 'su brukeren (super user) passord benyttes for å signere autentiserings token samt å tillate admin privileger i konsoll modus.',
NOT_ENABLED: 'Ikke aktiv',
ERRORS_OF: '{0} Feil',
DISCONNECT_REASON: 'Årsak til nedkobling',
ENABLE_MQTT: 'Aktiver MQTT',
BROKER: 'Broker',
CLIENT: 'Client',
BASE_TOPIC: 'Base',
OPTIONAL: 'Valgfritt',
FORMATTING: 'Formatering',
MQTT_FORMAT: 'Topic/Payload Format',
MQTT_NEST_1: 'Nestet i en topic',
MQTT_NEST_2: 'Som individuelle topics',
MQTT_RESPONSE: 'Publiser kommandoer til en `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publiser singel verdi topics ved endringer',
MQTT_PUBLISH_TEXT_2: 'Publiser til kommando topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Aktiver MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefiks for Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publiseringsintervall',
MQTT_INT_BOILER: 'Fyr/Varmepumpe',
MQTT_INT_THERMOSTATS: 'Termostat',
MQTT_INT_SOLAR: 'Solpaneler',
MQTT_INT_MIXER: 'Blandeventil',
MQTT_INT_HEARTBEAT: 'Heartbeat',
MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Standard',
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)', // TODO translate
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
MQTT_CLEAN_SESSION: 'Benytt Clean Session',
MQTT_RETAIN_FLAG: 'Alltid sett Retain flag',
INACTIVE: 'Innaktiv',
ACTIVE: 'Aktiv',
UNKNOWN: 'Ukjent',
SET_TIME: 'Sett Tid',
SET_TIME_TEXT: 'Skriv inn dato og klokke nedenfor',
LOCAL_TIME: 'Lokaltid',
UTC_TIME: 'UTC Tid',
ENABLE_NTP: 'Aktiver NTP',
NTP_SERVER: 'NTP Server',
TIME_ZONE: 'Tidssone',
ACCESS_POINT: 'Aksesspunkt',
AP_PROVIDE: 'Aktiver Aksesspunkt',
AP_PROVIDE_TEXT_1: 'alltid',
AP_PROVIDE_TEXT_2: 'når WiFi er utilgjengelig',
AP_PROVIDE_TEXT_3: 'aldri',
AP_PREFERRED_CHANNEL: 'Foretrukket kanal',
AP_HIDE_SSID: 'Skjul SSID',
AP_CLIENTS: 'AP Clients',
AP_MAX_CLIENTS: 'Max Clients',
AP_LOCAL_IP: 'Local IP',
NETWORK_SCAN: 'Søk etter trådløst nettverk',
IDLE: 'Klar',
LOST: 'Mistet',
SCANNING: 'Søker',
SCAN_AGAIN: 'Søk igjen',
NETWORK_SCANNER: 'Nettverk Scanner',
NETWORK_NO_WIFI: 'Ingen trådløse nett funnet',
NETWORK_BLANK_SSID: 'la feltet være blankt for å deaktivisere trådløst nettverk',
TX_POWER: 'Tx Effekt',
HOSTNAME: 'Hostname',
NETWORK_DISABLE_SLEEP: 'Hindre at trådløst nettverk går i Sleep Mode',
NETWORK_LOW_BAND: 'Benytt smalere båndbredde på trådløst nettverk',
NETWORK_USE_DNS: 'Aktiviser mDNS Service',
NETWORK_ENABLE_CORS: 'Aktiviser CORS',
NETWORK_CORS_ORIGIN: 'CORS origin',
NETWORK_ENABLE_IPV6: 'Aktiviser IPv6 støtte',
NETWORK_FIXED_IP: 'Benytt statisk IP adresse',
NETWORK_GATEWAY: 'Gateway',
NETWORK_SUBNET: 'Nettverksmaske',
NETWORK_DNS: 'DNS Servers',
ADDRESS_OF: '{0} Address',
ADMIN: 'Admin',
GUEST: 'Gjest',
NEW: 'Ny',
NEW_NAME_OF: 'Bytt navn {0}',
ENTITY: 'Entitet',
MIN: 'min',
MAX: 'max'
};
export default no;

View File

@@ -0,0 +1,309 @@
import type { BaseTranslation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const pl: BaseTranslation = {
LANGUAGE: 'Język',
RETRY: 'Ponów',
LOADING: 'Ładowanie',
IS_REQUIRED: 'Pole {0} nie może być puste!',
SIGN_IN: 'Zaloguj się',
SIGN_OUT: 'Wyloguj się',
USERNAME: '{{Użytkownik|Nazwa użytkownika|}}',
PASSWORD: 'Hasło',
SU_PASSWORD: 'Hasło "su"',
DASHBOARD: 'Pulpit',
SETTINGS_OF: 'Ustawienia {0}',
SAVED: 'zostały zapisane.',
HELP_OF: 'Pomoc {0}',
LOGGED_IN: 'Zalogowano użytkownika {name}.',
PLEASE_SIGNIN: 'Zaloguj się aby kontynuować.',
UPLOAD_SUCCESSFUL: 'Wysyłanie zakończone.',
DOWNLOAD_SUCCESSFUL: 'Pobieranie zakończone.',
INVALID_LOGIN: 'Nieprawidłowy użytkownik lub hasło!',
NETWORK: '{{Sieć|sieci|}}',
SECURITY: '{{B|b|}}ezpieczeństw{{o|a|}}',
ONOFF_CAP: 'wł./wył.',
ONOFF: 'włączono/wyłączono',
TYPE: 'Typ',
DESCRIPTION: 'Opis',
ENTITIES: 'Encje',
REFRESH: 'Odśwież',
EXPORT: 'Eksportuj',
DEVICE_DETAILS: 'Szczegóły urządzenia',
ID_OF: 'ID {0}',
DEVICE: 'urządzenia',
PRODUCT: 'produktu',
BRAND: 'Marka',
VERSION: 'Wersja',
ENTITY_NAME: 'Nazwa encji',
VALUE: '{{W|w|}}artość',
SHOW_FAV: 'Pokaż tylko "ulubione"',
DEVICE_SENSOR_DATA: 'Dane z urządzeń i czujników',
DEVICES_SENSORS: 'Urządzenia i czujniki',
ATTACHED_SENSORS: 'Urządzenia podłączone do EMS-ESP (czujniki temperatury/analogowe/cyfrowe, wyjścia cyfrowe)',
RUN_COMMAND: 'Wykonaj komendę',
CHANGE_VALUE: 'Zmień wartość',
CANCEL: 'Anuluj',
RESET: 'Reset{{uj|owanie|}}',
SEND: 'Wyślij',
SAVE: 'Zapisz',
REMOVE: 'Usuń',
PROBLEM_UPDATING: 'Problem z uaktualnieniem!',
PROBLEM_LOADING: 'Problem z załadowaniem!',
ACCESS_DENIED: 'Brak dostępu!',
ANALOG_SENSOR: 'urządzenia podłączonego do EMS-ESP',
ANALOG_SENSORS: 'Urządzenia podłączone do EMS-ESP',
UPDATED_OF: 'Zaktualizowano ustawienia {0}.',
UPDATE_OF: 'Aktualizacja {0}',
REMOVED_OF: 'Usunięto ustawienia {0}.',
DELETION_OF: 'Kasowanie {0}',
OFFSET: 'Korekta ±',
FACTOR: 'Mnożnik',
FREQ: 'Częstotliwość',
DUTY_CYCLE: 'Wypełnienie',
UNIT: 'J.m.',
STARTVALUE: 'Wartość początkowa',
WARN_GPIO: 'Uwaga! Zachowaj ostrożność przypisując GPIO do urządzenia!',
EDIT: 'Edycja',
SENSOR: 'czujnika',
TEMP_SENSOR: 'czujnika temperatury',
TEMP_SENSORS: 'Czujniki temperatury 1-Wire®',
WRITE_CMD_SENT: 'Komenda zapisu została wysłana.',
WRITE_CMD_FAILED: 'Komenda zapisu nie powiodła się!',
EMS_BUS_WARNING: 'Brak połączenia z magistralą EMS. Jeśli ten błąd występuje dłużej niż kilka sekund, sprawdź ustawienia oraz profil płytki interfejsu.',
EMS_BUS_SCANNING: 'Trwa skanowanie urządzeń na magistrali EMS...',
CONNECTED: '{{połączono|połączenie|}}',
TX_ISSUES: 'problem z zapisem na magistralę EMS, spróbuj wybrać inny "Tryb transmisji (Tx)"',
DISCONNECTED: 'brak połączenia',
EMS_SCAN: 'Czy na pewno wykonać pełne skanowanie magistrali EMS?',
EMS_BUS_STATUS: 'Status magistrali EMS',
ACTIVE_DEVICES: 'Aktywne urządzenia i czujniki',
EMS_DEVICE: 'Urządzenie EMS',
SUCCESS: 'Udane',
FAIL: 'Nieudane',
QUALITY: 'Jakość',
SCAN_DEVICES: 'Wyszukiwanie nowych urządzeń',
EMS_BUS_STATUS_TITLE: 'Aktywność',
SCAN: 'Skanuj',
STATUS_NAMES: [
'EMS, telegramy odebrane (Rx)',
'EMS, wysłane telegramy "odczyt" (Tx)',
'EMS, wysłane telegramy "zapis" (Tx)',
'Odczyty czujników temperatury 1-Wire®',
'Odczyty czujników analogowych i cyfrowych',
'Publikacje MQTT',
'Wywołania API',
'Wpisy w SysLog'
],
NUM_DEVICES: '{num} urządze{{ń|nie|nia|nia|ń}} EMS',
NUM_TEMP_SENSORS: '{num} czujni{{ków|k|ki|ki|ków}} temperatury',
NUM_ANALOG_SENSORS: '{num} inn{{ych|e|e|e|ych}} urządze{{ń|nie|nia(two)|nia|ń}} podłączon{{ych|e|e|e|ych}} do EMS-ESP',
NUM_DAYS: '{num} d{{ni|zień|ni|ni|ni}}',
NUM_SECONDS: '{num} sekun{{d|da|dy|dy|d}}',
NUM_HOURS: '{num} godzi{{n|na|ny|ny|n}}',
NUM_MINUTES: '{num} minu{{t|ta|ty|ty|t}}',
APPLICATION_SETTINGS: 'Ustawienia aplikacji',
CUSTOMIZATION: 'Personalizacja',
APPLICATION_RESTARTING: 'Trwa ponowne uruchamianie',
INTERFACE_BOARD_PROFILE: 'Profil płytki interfejsu',
BOARD_PROFILE_TEXT: 'Wybierz z listy gotowy profil płytki interfejsu lub "własny..." i samodzielnie skonfiguruj posiadany sprzęt.',
BOARD_PROFILE: 'Profil płytki',
CUSTOM: 'własny',
GPIO_OF: 'GPIO {0}',
BUTTON: 'przycisku',
TEMPERATURE: '1-Wire®',
PHY_TYPE: 'Typ układu ethernetowego (PHY)',
DISABLED: '{{wyłączono|brak|}}',
TX_MODE: 'Tryb transmisji (Tx)',
EMS_BUS: '{{magistrali EMS|na magistrali|}}',
HARDWARE: 'sprzętowy',
GENERAL_OPTIONS: 'Opcje podstawowe',
LANGUAGE_ENTITIES: 'Język encji',
HIDE_LED: 'Wyłącz LED',
ENABLE_TELNET: 'Aktywuj dostęp dla konsoli Telnet',
ENABLE_ANALOG: 'Aktywuj urządzenia GPIO (czujniki analogowe i cyfrowe oraz wyjścia cyfrowe)',
CONVERT_FAHRENHEIT: 'Konwertuj temperatury do skali Fahrenheita',
BYPASS_TOKEN: 'Pomiń autoryzację tokenem w wywołaniach API',
READONLY: 'Tryb pracy "tylko do odczytu" (blokuje wszystkie komendy zapisu na magistralę EMS)',
UNDERCLOCK_CPU: 'Obniż taktowanie CPU',
ENABLE_SHOWER_TIMER: 'Aktywuj minutnik prysznica',
ENABLE_SHOWER_ALERT: 'Aktywuj alarm prysznica',
TRIGGER_TIME: 'Wyzwalaj po czasie',
COLD_SHOT_DURATION: 'Czas trwania tryśnięcia zimnej wody',
FORMATTING_OPTIONS: 'Opcje formatowania',
BOOLEAN_FORMAT_DASHBOARD: 'Wartości dwustanowe na pulpicie',
BOOLEAN_FORMAT_API: 'Wartości dwustanowe w API/MQTT',
ENUM_FORMAT: 'Wartości z listy w API/MQTT',
INDEX: 'indeks',
ENABLE_PARASITE: 'Aktywuj zasilanie pasożytnicze',
LOGGING: 'Logowanie',
LOG_HEX: 'Loguj telegramy EMS w systemie szesnastkowym (hex)',
ENABLE_SYSLOG: 'Aktywuj SysLog',
LOG_LEVEL: 'Poziom logowania',
MARK_INTERVAL: 'Znaczniki interwałów (0=brak)',
SECONDS: 'sekund',
MINUTES: 'minut',
HOURS: 'godzin',
RESTART: 'Restart',
RESTART_TEXT: 'Aby zastosować wprowadzone zmiany interfejs EMS-ESP musi zostać zrestartowany.',
RESTART_CONFIRM: 'Jesteś pewien, że chcesz zrestartować interfejs EMS-ESP?',
COMMAND: 'KOMENDA',
CUSTOMIZATIONS_RESTART: 'Wszystkie personalizacje zostały usunięte. Restartuję...',
CUSTOMIZATIONS_FULL: 'Wybrano za dużo obiektów. Wprowadź zmiany w mniejszych partiach.',
CUSTOMIZATIONS_SAVED: 'Personalizacje zostały zapisane.',
CUSTOMIZATIONS_HELP_1: 'Wybierz urządzenie EMS, dostosuj opcje lub kliknij by zmienić nazwę encji.',
CUSTOMIZATIONS_HELP_2: 'oznacz jako ulubioną',
CUSTOMIZATIONS_HELP_3: 'zablokuj akcje zapisu',
CUSTOMIZATIONS_HELP_4: 'wyklucz z MQTT i API',
CUSTOMIZATIONS_HELP_5: 'ukryj na pulpicie',
CUSTOMIZATIONS_HELP_6: 'remove from memory',
SELECT_DEVICE: 'wybierz urządzenie',
SET_ALL: 'Ustaw wszystko jako',
OPTIONS: 'Opcje',
NAME: '{{Nazwa|nazwa|}}',
CUSTOMIZATIONS_RESET: 'Czy jesteś pewien, że chcesz usunąć wszystkie personalizacje łącznie z ustawieniami dla czujników temperatury 1-Wire® i urządzeń podłączonych do EMS-ESP?',
DEVICE_ENTITIES: 'Encje urządzenia',
USER_CUSTOMIZATION: 'Personalizacje użytkownika',
SUPPORT_INFORMATION: 'Informacje dotyczące wsparcia',
CLICK_HERE: 'Kliknij tu',
HELP_INFORMATION_1: 'Aby uzyskać instrukcje dotyczące konfiguracji EMS-ESP skorzystaj z wiki w internecie',
HELP_INFORMATION_2: 'Aby dołączyć do naszego serwera Discord i komunikować się na żywo ze społecznością',
HELP_INFORMATION_3: 'Aby zaproponować nową funkcjonalność lub zgłosić problem',
HELP_INFORMATION_4: 'Zgłaszając problem, nie zapomnij dołączyć informacji o swoim systemie!',
HELP_INFORMATION_5: 'EMS-ESP jest darmowym projektem typu open-source. Aby go wesprzeć, rozważ przyznanie nam gwiazdki na Github!',
SUPPORT_INFO: 'Pobierz informacje',
UPLOAD_OF: 'Wysyłanie {0}',
UPLOAD: 'Wysyłanie',
DOWNLOAD: '{{P|p||P}}obier{{anie|z||z}}',
ABORTED: 'zostało przerwane!',
FAILED: 'nie powiodło się!',
SUCCESSFUL: 'powiodło się.',
SYSTEM: '{{S|s||s}}yste{{m|mu||mowy}}',
LOG_OF: 'Log {0}',
STATUS_OF: 'Status {0}',
UPLOAD_DOWNLOAD: 'Przesyłanie plików',
SYSTEM_VERSION_RUNNING: 'Obecnie zainstalowana wersja to:',
SYSTEM_APPLY_FIRMWARE: '',
CLOSE: 'Zamknij',
USE: 'Aby zaktualizować firmware skorzystaj z funkcji',
FACTORY_RESET: 'Ustawienia fabryczne',
SYSTEM_FACTORY_TEXT: 'Interfejs EMS-ESP został przywrócony do ustawień fabrycznych i zostanie teraz ponownie uruchomiony.',
SYSTEM_FACTORY_TEXT_DIALOG: 'Czy jesteś pewien, że chcesz przywrócić ustawienia fabryczne interfejsu EMS-ESP? ',
VERSION_CHECK: 'Sprawd{{ź|zanie|}} wersj{{ę|i|}}',
THE_LATEST: 'Najnowsza',
OFFICIAL: 'oficjalna',
DEVELOPMENT: 'testowa',
VERSION_IS: 'wersja to',
RELEASE_NOTES: 'lista zmian',
EMS_ESP_VER: 'Wersja EMS-ESP',
PLATFORM: 'Urządzenie (platforma / SDK)',
UPTIME: 'Czas działania systemu',
CPU_FREQ: 'Taktowanie CPU',
HEAP: 'HEAP (wolne / maksymalny przydział)',
PSRAM: 'PSRAM (rozmiar / wolne)',
FLASH: 'Flash (rozmiar / taktowanie)',
APPSIZE: 'Aplikacja (wykorzystane / wolne)',
FILESYSTEM: 'System plików (wykorzystane / wolne)',
BUFFER_SIZE: 'Maksymalna pojemność bufora (ilość wpisów)',
COMPACT: 'Kompaktowy',
ENABLE_OTA: 'Aktywuj aktualizację OTA',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Pobierz personalizacje',
DOWNLOAD_SETTINGS_TEXT: 'Pobierz ustawienia aplikacji. Uważaj jeśli udostępniasz plik z ustawieniami, ponieważ zawiera on hasła oraz inne wrażliwe informacje!',
UPLOAD_TEXT: 'Wyślij firmware (.bin), ustawienia lub personalizacje (.json). Opcjonalnie, wyślij wcześniej plik walidacji (.md5).',
UPLOADING: 'Wysłano',
UPLOAD_DROP_TEXT: 'Przeciągnij tutaj plik lub kliknij',
ERROR: 'Nieoczekiwany błąd, spróbuj ponownie!',
TIME_SET: 'Zegar został ustawiony.',
MANAGE_USERS: 'Zarządzanie użytkownikami',
IS_ADMIN: '{{Administrator|Uprawnienia administratora|}}',
USER_WARNING: 'Przynajmniej jeden użytkownik musi mieć uprawnienia administratora!',
ADD: 'Doda{{j|wanie|}}',
ACCESS_TOKEN_FOR: 'Token dostępu dla użytkownika',
ACCESS_TOKEN_TEXT: 'Token jest używany w wywołaniach REST API wymagających autoryzacji. Można go przekazywać bezpośrednio lub przez URL.',
GENERATING_TOKEN: 'Generowanie tokenu',
USER: '{{Użytkownik|użytkownika|}}',
MODIFY: 'Edycja',
SU_TEXT: 'Hasło "su" (super-użytkownika) służy do podpisywania tokenów autoryzujących oraz włączania uprawnień administratora w konsoli.',
NOT_ENABLED: 'nie aktywowano',
ERRORS_OF: 'Błędy {0}',
DISCONNECT_REASON: 'Przyczyna braku połączenia',
ENABLE_MQTT: 'Aktywuj MQTT',
BROKER: 'brokera',
CLIENT: 'klienta',
BASE_TOPIC: 'Prefiks bazowy (unikalny!)',
OPTIONAL: 'opcjonalny',
FORMATTING: 'Formatowanie',
MQTT_FORMAT: 'Sposób publikowania danych',
MQTT_NEST_1: 'zagnieżdżone w jednym temacie',
MQTT_NEST_2: 'jako oddzielne tematy',
MQTT_RESPONSE: 'Rezultat wykonania komendy publikuj w temacie "response"',
MQTT_PUBLISH_TEXT_1: 'Tematy z pojedynczą wartością publikuj po jej zmianie',
MQTT_PUBLISH_TEXT_2: 'Publikuj w tematach "command" (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Włącz opcję "MQTT discovery" (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefiks dla "MQTT discovery"',
MQTT_PUBLISH_INTERVALS: 'Interwały publikowania',
MQTT_INT_BOILER: 'Kotły i pompy ciepła',
MQTT_INT_THERMOSTATS: 'Termostaty',
MQTT_INT_SOLAR: 'Panele solarne',
MQTT_INT_MIXER: 'Mieszacze',
MQTT_INT_HEARTBEAT: '"Heartbeat" (aktywność)',
MQTT_QUEUE: 'Kolejka MQTT',
DEFAULT: '{{Pozostałe|Domyślna|}}',
MQTT_ENTITY_FORMAT: 'Format "Entity ID"',
MQTT_ENTITY_FORMAT_0: 'długa nazwa (jak w v3.4)',
MQTT_ENTITY_FORMAT_1: 'krótka nazwa',
MQTT_ENTITY_FORMAT_2: 'prefiks bazowy + krótka nazwa',
MQTT_CLEAN_SESSION: 'Ustawiaj flagę "Clean session"',
MQTT_RETAIN_FLAG: 'Ustawiaj flagę "Retain"',
INACTIVE: 'nieaktywn{{y|a|}}',
ACTIVE: 'aktywny',
UNKNOWN: 'nieznany',
SET_TIME: '{{Ustaw zegar|Ustawianie zegara|}}',
SET_TIME_TEXT: 'Wprowadź aktualną datę i godzinę',
LOCAL_TIME: 'Czas lokalny',
UTC_TIME: 'Czas UTC',
ENABLE_NTP: 'Aktywuj NTP (data i godzina będą automatycznie synchronizowane z poniższym serwerem czasu)',
NTP_SERVER: 'Serwer NTP',
TIME_ZONE: 'Strefa czasowa',
ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}',
AP_PROVIDE: 'Punkt dostępowy',
AP_PROVIDE_TEXT_1: 'zawsze aktywny',
AP_PROVIDE_TEXT_2: 'aktywny jeśli brak połączenia z siecią',
AP_PROVIDE_TEXT_3: 'nieaktywny',
AP_PREFERRED_CHANNEL: 'Preferowany kanał',
AP_HIDE_SSID: 'Ukryj SSID',
AP_CLIENTS: 'Liczba klientów',
AP_MAX_CLIENTS: 'Maksymalna liczba klientów',
AP_LOCAL_IP: 'Lokalny adres IP',
NETWORK_SCAN: 'Skanowanie sieci WiFi',
IDLE: 'bezczynna',
LOST: 'zostało utracone.',
SCANNING: 'Skanuję',
SCAN_AGAIN: 'Skanuj ponownie',
NETWORK_SCANNER: 'Skaner sieci WiFi',
NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu',
NETWORK_BLANK_SSID: 'pozostaw puste aby wyłączyć WiFi',
TX_POWER: 'Moc nadawania',
HOSTNAME: 'Nazwa w sieci',
NETWORK_DISABLE_SLEEP: 'Wyłącz tryb usypiania WiFi',
NETWORK_LOW_BAND: 'Używaj mniejszej szerokości pasma WiFi (20MHz)',
NETWORK_USE_DNS: 'Włącz wsparcie dla mDNS',
NETWORK_ENABLE_CORS: 'Włącz wsparcie dla CORS',
NETWORK_CORS_ORIGIN: 'CORS origin',
NETWORK_ENABLE_IPV6: 'Włącz wsparcie dla IPv6',
NETWORK_FIXED_IP: 'Użyj stałego adresu IP',
NETWORK_GATEWAY: 'Brama',
NETWORK_SUBNET: 'Maska podsieci',
NETWORK_DNS: 'Serwery DNS',
ADDRESS_OF: 'Adres {0}',
ADMIN: 'Użytkownik "administrator".',
GUEST: 'Użytkownik "gość".',
NEW: 'Nowy',
NEW_NAME_OF: 'Nowa nazwa {0}',
ENTITY: 'encji',
MIN: 'Min.',
MAX: 'Maks.'
};
export default pl;

View File

@@ -0,0 +1,309 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const sv: Translation = {
LANGUAGE: 'Språk',
RETRY: 'Försök igen',
LOADING: 'Laddar',
IS_REQUIRED: '{0} Krävs',
SIGN_IN: 'Logga In',
SIGN_OUT: 'Logga Ut',
USERNAME: 'Användarnamn',
PASSWORD: 'Lösenord',
SU_PASSWORD: 'su Lösenord',
DASHBOARD: 'Kontrollpanel',
SETTINGS_OF: '{0} Inställningar',
SAVED: 'Sparat',
HELP_OF: '{0} Hjälp',
LOGGED_IN: 'Inloggad som {name}',
PLEASE_SIGNIN: 'Vänligen logga in för att fortsätta',
UPLOAD_SUCCESSFUL: 'Uppladdning lyckades',
DOWNLOAD_SUCCESSFUL: 'Nedladdning lyckades',
INVALID_LOGIN: 'Ogiltig login',
NETWORK: 'Nätverk',
SECURITY: 'Säkerhet',
ONOFF_CAP: 'PÅ/AV',
ONOFF: 'på/av',
TYPE: 'Typ',
DESCRIPTION: 'Beskrivning',
ENTITIES: 'Entiteter',
REFRESH: 'Uppdatera',
EXPORT: 'Exportera',
DEVICE_DETAILS: 'Enhetsdetaljer',
ID_OF: '{0}-ID',
DEVICE: 'Enhets',
PRODUCT: 'Produkt',
VERSION: 'Version',
BRAND: 'Fabrikat',
ENTITY_NAME: 'Entitetsnamn',
VALUE: '{{Värde|värde}}',
SHOW_FAV: 'Visa enbart favoriter',
DEVICE_SENSOR_DATA: 'Enhets och Sensor-data',
DEVICES_SENSORS: 'Enheter & Sensorer',
ATTACHED_SENSORS: 'Anslutna EMS-ESP Sensorer',
RUN_COMMAND: 'Kör Kommando',
CHANGE_VALUE: 'Ändra Värde',
CANCEL: 'Avbryt',
RESET: 'Nollställ',
SEND: 'Skicka',
SAVE: 'Spara',
REMOVE: 'Ta bort',
PROBLEM_UPDATING: 'Problem vid uppdatering',
PROBLEM_LOADING: 'Problem vid hämtning',
ACCESS_DENIED: 'Åtkomst Nekad',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analoga Sensorer',
UPDATED_OF: '{0} Uppdaterad',
UPDATE_OF: '{0} Uppdatera',
REMOVED_OF: '{0} Raderad',
DELETION_OF: '{0} Radering',
OFFSET: 'Kompensering',
FACTOR: 'Faktor',
FREQ: 'Frekvens',
DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM',
STARTVALUE: 'Startvärde',
WARN_GPIO: 'Varning: Var försiktig vid aktivering av GPIO!',
EDIT: 'Ändra',
SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperatursensorer',
WRITE_CMD_SENT: 'Skrivkommandon skickade',
WRITE_CMD_FAILED: 'Skrivkommandon misslyckade',
EMS_BUS_WARNING: 'EMS-buss nedkopplad. Om denna varning kvarstår efter några sekunder, kontrollera inställningar och enhets-profil.',
EMS_BUS_SCANNING: 'Söker efter EMS-enheter...',
CONNECTED: 'Ansluten',
TX_ISSUES: 'Sändfel - Prova ett annat TX-läge',
DISCONNECTED: 'Nedkopplad',
EMS_SCAN: 'Är du säker att du vill initiera en full genomsökning av EMS-bussen?',
EMS_BUS_STATUS: 'Status',
ACTIVE_DEVICES: 'Aktiva Enheter',
EMS_DEVICE: 'EMS Enhet',
SUCCESS: 'Lyckades',
FAIL: 'Misslyckades',
QUALITY: 'Kvalitet',
SCAN_DEVICES: 'Sök efter nya enheter',
EMS_BUS_STATUS_TITLE: 'EMS-buss & aktivitetsstatus',
SCAN: 'Sök',
STATUS_NAMES: [
'EMS-telegram (Rx)',
'EMS-läsningar (Tx)',
'EMS-skrivningar (Tx)',
'Temperatursensor-läsningar',
'Analog Sensor-läsningar',
'MQTT-publiceringar',
'API-anrop',
'Syslog-meddelanden'
],
NUM_DEVICES: '{num} Enhet{{er}}',
NUM_TEMP_SENSORS: '{num} Temperatur-sensor{{er}}',
NUM_ANALOG_SENSORS: '{num} Analoga Sensor{{er}}',
NUM_DAYS: '{num} dag{{ar}}',
NUM_SECONDS: '{num} sekund{{er}}',
NUM_HOURS: '{num} timmar',
NUM_MINUTES: '{num} minut{{er}}',
APPLICATION_SETTINGS: 'Inställningar',
CUSTOMIZATION: 'Anpassa',
APPLICATION_RESTARTING: 'EMS-ESP startar om',
INTERFACE_BOARD_PROFILE: 'Interface Hårdvaruprofil',
BOARD_PROFILE_TEXT: 'Välj en förkonfigurerad hårdvaruprofil från listan nedan eller välj Anpassad för att konfigurera dina egna hårdvaruinställningar',
BOARD_PROFILE: 'Hårdvarutyp',
CUSTOM: 'Anpassa',
GPIO_OF: '{0} GPIO',
BUTTON: 'Knapp',
TEMPERATURE: 'Temperatur',
PHY_TYPE: 'Eth PHY-typ',
DISABLED: 'inaktiverad',
TX_MODE: 'Tx-läge',
HARDWARE: 'Hårdvara',
EMS_BUS: '{{BUSS|EMS-BUSS}}',
GENERAL_OPTIONS: 'Allmänna Inställningar',
LANGUAGE_ENTITIES: 'Språk (för entiteter)',
HIDE_LED: 'Inaktivera LED',
ENABLE_TELNET: 'Aktivera Telnet',
ENABLE_ANALOG: 'Aktivera Analoga Sensorer',
CONVERT_FAHRENHEIT: 'Konvertera temperaturer till Fahrenheit',
BYPASS_TOKEN: 'Inaktivera Token-autensiering för API-anrop',
READONLY: 'Aktivera read-only (blockerar alla utgående skrivkommandon mot EMS-bussen)',
UNDERCLOCK_CPU: 'Nedklocka Processorhastighet',
ENABLE_SHOWER_TIMER: 'Aktivera Dusch-timer',
ENABLE_SHOWER_ALERT: 'Aktivera Dusch-varning',
TRIGGER_TIME: 'Aktiveringstid',
COLD_SHOT_DURATION: 'Längd på kalldusch',
FORMATTING_OPTIONS: 'Formatteringsalternativ',
BOOLEAN_FORMAT_DASHBOARD: 'Bool-format Kontrollpanel',
BOOLEAN_FORMAT_API: 'Bool-format API/MQTT',
ENUM_FORMAT: 'Enum-format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Aktivera parasitström',
LOGGING: 'Loggning',
LOG_HEX: 'Logga EMS-telegram i hexadecimal',
ENABLE_SYSLOG: 'Aktivera Syslog',
LOG_LEVEL: 'Loggnivå',
MARK_INTERVAL: 'Markerings-interval',
SECONDS: 'sekunder',
MINUTES: 'minuter',
HOURS: 'timmar',
RESTART: 'Starta om',
RESTART_TEXT: 'EMS-ESP kräver en omstart för att applicera förändrade systeminställningar',
RESTART_CONFIRM: 'Är du säker på att du vill starta om EMS-ESP?',
COMMAND: 'Kommando',
CUSTOMIZATIONS_RESTART: 'Alla anpassningr har raderats. Startar om...',
CUSTOMIZATIONS_FULL: 'Antal valda enheter för högt. Vänligen spara i mindre antal åt gången.',
CUSTOMIZATIONS_SAVED: 'Anpassningar sparade',
CUSTOMIZATIONS_HELP_1: 'Välj en enhet och anpassa underenheter med hjälp av alternativen',
CUSTOMIZATIONS_HELP_2: 'Favorit',
CUSTOMIZATIONS_HELP_3: 'Inaktivera skrivningar',
CUSTOMIZATIONS_HELP_4: 'Exkludera från MQTT & API',
CUSTOMIZATIONS_HELP_5: 'Göm från Kontrollpanel',
CUSTOMIZATIONS_HELP_6: 'remove from memory',
SELECT_DEVICE: 'Välj en enhet',
SET_ALL: 'ställ in alla',
OPTIONS: 'Alternativ',
NAME: 'Namn',
CUSTOMIZATIONS_RESET: 'Är du säker på att du vill ta bort alla anpassningar inklusive inställningar för Temperatur och Analoga sensorer?',
DEVICE_ENTITIES: 'Enhets-entiteter',
USER_CUSTOMIZATION: 'Användaranpassningar',
SUPPORT_INFORMATION: 'Supportinformation',
CLICK_HERE: 'Klicka Här',
HELP_INFORMATION_1: 'Besök Wikin för instruktioner för hur du kan konfigurera EMS-ESP',
HELP_INFORMATION_2: 'För community-support besök vår Discord-server',
HELP_INFORMATION_3: 'Önska en ny funktion eller rapportera en bugg',
HELP_INFORMATION_4: 'Bifoga din systeminformation för snabbare hantering när du rapporterar ett problem',
HELP_INFORMATION_5: 'EMS-ESP är gratis och är öppen källkod. Bidra till utvecklingen genom att ge oss en stjärna på GitHub!',
SUPPORT_INFO: 'Supportinfo',
UPLOAD_OF: '{0} Uppladdning',
UPLOAD: 'Uppladdning',
DOWNLOAD: 'Nedladdning',
ABORTED: 'Avbruten',
FAILED: 'Misslyckades',
SUCCESSFUL: 'Lyckades',
SYSTEM: 'System',
LOG_OF: '{0} Logg',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upp/Nedladdning',
SYSTEM_VERSION_RUNNING: 'Du använder version',
SYSTEM_APPLY_FIRMWARE: 'för att aktivera ny firmware',
CLOSE: 'Stäng',
USE: 'Använd',
FACTORY_RESET: 'Fabriksåterställning',
SYSTEM_FACTORY_TEXT: 'Enheten har blivit fabriksåterställd och startar nu om',
SYSTEM_FACTORY_TEXT_DIALOG: 'Är du säker att du vill fabriksåterställa enheten?',
VERSION_CHECK: 'Senaste versioner',
THE_LATEST: 'Den senaste',
OFFICIAL: 'officiell',
DEVELOPMENT: 'utveckling',
VERSION_IS: 'version är',
RELEASE_NOTES: 'release-logg',
EMS_ESP_VER: 'EMS-ESP Version',
PLATFORM: 'Enhet (Plattform / SDK)',
UPTIME: 'Systemets Upptid',
CPU_FREQ: 'CPU-frekvens',
HEAP: 'Heap (Ledigt / Max allokerat)',
PSRAM: 'PSRAM (Storlek / Ledigt)',
FLASH: 'Flashminne (Storlek / Hastighet)',
APPSIZE: 'Applikationer (Använt / Ledigt)',
FILESYSTEM: 'Filsystem (Använt / Ledigt)',
BUFFER_SIZE: 'Max Bufferstorlek',
COMPACT: 'Komprimera',
ENABLE_OTA: 'Aktivera OTA-uppdateringar',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Ladda ner entitetsanpassningar',
DOWNLOAD_SETTINGS_TEXT: 'Ladda ner applikationsinställningar. Var försiktig om du delar dina iställlningar då de innehåller lösenord och annan känslig systeminformation',
UPLOAD_TEXT: 'Ladda upp ett nytt firmware (.bin), inställningar eller anpassningar (.json) nedan',
UPLOADING: 'Laddar upp',
UPLOAD_DROP_TEXT: 'Släpp fil eller klicka här',
ERROR: 'Okänt Fel, var god försök igen',
TIME_SET: 'Ställ in tid',
MANAGE_USERS: 'Användare',
IS_ADMIN: 'Admin',
USER_WARNING: 'Du måste ha minst en admin konfigurerad',
ADD: 'Lägg till',
ACCESS_TOKEN_FOR: 'Access Token för',
ACCESS_TOKEN_TEXT: 'Nedan Token används med REST API-anrop som kräver auktorisering. Den kan skickas med antingen som en Bearer token i Authorization-headern eller i access_token URL query-parametern.',
GENERATING_TOKEN: 'Genererar token',
USER: 'Användare',
MODIFY: 'Ändra',
SU_TEXT: 'SU-användarens (super user) lösenord används för att signera autensierings-tokens samt för att aktivera administratörsprivilegier i Console-läge',
NOT_ENABLED: 'Ej aktiv',
ERRORS_OF: '{0} fel',
DISCONNECT_REASON: 'Anledning till nedkoppling',
ENABLE_MQTT: 'Aktivera MQTT',
BROKER: 'Broker',
CLIENT: 'Client',
BASE_TOPIC: 'Base',
OPTIONAL: 'Valfritt',
FORMATTING: 'Formatering',
MQTT_FORMAT: 'Topic/Payload Format',
MQTT_NEST_1: 'Nestlat i en topic.',
MQTT_NEST_2: 'Som individuella topics',
MQTT_RESPONSE: 'Publish-kommando som ett `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publicera single value topics vid värdeförändring',
MQTT_PUBLISH_TEXT_2: 'Publicera till kommando-topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Aktivera MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix för Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publiceringsintervall',
MQTT_INT_BOILER: 'Värmepump/panna',
MQTT_INT_THERMOSTATS: 'Termostater',
MQTT_INT_SOLAR: 'Solpaneler',
MQTT_INT_MIXER: 'Blandningsventiler',
MQTT_INT_HEARTBEAT: 'Heartbeat',
MQTT_QUEUE: 'MQTT-kö',
DEFAULT: 'Standard',
MQTT_ENTITY_FORMAT: 'Entitets-ID format',
MQTT_ENTITY_FORMAT_0: 'Singel-instans, långt namn(v3.4)',
MQTT_ENTITY_FORMAT_1: 'Singel-instans, kort name',
MQTT_ENTITY_FORMAT_2: 'Multi-instans, kort name',
MQTT_CLEAN_SESSION: 'Använd "Clean Session"-flaggan',
MQTT_RETAIN_FLAG: 'Använd "Always Retain"-flaggan',
INACTIVE: 'Inaktiv',
ACTIVE: 'Aktiv',
UNKNOWN: 'Okänt',
SET_TIME: 'Ställ in klockan',
SET_TIME_TEXT: 'Ange lokal datum och tid nedan för att ställa in klockan',
LOCAL_TIME: 'Tid (lokal)',
UTC_TIME: 'Tid (UTC)',
ENABLE_NTP: 'Aktivera NTP',
NTP_SERVER: 'NTP-server',
TIME_ZONE: 'Tidszon',
ACCESS_POINT: 'Accesspunkt',
AP_PROVIDE: 'Aktivera Accesspunkt',
AP_PROVIDE_TEXT_1: 'alltid',
AP_PROVIDE_TEXT_2: 'när WiFi är nedkopplat',
AP_PROVIDE_TEXT_3: 'aldrig',
AP_PREFERRED_CHANNEL: 'Kanal',
AP_HIDE_SSID: 'Göm SSID',
AP_CLIENTS: 'AP-klienter',
AP_MAX_CLIENTS: 'Max Klienter',
AP_LOCAL_IP: 'Lokalt IP',
NETWORK_SCAN: 'Sök efter WiFi-nätverk',
IDLE: 'Vilande',
LOST: 'Förlorad',
SCANNING: 'Söker',
SCAN_AGAIN: 'Sök igen',
NETWORK_SCANNER: 'Hittade nätverk',
NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades',
NETWORK_BLANK_SSID: 'lämna blankt för att inaktivera WiFi',
TX_POWER: 'Tx Effekt',
HOSTNAME: 'Värdnamn',
NETWORK_DISABLE_SLEEP: 'Inaktivera sömnläge',
NETWORK_LOW_BAND: 'Använd lägre bandbredd',
NETWORK_USE_DNS: 'Aktivera mDNS-tjänsten',
NETWORK_ENABLE_CORS: 'Aktivera CORS',
NETWORK_CORS_ORIGIN: 'CORS origin',
NETWORK_ENABLE_IPV6: 'Aktivera IPv6-support',
NETWORK_FIXED_IP: 'Använd statiskt IP',
NETWORK_GATEWAY: 'Gateway',
NETWORK_SUBNET: 'Subnätmask',
NETWORK_DNS: 'DNS-Server',
ADDRESS_OF: '{0} Adress',
ADMIN: 'Admin',
GUEST: 'Gäst',
NEW: 'Ny',
NEW_NAME_OF: 'Byt namn {0}',
ENTITY: 'Entitet',
MIN: 'min',
MAX: 'max'
};
export default sv;

View File

@@ -5,17 +5,21 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import DashboardStatus from './DashboardStatus';
import DashboardData from './DashboardData';
const Dashboard: FC = () => {
useLayoutTitle('Dashboard');
const { routerTab } = useRouterTab();
const { LL } = useI18nContext();
useLayoutTitle(LL.DASHBOARD());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="data" label="Devices &amp; Sensors" />
<Tab value="data" label={LL.DEVICES_SENSORS()} />
<Tab value="status" label="Status" />
</RouterTabs>
<Routes>

View File

@@ -49,8 +49,6 @@ import DeviceIcon from './DeviceIcon';
import { IconContext } from 'react-icons';
import { formatDurationMin, pluralize } from '../utils';
import { AuthenticatedContext } from '../contexts/authentication';
import { ButtonRow, ValidatedTextField, SectionContent, MessageBox } from '../components';
@@ -74,12 +72,23 @@ import {
DeviceEntityMask
} from './types';
import { useI18nContext } from '../i18n/i18n-react';
const DashboardData: FC = () => {
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [coreData, setCoreData] = useState<CoreData>({ connected: true, devices: [], active_sensors: 0, analog_enabled: false });
const [coreData, setCoreData] = useState<CoreData>({
connected: true,
devices: [],
s_n: '',
active_sensors: 0,
analog_enabled: false
});
const [deviceData, setDeviceData] = useState<DeviceData>({ label: '', data: [] });
const [sensorData, setSensorData] = useState<SensorData>({ sensors: [], analogs: [] });
const [deviceValue, setDeviceValue] = useState<DeviceValue>();
@@ -134,7 +143,7 @@ const DashboardData: FC = () => {
common_theme,
{
Table: `
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 80px 40px;
--data-table-library_grid-template-columns: 40px 160px repeat(1, minmax(0, 1fr)) 100px 40px;
`,
BaseRow: `
.td {
@@ -162,7 +171,7 @@ const DashboardData: FC = () => {
common_theme,
{
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 140px 40px;
--data-table-library_grid-template-columns: minmax(0, 1fr) 35% 40px;
`,
BaseRow: `
.td {
@@ -319,10 +328,10 @@ const DashboardData: FC = () => {
const handleDownloadCsv = () => {
const columns = [
{ accessor: (dv: any) => dv.id.slice(2), name: 'Entity' },
{ accessor: (dv: any) => dv.id.slice(2), name: LL.ENTITY_NAME() },
{
accessor: (dv: any) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v),
name: 'Value'
name: LL.VALUE(0)
},
{ accessor: (dv: any) => DeviceValueUOM_s[dv.u], name: 'UoM' }
];
@@ -354,10 +363,10 @@ const DashboardData: FC = () => {
const fetchCoreData = useCallback(async () => {
try {
setCoreData((await EMSESP.readCoreData()).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Failed to fetch core data'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
}, [enqueueSnackbar]);
}, [enqueueSnackbar, LL]);
useEffect(() => {
fetchCoreData();
@@ -375,30 +384,50 @@ const DashboardData: FC = () => {
const unique_id = parseInt(id);
try {
setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching device data'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
const fetchSensorData = async () => {
try {
setSensorData((await EMSESP.readSensorData()).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching sensor data'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c;
const formatDurationMin = (duration_min: number) => {
const days = Math.trunc((duration_min * 60000) / 86400000);
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
const minutes = Math.trunc((duration_min * 60000) / 60000) % 60;
let formatted = '';
if (days) {
formatted += LL.NUM_DAYS({ num: days }) + ' ';
}
if (hours) {
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
}
if (minutes) {
formatted += LL.NUM_MINUTES({ num: minutes });
}
return formatted;
};
function formatValue(value: any, uom: number) {
if (value === undefined) {
return '';
}
switch (uom) {
case DeviceValueUOM.HOURS:
return value ? formatDurationMin(value * 60) : '0 hours';
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
case DeviceValueUOM.MINUTES:
return value ? formatDurationMin(value) : '0 minutes';
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
case DeviceValueUOM.SECONDS:
return LL.NUM_SECONDS({ num: value });
case DeviceValueUOM.NONE:
if (typeof value === 'number') {
return new Intl.NumberFormat().format(value);
@@ -414,13 +443,24 @@ const DashboardData: FC = () => {
' ' +
DeviceValueUOM_s[uom]
);
case DeviceValueUOM.SECONDS:
return pluralize(value, DeviceValueUOM_s[uom]);
default:
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
}
}
const setUom = (uom: number) => {
switch (uom) {
case DeviceValueUOM.HOURS:
return LL.HOURS();
case DeviceValueUOM.MINUTES:
return LL.MINUTES();
case DeviceValueUOM.SECONDS:
return LL.SECONDS();
default:
return DeviceValueUOM_s[uom];
}
};
const sendDeviceValue = async () => {
if (deviceValue) {
try {
@@ -429,15 +469,15 @@ const DashboardData: FC = () => {
devicevalue: deviceValue
});
if (response.status === 204) {
enqueueSnackbar('Write command failed', { variant: 'error' });
enqueueSnackbar(LL.WRITE_CMD_FAILED(), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Write access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Write command sent', { variant: 'success' });
enqueueSnackbar(LL.WRITE_CMD_SENT(), { variant: 'success' });
}
setDeviceValue(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem writing value'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
refreshData();
setDeviceValue(undefined);
@@ -449,7 +489,7 @@ const DashboardData: FC = () => {
if (deviceValue) {
return (
<Dialog open={deviceValue !== undefined} onClose={() => setDeviceValue(undefined)}>
<DialogTitle>{isCmdOnly(deviceValue) ? 'Run Command' : 'Change Value'}</DialogTitle>
<DialogTitle>{isCmdOnly(deviceValue) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
<DialogContent dividers>
{deviceValue.l && (
<ValidatedTextField
@@ -470,7 +510,7 @@ const DashboardData: FC = () => {
<ValidatedTextField
name="v"
label={deviceValue.id.slice(2)}
value={deviceValue.u ? numberValue(deviceValue.v) : deviceValue.v}
value={typeof deviceValue.v === 'number' ? Math.round(deviceValue.v * 10) / 10 : deviceValue.v}
autoFocus
multiline={deviceValue.u ? false : true}
sx={{ width: '30ch' }}
@@ -478,7 +518,7 @@ const DashboardData: FC = () => {
onChange={updateValue(setDeviceValue)}
inputProps={deviceValue.u ? { min: deviceValue.m, max: deviceValue.x, step: deviceValue.s } : {}}
InputProps={{
startAdornment: <InputAdornment position="start">{DeviceValueUOM_s[deviceValue.u]}</InputAdornment>
startAdornment: <InputAdornment position="start">{setUom(deviceValue.u)}</InputAdornment>
}}
/>
)}
@@ -491,7 +531,7 @@ const DashboardData: FC = () => {
onClick={() => setDeviceValue(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SendIcon />}
@@ -500,7 +540,7 @@ const DashboardData: FC = () => {
onClick={() => sendDeviceValue()}
color="warning"
>
Send
{LL.SEND()}
</Button>
</DialogActions>
</Dialog>
@@ -521,15 +561,15 @@ const DashboardData: FC = () => {
offset: sensor.o
});
if (response.status === 204) {
enqueueSnackbar('Sensor change failed', { variant: 'error' });
enqueueSnackbar(LL.UPLOAD_OF(LL.SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Sensor updated', { variant: 'success' });
enqueueSnackbar(LL.UPDATED_OF(LL.SENSOR()), { variant: 'success' });
}
setSensor(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating sensor'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setSensor(undefined);
fetchSensorData();
@@ -541,16 +581,20 @@ const DashboardData: FC = () => {
if (sensor) {
return (
<Dialog open={sensor !== undefined} onClose={() => setSensor(undefined)}>
<DialogTitle>Edit Temperature Sensor</DialogTitle>
<DialogTitle>
{LL.EDIT()} {LL.TEMP_SENSOR()}
</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">Sensor ID {sensor.id}</Typography>
<Typography variant="body2">
{LL.ID_OF(LL.SENSOR())}: {sensor.id}
</Typography>
</Box>
<Grid container spacing={1}>
<Grid item>
<ValidatedTextField
name="n"
label="Name"
label={LL.ENTITY_NAME()}
value={sensor.n}
autoFocus
sx={{ width: '30ch' }}
@@ -560,7 +604,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Offset"
label={LL.OFFSET()}
value={numberValue(sensor.o)}
sx={{ width: '12ch' }}
type="number"
@@ -581,7 +625,7 @@ const DashboardData: FC = () => {
onClick={() => setSensor(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -590,7 +634,7 @@ const DashboardData: FC = () => {
onClick={() => sendSensor()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -602,35 +646,35 @@ const DashboardData: FC = () => {
if (coreData && coreData.devices.length > 0 && deviceDialog !== -1) {
return (
<Dialog open={deviceDialog !== -1} onClose={() => setDeviceDialog(-1)}>
<DialogTitle>Device Details</DialogTitle>
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
<DialogContent dividers>
<List dense={true}>
<ListItem>
<ListItemText primary="Type" secondary={coreData.devices[deviceDialog].t} />
<ListItemText primary={LL.TYPE()} secondary={coreData.devices[deviceDialog].t} />
</ListItem>
<ListItem>
<ListItemText primary="Name" secondary={coreData.devices[deviceDialog].n} />
<ListItemText primary={LL.NAME(0)} secondary={coreData.devices[deviceDialog].n} />
</ListItem>
<ListItem>
<ListItemText primary="Brand" secondary={coreData.devices[deviceDialog].b} />
<ListItemText primary={LL.BRAND()} secondary={coreData.devices[deviceDialog].b} />
</ListItem>
<ListItem>
<ListItemText
primary="Device ID"
primary={LL.ID_OF(LL.DEVICE())}
secondary={'0x' + ('00' + coreData.devices[deviceDialog].d.toString(16).toUpperCase()).slice(-2)}
/>
</ListItem>
<ListItem>
<ListItemText primary="Product ID" secondary={coreData.devices[deviceDialog].p} />
<ListItemText primary={LL.ID_OF(LL.PRODUCT())} secondary={coreData.devices[deviceDialog].p} />
</ListItem>
<ListItem>
<ListItemText primary="Version" secondary={coreData.devices[deviceDialog].v} />
<ListItemText primary={LL.VERSION()} secondary={coreData.devices[deviceDialog].v} />
</ListItem>
</List>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setDeviceDialog(-1)} color="secondary">
Close
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
@@ -640,17 +684,20 @@ const DashboardData: FC = () => {
const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message="EMSbus disconnected, check settings and board profile" />}
{coreData.connected && coreData.devices.length === 0 && <MessageBox my={2} level="warning" message="Scanning for EMS devices..." />}
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
{coreData.connected && coreData.devices.length === 0 && (
<MessageBox my={2} level="warning" message={LL.EMS_BUS_SCANNING()} />
)}
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff />
<HeaderCell stiff>TYPE</HeaderCell>
<HeaderCell resize>DESCRIPTION</HeaderCell>
<HeaderCell stiff>ENTITIES</HeaderCell>
<HeaderCell stiff>{LL.TYPE()}</HeaderCell>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
<HeaderCell stiff>{LL.ENTITIES()}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -658,9 +705,9 @@ const DashboardData: FC = () => {
{tableList.map((device: Device, index: number) => (
<Row key={device.id} item={device}>
<Cell stiff>
<DeviceIcon type={device.t} />
<DeviceIcon type_id={device.t} />
</Cell>
<Cell stiff>{device.t}</Cell>
<Cell stiff>{device.tn}</Cell>
<Cell>{device.n}</Cell>
<Cell stiff>{device.e}</Cell>
<Cell stiff>
@@ -673,10 +720,10 @@ const DashboardData: FC = () => {
{(coreData.active_sensors > 0 || coreData.analog_enabled) && (
<Row key="sensor" item={{ id: 'sensor' }}>
<Cell>
<DeviceIcon type="Sensor" />
<DeviceIcon type_id={1} />
</Cell>
<Cell>Sensors</Cell>
<Cell>Attached EMS-ESP Sensors</Cell>
<Cell>{coreData.s_n}</Cell>
<Cell>{LL.ATTACHED_SENSORS()}</Cell>
<Cell>{coreData.active_sensors}</Cell>
<Cell>
<IconButton size="small" onClick={() => addAnalogSensor()}>
@@ -723,7 +770,7 @@ const DashboardData: FC = () => {
control={<Checkbox size="small" name="onlyFav" checked={onlyFav} onChange={() => setOnlyFav(!onlyFav)} />}
label={
<span style={{ fontSize: '12px' }}>
only show favorites&nbsp;
{LL.SHOW_FAV()}&nbsp;
<StarIcon color="primary" sx={{ fontSize: 12 }} />
</span>
}
@@ -749,7 +796,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'NAME')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
ENTITY NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell resize>
@@ -759,7 +806,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
>
VALUE
{LL.VALUE(0)}
</Button>
</HeaderCell>
<HeaderCell stiff />
@@ -806,7 +853,7 @@ const DashboardData: FC = () => {
const renderDallasData = () => (
<>
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
Temperature Sensors
{LL.TEMP_SENSORS()}
</Typography>
<Table
data={{ nodes: sensorData.sensors }}
@@ -825,7 +872,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(sensor_sort.state, 'NAME')}
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell stiff>
@@ -835,7 +882,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(sensor_sort.state, 'TEMPERATURE')}
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'TEMPERATURE' })}
>
TEMPERATURE
{LL.VALUE(0)}
</Button>
</HeaderCell>
<HeaderCell stiff />
@@ -865,7 +912,7 @@ const DashboardData: FC = () => {
const renderAnalogData = () => (
<>
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
Analog Sensors
{LL.ANALOG_SENSORS()}
</Typography>
<Table data={{ nodes: sensorData.analogs }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
@@ -890,7 +937,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(analog_sort.state, 'NAME')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell stiff>
@@ -900,10 +947,10 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
>
TYPE
{LL.TYPE()}
</Button>
</HeaderCell>
<HeaderCell stiff>VALUE</HeaderCell>
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -943,14 +990,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog deletion failed', { variant: 'error' });
enqueueSnackbar(LL.DELETION_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor removed', { variant: 'success' });
enqueueSnackbar(LL.REMOVED_OF(LL.ANALOG_SENSOR()), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog sensor'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -971,14 +1018,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog sensor update failed', { variant: 'error' });
enqueueSnackbar(LL.UPDATE_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor updated', { variant: 'success' });
enqueueSnackbar(LL.UPDATED_OF(LL.ANALOG_SENSOR()), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -990,32 +1037,42 @@ const DashboardData: FC = () => {
if (analog) {
return (
<Dialog open={analog !== undefined} onClose={() => setAnalog(undefined)}>
<DialogTitle>Edit Analog Sensor</DialogTitle>
<DialogTitle>
{LL.EDIT()} {LL.ANALOG_SENSOR()}
</DialogTitle>
<DialogContent dividers>
<Grid container spacing={2}>
<Grid item>
<Grid item xs={12}>
<ValidatedTextField
name="n"
label={LL.ENTITY_NAME()}
value={analog.n}
fullWidth
variant="outlined"
onChange={updateValue(setAnalog)}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="g"
label="GPIO"
value={analog.g}
fullWidth
type="number"
variant="outlined"
autoFocus
onChange={updateValue(setAnalog)}
/>
</Grid>
<Grid item>
<Grid item xs={8}>
<ValidatedTextField
name="n"
label="Name"
value={analog.n}
sx={{ width: '20ch' }}
variant="outlined"
name="t"
label={LL.TYPE()}
value={analog.t}
fullWidth
select
onChange={updateValue(setAnalog)}
/>
</Grid>
<Grid item>
<ValidatedTextField name="t" label="Type" value={analog.t} select onChange={updateValue(setAnalog)}>
>
{AnalogTypeNames.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
@@ -1025,8 +1082,15 @@ const DashboardData: FC = () => {
</Grid>
{analog.t >= AnalogType.COUNTER && analog.t <= AnalogType.RATE && (
<>
<Grid item>
<ValidatedTextField name="u" label="UoM" value={analog.u} select onChange={updateValue(setAnalog)}>
<Grid item xs={4}>
<ValidatedTextField
name="u"
label={LL.UNIT()}
value={analog.u}
fullWidth
select
onChange={updateValue(setAnalog)}
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
@@ -1035,12 +1099,12 @@ const DashboardData: FC = () => {
</ValidatedTextField>
</Grid>
{analog.t === AnalogType.ADC && (
<Grid item>
<Grid item xs={4}>
<ValidatedTextField
name="o"
label="Offset"
label={LL.OFFSET()}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
fullWidth
type="number"
variant="outlined"
onChange={updateValue(setAnalog)}
@@ -1052,41 +1116,41 @@ const DashboardData: FC = () => {
</Grid>
)}
{analog.t === AnalogType.COUNTER && (
<Grid item>
<Grid item xs={4}>
<ValidatedTextField
name="o"
label="Start Value"
label={LL.STARTVALUE()}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
fullWidth
type="number"
variant="outlined"
onChange={updateValue(setAnalog)}
inputProps={{ min: '0', step: '1' }}
inputProps={{ step: '0.001' }}
/>
</Grid>
)}
<Grid item>
<Grid item xs={4}>
<ValidatedTextField
name="f"
label="Factor"
label={LL.FACTOR()}
value={numberValue(analog.f)}
sx={{ width: '20ch' }}
fullWidth
type="number"
variant="outlined"
onChange={updateValue(setAnalog)}
inputProps={{ min: '-100', max: '100', step: '0.1' }}
inputProps={{ step: '0.001' }}
/>
</Grid>
</>
)}
{analog.t === AnalogType.DIGITAL_OUT && (analog.id === '25' || analog.id === '26') && (
{analog.t === AnalogType.DIGITAL_OUT && (analog.g === 25 || analog.g === 26) && (
<>
<Grid item>
<Grid item xs={4}>
<ValidatedTextField
name="o"
label="DAC Value"
label={LL.VALUE(0)}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
fullWidth
type="number"
variant="outlined"
onChange={updateValue(setAnalog)}
@@ -1095,14 +1159,14 @@ const DashboardData: FC = () => {
</Grid>
</>
)}
{analog.t === AnalogType.DIGITAL_OUT && analog.id !== '25' && analog.id !== '26' && (
{analog.t === AnalogType.DIGITAL_OUT && analog.g !== 25 && analog.g !== 26 && (
<>
<Grid item>
<Grid item xs={4}>
<ValidatedTextField
name="o"
label="Value"
label={LL.VALUE(0)}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
fullWidth
type="number"
variant="outlined"
onChange={updateValue(setAnalog)}
@@ -1113,12 +1177,12 @@ const DashboardData: FC = () => {
)}
{analog.t >= AnalogType.PWM_0 && (
<>
<Grid item>
<Grid item xs={4}>
<ValidatedTextField
name="f"
label="Frequency"
label={LL.FREQ()}
value={numberValue(analog.f)}
sx={{ width: '20ch' }}
fullWidth
type="number"
variant="outlined"
onChange={updateValue(setAnalog)}
@@ -1128,12 +1192,12 @@ const DashboardData: FC = () => {
}}
/>
</Grid>
<Grid item>
<Grid item xs={4}>
<ValidatedTextField
name="o"
label="Dutycycle"
label={LL.DUTY_CYCLE()}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
fullWidth
type="number"
variant="outlined"
onChange={updateValue(setAnalog)}
@@ -1147,13 +1211,13 @@ const DashboardData: FC = () => {
)}
</Grid>
<Box color="warning.main" mt={2}>
<Typography variant="body2">Warning: be careful when assigning a GPIO!</Typography>
<Typography variant="body2">{LL.WARN_GPIO()}</Typography>
</Box>
</DialogContent>
<DialogActions>
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={() => sendRemoveAnalog()}>
Remove
{LL.REMOVE()}
</Button>
</Box>
<Button
@@ -1162,7 +1226,7 @@ const DashboardData: FC = () => {
onClick={() => setAnalog(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -1171,7 +1235,7 @@ const DashboardData: FC = () => {
onClick={() => sendAnalog()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -1180,7 +1244,7 @@ const DashboardData: FC = () => {
};
return (
<SectionContent title="Device and Sensor Data" titleGutter>
<SectionContent title={LL.DEVICE_SENSOR_DATA()} titleGutter>
{renderCoreData()}
{renderDeviceData()}
{renderDeviceDialog()}
@@ -1191,11 +1255,11 @@ const DashboardData: FC = () => {
{renderAnalogDialog()}
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
Refresh
{LL.REFRESH()}
</Button>
{device_select.state.id && device_select.state.id !== 'sensor' && (
<Button startIcon={<DownloadIcon />} variant="outlined" onClick={handleDownloadCsv}>
Export
{LL.EXPORT()}
</Button>
)}
</ButtonRow>

View File

@@ -32,10 +32,13 @@ import { ButtonRow, FormLoader, SectionContent } from '../components';
import { Status, busConnectionStatus, Stat } from './types';
import { formatDurationSec, pluralize, extractErrorMessage, useRest } from '../utils';
import { extractErrorMessage, useRest } from '../utils';
import * as EMSESP from './api';
import type { Translation } from '../i18n/i18n-types';
import { useI18nContext } from '../i18n/i18n-react';
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
const busStatusHighlight = ({ status }: Status, theme: Theme) => {
@@ -51,19 +54,6 @@ const busStatusHighlight = ({ status }: Status, theme: Theme) => {
}
};
const busStatus = ({ status }: Status) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return 'Connected';
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return 'Tx issues - try a different Tx Mode';
case busConnectionStatus.BUS_STATUS_OFFLINE:
return 'Disconnected';
default:
return 'Unknown';
}
};
const showQuality = (stat: Stat) => {
if (stat.q === 0 || stat.s + stat.f === 0) {
return;
@@ -81,12 +71,32 @@ const showQuality = (stat: Stat) => {
const DashboardStatus: FC = () => {
const { loadData, data, errorMessage } = useRest<Status>({ read: EMSESP.readStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const [confirmScan, setConfirmScan] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar();
const { me } = useContext(AuthenticatedContext);
const showName = (id: any) => {
let name: keyof Translation['STATUS_NAMES'] = id;
return LL.STATUS_NAMES[name]();
};
const busStatus = ({ status }: Status) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return LL.CONNECTED(0);
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return LL.TX_ISSUES();
case busConnectionStatus.BUS_STATUS_OFFLINE:
return LL.DISCONNECTED();
default:
return 'Unknown';
}
};
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
@@ -137,24 +147,44 @@ const DashboardStatus: FC = () => {
const scan = async () => {
try {
await EMSESP.scanDevices();
enqueueSnackbar('Scanning for devices...', { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem initiating scan'), { variant: 'error' });
enqueueSnackbar(LL.SCANNING() + '...', { variant: 'info' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setConfirmScan(false);
}
};
const formatDurationSec = (duration_sec: number) => {
const days = Math.trunc((duration_sec * 1000) / 86400000);
const hours = Math.trunc((duration_sec * 1000) / 3600000) % 24;
const minutes = Math.trunc((duration_sec * 1000) / 60000) % 60;
const seconds = Math.trunc((duration_sec * 1000) / 1000) % 60;
let formatted = '';
if (days) {
formatted += LL.NUM_DAYS({ num: days }) + ' ';
}
if (hours) {
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
}
if (minutes) {
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
}
formatted += LL.NUM_SECONDS({ num: seconds });
return formatted;
};
const renderScanDialog = () => (
<Dialog open={confirmScan} onClose={() => setConfirmScan(false)}>
<DialogTitle>EMS Device Scan</DialogTitle>
<DialogContent dividers>Are you sure you want to initiate a full device scan of the EMS bus?</DialogContent>
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
Cancel
{LL.CANCEL()}
</Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary" autoFocus>
Scan
{LL.SCAN()}
</Button>
</DialogActions>
</Dialog>
@@ -174,7 +204,10 @@ const DashboardStatus: FC = () => {
<DirectionsBusIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="EMS Bus Status" secondary={busStatus(data) + formatDurationSec(data.uptime)} />
<ListItemText
primary={LL.EMS_BUS_STATUS()}
secondary={busStatus(data) + ' (' + formatDurationSec(data.uptime) + ')'}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
@@ -183,13 +216,13 @@ const DashboardStatus: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Active Devices &amp; Sensors"
primary={LL.ACTIVE_DEVICES()}
secondary={
pluralize(data.num_devices, 'EMS Device') +
LL.NUM_DEVICES({ num: data.num_devices }) +
', ' +
pluralize(data.num_sensors, 'Temperature Sensor') +
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
', ' +
pluralize(data.num_analogs, 'Analog Sensor')
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
}
/>
</ListItem>
@@ -200,15 +233,15 @@ const DashboardStatus: FC = () => {
<Header>
<HeaderRow>
<HeaderCell resize></HeaderCell>
<HeaderCell stiff>SUCCESS</HeaderCell>
<HeaderCell stiff>FAIL</HeaderCell>
<HeaderCell stiff>QUALITY</HeaderCell>
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((stat: Stat) => (
<Row key={stat.id} item={stat}>
<Cell>{stat.id}</Cell>
<Cell>{showName(stat.id)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
<Cell stiff>{showQuality(stat)}</Cell>
@@ -223,7 +256,7 @@ const DashboardStatus: FC = () => {
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
@@ -235,7 +268,7 @@ const DashboardStatus: FC = () => {
disabled={!me.admin}
onClick={() => setConfirmScan(true)}
>
Scan for new devices
{LL.SCAN_DEVICES()}
</Button>
</ButtonRow>
</Box>
@@ -245,7 +278,7 @@ const DashboardStatus: FC = () => {
};
return (
<SectionContent title="EMS Bus &amp; Activity Status" titleGutter>
<SectionContent title={LL.EMS_BUS_STATUS_TITLE()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -9,31 +9,61 @@ import { GiHeatHaze } from 'react-icons/gi';
import { TiFlowSwitch } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc';
import { AiOutlineGateway } from 'react-icons/ai';
import { AiOutlineAlert } from 'react-icons/ai';
import { AiOutlineChrome } from 'react-icons/ai';
interface DeviceIconProps {
type: string;
type_id: number;
}
const DeviceIcon: FC<DeviceIconProps> = ({ type }) => {
switch (type) {
case 'Boiler':
return <CgSmartHomeBoiler />;
case 'Sensor':
// matches emsdevice.h DeviceType
const enum DeviceType {
SYSTEM = 0,
DALLASSENSOR,
ANALOGSENSOR,
BOILER,
THERMOSTAT,
MIXER,
SOLAR,
HEATPUMP,
GATEWAY,
SWITCH,
CONTROLLER,
CONNECT,
ALERT,
PUMP,
GENERIC,
HEATSOURCE,
UNKNOWN
}
const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
switch (type_id) {
case DeviceType.DALLASSENSOR:
case DeviceType.ANALOGSENSOR:
return <MdOutlineSensors />;
case 'Solar':
return <FaSolarPanel />;
case 'Thermostat':
case DeviceType.BOILER:
case DeviceType.HEATSOURCE:
return <CgSmartHomeBoiler />;
case DeviceType.THERMOSTAT:
return <MdThermostatAuto />;
case 'Mixer':
case DeviceType.MIXER:
return <AiOutlineControl />;
case 'Heatpump':
case DeviceType.SOLAR:
return <FaSolarPanel />;
case DeviceType.HEATPUMP:
return <GiHeatHaze />;
case 'Switch':
return <TiFlowSwitch />;
case 'Connect':
return <VscVmConnect />;
case 'Gateway':
case DeviceType.GATEWAY:
return <AiOutlineGateway />;
case DeviceType.SWITCH:
return <TiFlowSwitch />;
case DeviceType.CONTROLLER:
case DeviceType.CONNECT:
return <VscVmConnect />;
case DeviceType.ALERT:
return <AiOutlineAlert />;
case DeviceType.PUMP:
return <AiOutlineChrome />;
default:
return null;
}

View File

@@ -5,16 +5,20 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import HelpInformation from './HelpInformation';
const Help: FC = () => {
useLayoutTitle('Help');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.HELP_OF(''));
return (
<>
<RouterTabs value={routerTab}>
<Tab value="information" label="EMS-ESP Help" />
<Tab value="information" label={LL.HELP_OF('EMS-ESP')} />
</RouterTabs>
<Routes>
<Route path="information" element={<HelpInformation />} />

View File

@@ -9,14 +9,18 @@ import { useSnackbar } from 'notistack';
import CommentIcon from '@mui/icons-material/CommentTwoTone';
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
import GitHubIcon from '@mui/icons-material/GitHub';
import StarIcon from '@mui/icons-material/Star';
import DownloadIcon from '@mui/icons-material/GetApp';
import EastIcon from '@mui/icons-material/East';
import { extractErrorMessage } from '../utils';
import { useI18nContext } from '../i18n/i18n-react';
import * as EMSESP from './api';
const HelpInformation: FC = () => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const saveFile = (json: any, endpoint: string) => {
@@ -31,7 +35,7 @@ const HelpInformation: FC = () => {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
enqueueSnackbar('System information downloaded', { variant: 'info' });
enqueueSnackbar(LL.DOWNLOAD_SUCCESSFUL(), { variant: 'info' });
};
const callAPI = async (endpoint: string) => {
@@ -42,83 +46,84 @@ const HelpInformation: FC = () => {
id: 0
});
if (response.status !== 200) {
enqueueSnackbar('API call failed', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
} else {
saveFile(response.data, endpoint);
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
return (
<SectionContent title="Support Information" titleGutter>
<SectionContent title={LL.SUPPORT_INFORMATION()} titleGutter>
<List>
<ListItem>
<ListItemAvatar>
<MenuBookIcon />
<MenuBookIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
Visit the online&nbsp;
{LL.HELP_INFORMATION_1()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
&nbsp;
<Link target="_blank" href="https://emsesp.github.io/docs" color="primary">
{'Wiki'}
{LL.CLICK_HERE()}
</Link>
&nbsp;to get instructions on how to&nbsp;
<Link
target="_blank"
href="https://emsesp.github.io/docs/#/Configure-firmware?id=ems-esp-settings"
color="primary"
>
{'configure'}
</Link>
&nbsp;EMS-ESP and access other information.
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<CommentIcon />
<CommentIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
For live community chat join our&nbsp;
{LL.HELP_INFORMATION_2()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
&nbsp;
<Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
{'Discord'}
{LL.CLICK_HERE()}
</Link>
&nbsp;server.
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<GitHubIcon />
<GitHubIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
Submit a&nbsp;
{LL.HELP_INFORMATION_3()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">
support issue
{LL.CLICK_HERE()}
</Link>
&nbsp;for requesting a new feature or reporting a bug.
<br />
Make sure you also&nbsp;
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => callAPI('info')}>
download
<i>({LL.HELP_INFORMATION_4()}</i>&nbsp;&nbsp;
<Button
startIcon={<DownloadIcon />}
size="small"
variant="outlined"
color="primary"
onClick={() => callAPI('info')}
>
{LL.SUPPORT_INFO()}
</Button>
&nbsp; and attach your system details for a faster response.
&nbsp;)
</ListItemText>
</ListItem>
</List>
<Box border={1} p={1} mt={4}>
<Typography align="center" variant="h6" color="orange">
EMS-ESP will always be a free and open-source project
<br></br>Please consider supporting it with a&nbsp;
<StarIcon style={{ fontSize: 16, color: 'yellow', verticalAlign: 'middle' }} /> on&nbsp;
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
{'GitHub'}
<Box border={1} p={1} mt={4} color="orange">
<Typography align="center" variant="subtitle1" color="orange">
<b>{LL.HELP_INFORMATION_5()}</b>
</Typography>
<Typography align="center">
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32" color="primary">
{'github.com/emsesp/EMS-ESP32'}
</Link>
</Typography>
<Typography align="center">@proddy @MichaelDvP</Typography>
<Typography color="white" align="center">
@proddy @MichaelDvP
</Typography>
</Box>
</SectionContent>
);

View File

@@ -13,9 +13,13 @@ import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
type OptionType = 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = {
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
api_mqtt_exclude: [CommentsDisabledOutlinedIcon, InsertCommentOutlinedIcon],

View File

@@ -6,6 +6,8 @@ import { AuthenticatedContext } from '../contexts/authentication';
import { PROJECT_PATH } from '../api/env';
import { useI18nContext } from '../i18n/i18n-react';
import TuneIcon from '@mui/icons-material/Tune';
import DashboardIcon from '@mui/icons-material/Dashboard';
import LayoutMenuItem from '../components/layout/LayoutMenuItem';
@@ -13,17 +15,18 @@ import InfoIcon from '@mui/icons-material/Info';
const ProjectMenu: FC = () => {
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
return (
<List>
<LayoutMenuItem icon={DashboardIcon} label="Dashboard" to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem icon={DashboardIcon} label={LL.DASHBOARD()} to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem
icon={TuneIcon}
label="Settings"
label={LL.SETTINGS_OF('')}
to={`/${PROJECT_PATH}/settings`}
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={InfoIcon} label="Help" to={`/${PROJECT_PATH}/help`} />
<LayoutMenuItem icon={InfoIcon} label={LL.HELP_OF('')} to={`/${PROJECT_PATH}/help`} />
</List>
);
};

View File

@@ -5,18 +5,22 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import SettingsApplication from './SettingsApplication';
import SettingsCustomization from './SettingsCustomization';
const Settings: FC = () => {
useLayoutTitle('Settings');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.SETTINGS_OF(''));
return (
<>
<RouterTabs value={routerTab}>
<Tab value="application" label="Application Settings" />
<Tab value="customization" label="Customization" />
<Tab value="application" label={LL.APPLICATION_SETTINGS()} />
<Tab value="customization" label={LL.CUSTOMIZATION()} />
</RouterTabs>
<Routes>
<Route path="application" element={<SettingsApplication />} />

View File

@@ -3,7 +3,7 @@ import { ValidateFieldsError } from 'async-validator';
import { useSnackbar } from 'notistack';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider } from '@mui/material';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
@@ -24,6 +24,9 @@ import { numberValue, extractErrorMessage, updateValue, useRest } from '../utils
import * as EMSESP from './api';
import { Settings, BOARD_PROFILES } from './types';
import { useI18nContext } from '../i18n/i18n-react';
import RestartMonitor from '../framework/system/RestartMonitor';
export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}>
@@ -37,6 +40,9 @@ const SettingsApplication: FC = () => {
read: EMSESP.readSettings,
update: EMSESP.writeSettings
});
const [restarting, setRestarting] = useState<boolean>();
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
@@ -64,8 +70,8 @@ const SettingsApplication: FC = () => {
eth_clock_mode: response.data.eth_clock_mode
});
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching board profile'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setProcessingBoard(false);
}
@@ -102,28 +108,26 @@ const SettingsApplication: FC = () => {
validateAndSubmit();
try {
await EMSESP.restart();
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
setRestarting(true);
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
};
return (
<>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Interface Board Profile
{LL.INTERFACE_BOARD_PROFILE()}
</Typography>
<Box color="warning.main">
<Typography variant="body2">
Select a pre-configured interface board profile from the list below or choose "Custom" to configure your own
hardware settings.
</Typography>
<Typography variant="body2">{LL.BOARD_PROFILE_TEXT()}</Typography>
</Box>
<ValidatedTextField
name="board_profile"
label="Board Profile"
label={LL.BOARD_PROFILE()}
value={data.board_profile}
disabled={processingBoard}
fullWidth
variant="outlined"
onChange={changeBoardProfile}
margin="normal"
@@ -132,17 +136,24 @@ const SettingsApplication: FC = () => {
{boardProfileSelectItems()}
<Divider />
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
Custom&hellip;
{LL.CUSTOM()}&hellip;
</MenuItem>
</ValidatedTextField>
{data.board_profile === 'CUSTOM' && (
<>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={4}>
<Grid
container
spacing={1}
sx={{ pt: 1 }}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="rx_gpio"
label="Rx GPIO"
label={LL.GPIO_OF('Rx')}
fullWidth
variant="outlined"
value={numberValue(data.rx_gpio)}
@@ -152,11 +163,11 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="tx_gpio"
label="Tx GPIO"
label={LL.GPIO_OF('Tx')}
fullWidth
variant="outlined"
value={numberValue(data.tx_gpio)}
@@ -166,11 +177,11 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="pbutton_gpio"
label="Button GPIO"
label={LL.GPIO_OF(LL.BUTTON())}
fullWidth
variant="outlined"
value={numberValue(data.pbutton_gpio)}
@@ -180,11 +191,11 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="dallas_gpio"
label="Temperature GPIO (0=disabled)"
label={LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'}
fullWidth
variant="outlined"
value={numberValue(data.dallas_gpio)}
@@ -194,11 +205,11 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item>
<Grid item xs={6} sm={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="led_gpio"
label="LED GPIO (0=disabled)"
label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'}
fullWidth
variant="outlined"
value={numberValue(data.led_gpio)}
@@ -208,11 +219,10 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
name="phy_type"
label="Eth PHY Type"
label={LL.PHY_TYPE()}
disabled={saving}
value={data.phy_type}
fullWidth
@@ -221,17 +231,25 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={0}>No Ethernet Module</MenuItem>
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
<MenuItem value={1}>LAN8720</MenuItem>
<MenuItem value={2}>TLK110</MenuItem>
</ValidatedTextField>
</Grid>
</Grid>
{data.phy_type !== 0 && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<Grid
container
spacing={1}
sx={{ pt: 1 }}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={6} sm={4}>
<ValidatedTextField
name="eth_power"
label="Eth Power GPIO (-1=disabled)"
label={LL.GPIO_OF('PHY Power') + ' (-1=' + LL.DISABLED(1) + ')'}
fullWidth
variant="outlined"
value={numberValue(data.eth_power)}
@@ -241,10 +259,10 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item>
<Grid item xs={6} sm={4}>
<ValidatedTextField
name="eth_phy_addr"
label="Eth I²C-address"
label={LL.ADDRESS_OF('PHY I²C')}
fullWidth
variant="outlined"
value={numberValue(data.eth_phy_addr)}
@@ -254,10 +272,10 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item>
<Grid item xs={6} sm={4}>
<ValidatedTextField
name="eth_clock_mode"
label="Eth Clock Mode"
label="PHY Clk"
disabled={saving}
value={data.eth_clock_mode}
fullWidth
@@ -276,14 +294,14 @@ const SettingsApplication: FC = () => {
)}
</>
)}
<Typography variant="h6" color="primary">
EMS Bus Settings
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={6}>
<ValidatedTextField
name="tx_mode"
label="Tx Mode"
label={LL.TX_MODE()}
disabled={saving}
value={data.tx_mode}
fullWidth
@@ -295,13 +313,13 @@ const SettingsApplication: FC = () => {
<MenuItem value={1}>EMS</MenuItem>
<MenuItem value={2}>EMS+</MenuItem>
<MenuItem value={3}>HT3</MenuItem>
<MenuItem value={4}>Hardware</MenuItem>
<MenuItem value={4}>{LL.HARDWARE()}</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={6}>
<ValidatedTextField
name="ems_bus_id"
label="Bus ID"
label={LL.ID_OF(LL.EMS_BUS(1))}
disabled={saving}
value={data.ems_bus_id}
fullWidth
@@ -310,102 +328,143 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
<MenuItem value={0x0e}>Converter (0x0E)</MenuItem>
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
<MenuItem value={0x48}>Gateway 1 (0x48)</MenuItem>
<MenuItem value={0x49}>Gateway 2 (0x49)</MenuItem>
<MenuItem value={0x4a}>Gateway 3 (0x4A)</MenuItem>
<MenuItem value={0x4b}>Gateway 4 (0x4B)</MenuItem>
<MenuItem value={0x4c}>Gateway 5 (0x4C)</MenuItem>
<MenuItem value={0x4d}>Gateway 7 (0x4D)</MenuItem>
</ValidatedTextField>
</Grid>
</Grid>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
General Options
{LL.GENERAL_OPTIONS()}
</Typography>
<Box
sx={{
'& .MuiTextField-root': { width: '25ch' }
}}
>
<ValidatedTextField
name="locale"
label={LL.LANGUAGE_ENTITIES()}
disabled={saving}
value={data.locale}
variant="outlined"
onChange={updateFormValue}
margin="normal"
size="small"
select
>
<MenuItem value="en">English (EN)</MenuItem>
<Divider />
<MenuItem value="de">Deutsch (DE)</MenuItem>
<MenuItem value="fr">Français (FR)</MenuItem>
<MenuItem value="nl">Nederlands (NL)</MenuItem>
<MenuItem value="no">Norsk (NO)</MenuItem>
<MenuItem value="pl">Polski (PL)</MenuItem>
<MenuItem value="sv">Svenska (SV)</MenuItem>
</ValidatedTextField>
</Box>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
label="Hide LED"
label={LL.HIDE_LED()}
disabled={saving}
/>
)}
<BlockFormControlLabel
control={<Checkbox checked={data.telnet_enabled} onChange={updateFormValue} name="telnet_enabled" />}
label="Enable Telnet Console"
label={LL.ENABLE_TELNET()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.analog_enabled} onChange={updateFormValue} name="analog_enabled" />}
label="Enable Analog Sensors"
label={LL.ENABLE_ANALOG()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />}
label="Convert temperature values to Fahrenheit"
label={LL.CONVERT_FAHRENHEIT()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.notoken_api} onChange={updateFormValue} name="notoken_api" />}
label="Bypass Access Token authorization on API calls"
label={LL.BYPASS_TOKEN()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
label="Enable Read only mode (blocks all outgoing EMS Tx write commands)"
label={LL.READONLY()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.low_clock} onChange={updateFormValue} name="low_clock" />}
label="Underclock CPU speed"
label={LL.UNDERCLOCK_CPU()}
disabled={saving}
/>
<Grid container spacing={0} direction="row" justifyContent="flex-start" alignItems="flex-start">
<BlockFormControlLabel
control={<Checkbox checked={data.shower_timer} onChange={updateFormValue} name="shower_timer" />}
label="Enable Shower Timer"
label={LL.ENABLE_SHOWER_TIMER()}
disabled={saving}
/>
<BlockFormControlLabel
sx={{ pb: 2 }}
control={<Checkbox checked={data.shower_alert} onChange={updateFormValue} name="shower_alert" />}
label="Enable Shower Alert"
label={LL.ENABLE_SHOWER_ALERT()}
disabled={!data.shower_timer}
/>
{data.shower_alert && (
<>
<Grid item xs={2}>
<Grid item sx={{ pr: 1, pb: 2 }}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_trigger"
label="Trigger Time (minutes)"
label={LL.TRIGGER_TIME()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_trigger}
type="number"
onChange={updateFormValue}
size="small"
disabled={!data.shower_timer}
/>
</Grid>
<Grid item xs={2}>
<Grid item sx={{ pb: 3 }}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_coldshot"
label="Cold Shot Time (seconds)"
label={LL.COLD_SHOT_DURATION()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_coldshot}
type="number"
onChange={updateFormValue}
size="small"
disabled={!data.shower_timer}
/>
</Grid>
</>
)}
</Grid>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Formatting Options
<Typography variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()}
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
name="bool_dashboard"
label="Boolean Format Dashboard"
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
value={data.bool_dashboard}
fullWidth
variant="outlined"
@@ -413,16 +472,16 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>on/off</MenuItem>
<MenuItem value={2}>ON/OFF</MenuItem>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>true/false</MenuItem>
<MenuItem value={5}>1/0</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
name="bool_format"
label="Boolean Format API/MQTT"
label={LL.BOOLEAN_FORMAT_API()}
value={data.bool_format}
fullWidth
variant="outlined"
@@ -430,18 +489,18 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>"on"/"off"</MenuItem>
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>"true"/"false"</MenuItem>
<MenuItem value={4}>true/false</MenuItem>
<MenuItem value={5}>"1"/"0"</MenuItem>
<MenuItem value={6}>1/0</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={4}>
<Grid item xs={6} sm={4}>
<ValidatedTextField
name="enum_format"
label="Enum Format API/MQTT"
label={LL.ENUM_FORMAT()}
value={data.enum_format}
fullWidth
variant="outlined"
@@ -449,29 +508,29 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>Value</MenuItem>
<MenuItem value={2}>Index</MenuItem>
<MenuItem value={1}>{LL.VALUE(1)}</MenuItem>
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
</ValidatedTextField>
</Grid>
</Grid>
{data.dallas_gpio !== 0 && (
<>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Temperature Sensors
{LL.TEMP_SENSORS()}
</Typography>
<BlockFormControlLabel
control={<Checkbox checked={data.dallas_parasite} onChange={updateFormValue} name="dallas_parasite" />}
label="Enable parasite power"
label={LL.ENABLE_PARASITE()}
disabled={saving}
/>
</>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Logging
{LL.LOGGING()}
</Typography>
<BlockFormControlLabel
control={<Checkbox checked={data.trace_raw} onChange={updateFormValue} name="trace_raw" />}
label="Log EMS telegrams in hexadecimal"
label={LL.LOG_HEX()}
disabled={saving}
/>
<BlockFormControlLabel
@@ -483,11 +542,11 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
}
label="Enable Syslog"
label={LL.ENABLE_SYSLOG()}
/>
{data.syslog_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_host"
@@ -500,7 +559,7 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_port"
@@ -514,10 +573,10 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
name="syslog_level"
label="Log Level"
label={LL.LOG_LEVEL()}
value={data.syslog_level}
fullWidth
variant="outlined"
@@ -534,11 +593,14 @@ const SettingsApplication: FC = () => {
<MenuItem value={9}>ALL</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_mark_interval"
label="Mark Interval (seconds, 0=off)"
label={LL.MARK_INTERVAL()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={data.syslog_mark_interval}
@@ -551,9 +613,9 @@ const SettingsApplication: FC = () => {
</Grid>
)}
{restartNeeded && (
<MessageBox my={2} level="warning" message="EMS-ESP needs to be restarted to apply changed system settings">
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
Restart
{LL.RESTART()}
</Button>
</MessageBox>
)}
@@ -567,7 +629,7 @@ const SettingsApplication: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
)}
@@ -576,8 +638,8 @@ const SettingsApplication: FC = () => {
};
return (
<SectionContent title="Application Settings" titleGutter>
{content()}
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};

View File

@@ -17,9 +17,11 @@ import {
Link
} from '@mui/material';
import { MessageBox } from '../components';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import { Table } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useSnackbar } from 'notistack';
@@ -28,9 +30,6 @@ import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Cancel';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
import SearchIcon from '@mui/icons-material/Search';
import FilterListIcon from '@mui/icons-material/FilterList';
@@ -40,16 +39,25 @@ import { ButtonRow, FormLoader, ValidatedTextField, SectionContent } from '../co
import * as EMSESP from './api';
import { extractErrorMessage } from '../utils';
import { extractErrorMessage, updateValue } from '../utils';
import { DeviceShort, Devices, DeviceEntity, DeviceEntityMask } from './types';
import { useI18nContext } from '../i18n/i18n-react';
import RestartMonitor from '../framework/system/RestartMonitor';
export const APIURL = window.location.origin + '/api/';
const SettingsCustomization: FC = () => {
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>(false);
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar();
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([{ id: '', v: 0, n: '', m: 0, w: false }]);
const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false };
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([emptyDeviceEntity]);
const [devices, setDevices] = useState<Devices>();
const [errorMessage, setErrorMessage] = useState<string>();
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
@@ -57,12 +65,14 @@ const SettingsCustomization: FC = () => {
const [selectedFilters, setSelectedFilters] = useState<number>(0);
const [search, setSearch] = useState('');
const [deviceEntity, setDeviceEntity] = useState<DeviceEntity>();
// eslint-disable-next-line
const [masks, setMasks] = useState(() => ['']);
const entities_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: 120px repeat(1, minmax(0, 1fr)) 120px;
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px 45px 120px;
`,
BaseRow: `
font-size: 14px;
@@ -71,6 +81,12 @@ const SettingsCustomization: FC = () => {
}
`,
BaseCell: `
&:nth-of-type(3) {
text-align: right;
}
&:nth-of-type(4) {
text-align: right;
}
&:last-of-type {
text-align: right;
}
@@ -92,6 +108,7 @@ const SettingsCustomization: FC = () => {
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
.td {
border-top: 1px solid #565656;
@@ -104,6 +121,11 @@ const SettingsCustomization: FC = () => {
font-weight: normal;
}
&:hover .td {
border-top: 1px solid #177ac9;
border-bottom: 1px solid #177ac9;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
@@ -112,56 +134,36 @@ const SettingsCustomization: FC = () => {
&:nth-of-type(2) {
padding: 8px;
}
&:nth-of-type(3) {
padding-right: 4px;
}
&:nth-of-type(4) {
padding-right: 4px;
}
&:last-of-type {
padding-right: 8px;
}
`
});
const getSortIcon = (state: any, sortKey: any) => {
if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />;
}
if (state.sortKey === sortKey && !state.reverse) {
return <KeyboardArrowUpOutlinedIcon />;
}
return <UnfoldMoreOutlinedIcon />;
};
const entity_sort = useSort(
{ nodes: deviceEntities },
{},
{
sortIcon: {
iconDefault: <UnfoldMoreOutlinedIcon />,
iconUp: <KeyboardArrowUpOutlinedIcon />,
iconDown: <KeyboardArrowDownOutlinedIcon />
},
sortToggleType: SortToggleType.AlternateWithReset,
sortFns: {
NAME: (array) => array.sort((a, b) => a.id.localeCompare(b.id))
}
}
);
const fetchDevices = useCallback(async () => {
try {
setDevices((await EMSESP.readDevices()).data);
} catch (error: unknown) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch device list'));
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, []);
}, [LL]);
const setInitialMask = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, om: de.m })));
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
};
const fetchDeviceEntities = async (unique_id: number) => {
try {
const data = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
setInitialMask(data);
} catch (error: unknown) {
setErrorMessage(extractErrorMessage(error, 'Problem fetching device entities'));
const new_deviceEntities = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
setInitialMask(new_deviceEntities);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
@@ -181,15 +183,10 @@ const SettingsCustomization: FC = () => {
}
function formatName(de: DeviceEntity) {
if (de.n === undefined || de.n === de.id) {
return de.id;
} else if (de.n === '') {
return 'Command: ' + de.id;
}
return (
<>
{de.n}&nbsp;(
<Link target="_blank" href={APIURL + devices?.devices[selectedDevice].t + '/' + de.id}>
{de.n && (de.n[0] === '!' ? LL.COMMAND() + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) + ' '}(
<Link target="_blank" href={APIURL + devices?.devices[selectedDevice].tn + '/' + de.id}>
{de.id}
</Link>
)
@@ -219,6 +216,9 @@ const SettingsCustomization: FC = () => {
if ((m & 8) === 8) {
new_masks.push('8');
}
if ((m & 128) === 128) {
new_masks.push('128');
}
return new_masks;
};
@@ -244,43 +244,65 @@ const SettingsCustomization: FC = () => {
const selected_device = parseInt(event.target.value, 10);
setSelectedDevice(selected_device);
fetchDeviceEntities(devices?.devices[selected_device].i);
setRestartNeeded(false);
}
};
const resetCustomization = async () => {
try {
await EMSESP.resetCustomizations();
enqueueSnackbar('All customizations have been removed. Restarting...', { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem resetting customizations'), { variant: 'error' });
enqueueSnackbar(LL.CUSTOMIZATIONS_RESTART(), { variant: 'info' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setConfirmReset(false);
}
};
const restart = async () => {
try {
await EMSESP.restart();
setRestarting(true);
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
};
const saveCustomization = async () => {
if (devices && deviceEntities && selectedDevice !== -1) {
const masked_entities = deviceEntities
.filter((de) => de.m !== de.om)
.map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.id);
.filter((de) => de.m !== de.o_m || de.cn !== de.o_cn || de.ma !== de.o_ma || de.mi !== de.o_mi)
.map(
(new_de) =>
new_de.m.toString(16).padStart(2, '0') +
new_de.id +
(new_de.cn || new_de.mi || new_de.ma ? '|' : '') +
(new_de.cn ? new_de.cn : '') +
(new_de.mi ? '>' + new_de.mi : '') +
(new_de.ma ? '<' + new_de.ma : '')
);
if (masked_entities.length > 60) {
enqueueSnackbar('Selected entities exceeded limit of 60. Please Save in batches', { variant: 'warning' });
// check size in bytes to match buffer in CPP, which is 2048
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
if (bytes > 2000) {
enqueueSnackbar(LL.CUSTOMIZATIONS_FULL(), { variant: 'warning' });
return;
}
try {
const response = await EMSESP.writeMaskedEntities({
const response = await EMSESP.writeCustomEntities({
id: devices?.devices[selectedDevice].i,
entity_ids: masked_entities
});
if (response.status === 200) {
enqueueSnackbar('Customization saved', { variant: 'success' });
enqueueSnackbar(LL.CUSTOMIZATIONS_SAVED(), { variant: 'success' });
} else if (response.status === 201) {
setRestartNeeded(true);
} else {
enqueueSnackbar('Customization save failed', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem sending entity list'), { variant: 'error' });
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
setInitialMask(deviceEntities);
}
@@ -294,21 +316,18 @@ const SettingsCustomization: FC = () => {
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">Select a device and customize each of its entities using the options:</Typography>
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}</Typography>
<Typography variant="body2">
<OptionIcon type="favorite" isSet={true} />
=mark as favorite&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />
=disable write action&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />
=exclude from MQTT and API&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />
=hide from Dashboard
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography>
</Box>
<ValidatedTextField
name="device"
label="EMS Device"
label={LL.EMS_DEVICE()}
variant="outlined"
fullWidth
value={selectedDevice}
@@ -317,7 +336,7 @@ const SettingsCustomization: FC = () => {
select
>
<MenuItem disabled key={0} value={-1}>
Select a device...
{LL.SELECT_DEVICE()}...
</MenuItem>
{devices.devices.map((device: DeviceShort, index) => (
<MenuItem key={index} value={index}>
@@ -329,6 +348,33 @@ const SettingsCustomization: FC = () => {
);
};
const editEntity = (de: DeviceEntity) => {
if (de.n === undefined || (de.n && de.n[0] === '!')) {
return;
}
if (de.cn === undefined) {
de.cn = '';
}
setDeviceEntity(de);
};
const updateEntity = () => {
if (deviceEntity) {
setDeviceEntities((prevState) => {
const newState = prevState.map((obj) => {
if (obj.id === deviceEntity.id) {
return { ...obj, cn: deviceEntity.cn, mi: deviceEntity.mi, ma: deviceEntity.ma };
}
return obj;
});
return newState;
});
}
setDeviceEntity(undefined);
};
const renderDeviceData = () => {
if (devices?.devices.length === 0 || deviceEntities[0].id === '') {
return;
@@ -389,6 +435,9 @@ const SettingsCustomization: FC = () => {
<ToggleButton value="1">
<OptionIcon type="web_exclude" isSet={true} />
</ToggleButton>
<ToggleButton value="128">
<OptionIcon type="deleted" isSet={true} />
</ToggleButton>
</ToggleButtonGroup>
</Grid>
@@ -401,7 +450,7 @@ const SettingsCustomization: FC = () => {
color="inherit"
onClick={() => maskDisabled(false)}
>
set all&nbsp;
{LL.SET_ALL()}&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={false} />
<OptionIcon type="web_exclude" isSet={false} />
</Button>
@@ -416,36 +465,34 @@ const SettingsCustomization: FC = () => {
color="inherit"
onClick={() => maskDisabled(true)}
>
set all&nbsp;
{LL.SET_ALL()}&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />
<OptionIcon type="web_exclude" isSet={true} />
</Button>
</Tooltip>
</Grid>
</Grid>
<Table data={{ nodes: shown_data }} theme={entities_theme} sort={entity_sort} layout={{ custom: true }}>
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff>OPTIONS</HeaderCell>
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(entity_sort.state, 'NAME')}
onClick={() => entity_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
<Button fullWidth style={{ fontSize: '14px', justifyContent: 'flex-start' }}>
{LL.NAME(1)}
</Button>
</HeaderCell>
<HeaderCell resize>VALUE</HeaderCell>
<HeaderCell stiff>{LL.MIN()}</HeaderCell>
<HeaderCell stiff>{LL.MAX()}</HeaderCell>
<HeaderCell resize>{LL.VALUE(0)}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((de: DeviceEntity) => (
<Row key={de.id} item={de}>
<Row key={de.id} item={de} onClick={() => editEntity(de)}>
<Cell stiff>
{!deviceEntity && (
<ToggleButtonGroup
size="small"
color="secondary"
@@ -461,19 +508,19 @@ const SettingsCustomization: FC = () => {
setMasks(['']);
}}
>
<ToggleButton value="8" disabled={(de.m & 1) !== 0 || de.n === undefined}>
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
<OptionIcon
type="favorite"
isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE}
/>
</ToggleButton>
<ToggleButton value="4" disabled={!de.w || (de.m & 3) === 3}>
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
<OptionIcon
type="readonly"
isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY}
/>
</ToggleButton>
<ToggleButton value="2" disabled={de.n === ''}>
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
<OptionIcon
type="api_mqtt_exclude"
isSet={
@@ -481,16 +528,25 @@ const SettingsCustomization: FC = () => {
}
/>
</ToggleButton>
<ToggleButton value="1" disabled={de.n === undefined}>
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
<OptionIcon
type="web_exclude"
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
/>
</ToggleButton>
<ToggleButton value="128">
<OptionIcon
type="deleted"
isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED}
/>
</ToggleButton>
</ToggleButtonGroup>
)}
</Cell>
<Cell>{formatName(de)}</Cell>
<Cell>{formatValue(de.v)}</Cell>
<Cell>{!deviceEntity && formatName(de)}</Cell>
<Cell>{!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
<Cell>{!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
<Cell>{!deviceEntity && formatValue(de.v)}</Cell>
</Row>
))}
</Body>
@@ -503,14 +559,11 @@ const SettingsCustomization: FC = () => {
const renderResetDialog = () => (
<Dialog open={confirmReset} onClose={() => setConfirmReset(false)}>
<DialogTitle>Reset</DialogTitle>
<DialogContent dividers>
Are you sure you want remove all customizations including the custom settings of the Temperature and Analog
sensors?
</DialogContent>
<DialogTitle>{LL.RESET(1)}</DialogTitle>
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
@@ -519,25 +572,32 @@ const SettingsCustomization: FC = () => {
autoFocus
color="error"
>
Reset
{LL.RESET(0)}
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
return (
const renderContent = () => (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
Device Entities
{LL.DEVICE_ENTITIES()}
</Typography>
{renderDeviceList()}
{renderDeviceData()}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
{LL.RESTART()}
</Button>
</MessageBox>
)}
{!restartNeeded && (
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<ButtonRow>
<Button startIcon={<SaveIcon />} variant="outlined" color="primary" onClick={() => saveCustomization()}>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</Box>
@@ -548,18 +608,90 @@ const SettingsCustomization: FC = () => {
color="error"
onClick={() => setConfirmReset(true)}
>
Reset
{LL.RESET(0)}
</Button>
</ButtonRow>
</Box>
)}
{renderResetDialog()}
</>
);
const renderEditDialog = () => {
if (deviceEntity) {
const de = deviceEntity;
return (
<Dialog open={!!deviceEntity} onClose={() => setDeviceEntity(undefined)}>
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY() + ' "' + de.id + '"'}</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" mb={2}>
<Typography variant="body2">
{LL.DEFAULT(1) + ' ' + LL.NAME(1)}:&nbsp;{deviceEntity.n}
</Typography>
</Box>
<Grid container spacing={1}>
<Grid item>
<TextField
name="cn"
label={LL.NEW_NAME_OF(LL.ENTITY())}
value={deviceEntity.cn}
autoFocus
sx={{ width: '30ch' }}
onChange={updateValue(setDeviceEntity)}
/>
</Grid>
{typeof de.v === 'number' && de.w && !(de.m & DeviceEntityMask.DV_READONLY) && (
<>
<Grid item>
<TextField
name="mi"
label={LL.MIN()}
value={deviceEntity.mi}
sx={{ width: '8ch' }}
onChange={updateValue(setDeviceEntity)}
/>
</Grid>
<Grid item>
<TextField
name="ma"
label={LL.MAX()}
value={deviceEntity.ma}
sx={{ width: '8ch' }}
onChange={updateValue(setDeviceEntity)}
/>
</Grid>
</>
)}
</Grid>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setDeviceEntity(undefined)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
variant="outlined"
type="submit"
onClick={() => updateEntity()}
color="warning"
>
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
);
}
};
return (
<SectionContent title="User Customization" titleGutter>
{content()}
<SectionContent title={LL.USER_CUSTOMIZATION()} titleGutter>
{restarting ? <RestartMonitor /> : renderContent()}
{renderEditDialog()}
</SectionContent>
);
};

View File

@@ -12,7 +12,7 @@ import {
DeviceData,
DeviceEntity,
UniqueID,
MaskedEntities,
CustomEntities,
WriteValue,
WriteSensor,
WriteAnalog,
@@ -63,8 +63,8 @@ export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEnti
return AXIOS_BIN.post('/deviceEntities', unique_id);
}
export function writeMaskedEntities(maskedEntities: MaskedEntities): AxiosPromise<void> {
return AXIOS.post('/maskedEntities', maskedEntities);
export function writeCustomEntities(customEntities: CustomEntities): AxiosPromise<void> {
return AXIOS.post('/customEntities', customEntities);
}
export function writeValue(writevalue: WriteValue): AxiosPromise<void> {

View File

@@ -1,4 +1,5 @@
export interface Settings {
locale: string;
tx_mode: number;
ems_bus_id: number;
syslog_enabled: boolean;
@@ -32,6 +33,7 @@ export interface Settings {
eth_power: number;
eth_phy_addr: number;
eth_clock_mode: number;
platform: string;
}
export enum busConnectionStatus {
@@ -41,7 +43,7 @@ export enum busConnectionStatus {
}
export interface Stat {
id: string; // name
id: string; // id - needs to be a string
s: number; // success
f: number; // fail
q: number; // quality
@@ -57,7 +59,8 @@ export interface Status {
}
export interface Device {
id: string; // id index
t: string; // type
tn: string; // device type translated name
t: number; // device type id
b: string; // brand
n: string; // name
d: number; // deviceid
@@ -99,6 +102,7 @@ export interface SensorData {
export interface CoreData {
connected: boolean;
devices: Device[];
s_n: string;
active_sensors: number;
analog_enabled: boolean;
}
@@ -108,7 +112,8 @@ export interface DeviceShort {
d?: number; // deviceid
p?: number; // productid
s: string; // shortname
t?: string; // device type name
t?: number; // device type id
tn?: string; // device type internal name
}
export interface Devices {
@@ -136,12 +141,18 @@ export interface DeviceEntity {
id: string; // shortname
v?: any; // value, in any format, optional
n?: string; // fullname, optional
cn?: string; // custom fullname, optional
m: number; // mask
om?: number; // original mask before edits
o_m?: number; // original mask before edits
o_cn?: string; // original cn before edits
w: boolean; // writeable
mi?: string; // min value
ma?: string; // max value
o_mi?: string;
o_ma?: string;
}
export interface MaskedEntities {
export interface CustomEntities {
id: number;
entity_ids: string[];
}
@@ -171,7 +182,9 @@ export enum DeviceValueUOM {
MV,
SQM,
M3,
L
L,
KMIN,
K
}
export const DeviceValueUOM_s = [
@@ -184,18 +197,20 @@ export const DeviceValueUOM_s = [
'Wh',
'hours',
'minutes',
'uA',
'µA',
'bar',
'kW',
'W',
'KB',
'second',
'seconds',
'dBm',
'°F',
'mV',
'sqm',
'm3',
'l'
'm²',
'm³',
'l',
'K*min',
'K'
];
export enum AnalogType {
@@ -235,7 +250,10 @@ export const BOARD_PROFILES: BoardProfiles = {
'MH-ET': 'MH-ET Live D1 Mini',
LOLIN: 'Lolin D32',
OLIMEX: 'Olimex ESP32-EVB',
OLIMEXPOE: 'Olimex ESP32-POE'
OLIMEXPOE: 'Olimex ESP32-POE',
C3MINI: 'Wemos C3 Mini',
S2MINI: 'Wemos S2 Mini',
S3MINI: 'Liligo S3'
};
export interface BoardProfileName {
@@ -280,5 +298,6 @@ export enum DeviceEntityMask {
DV_WEB_EXCLUDE = 1,
DV_API_MQTT_EXCLUDE = 2,
DV_READONLY = 4,
DV_FAVORITE = 8
DV_FAVORITE = 8,
DV_DELETED = 128
}

View File

@@ -7,12 +7,13 @@ export const GPIO_VALIDATOR = {
if (
value &&
(value === 1 ||
(value >= 6 && value <= 12) ||
(value >= 10 && value <= 12) ||
(value >= 14 && value <= 15) ||
value === 20 ||
value === 24 ||
(value >= 28 && value <= 31) ||
value > 40)
value > 40 ||
value < 0)
) {
callback('Must be an valid GPIO port');
} else {
@@ -21,24 +22,61 @@ export const GPIO_VALIDATOR = {
}
};
export const GPIO_VALIDATORC3 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
callback('Must be an valid GPIO port');
} else {
callback();
}
}
};
export const GPIO_VALIDATORS2 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
if (value && ((value >= 19 && value <= 20) || (value >= 22 && value <= 32) || value > 40 || value < 0)) {
callback('Must be an valid GPIO port');
} else {
callback();
}
}
};
export const createSettingsValidator = (settings: Settings) =>
new Schema({
...(settings.board_profile === 'CUSTOM' && {
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATOR],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATOR],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATOR],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATOR],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
}),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-C3' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORC3],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORC3],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORC3],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORC3],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORC3]
}),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-S2' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS2],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS2],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS2],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS2],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS2]
}),
...(settings.syslog_enabled && {
syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
syslog_port: [
{ required: true, message: 'Port is required' },
{ type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' }
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
],
syslog_mark_interval: [
{ required: true, message: 'Mark interval is required' },
{ type: 'number', min: 0, max: 10, message: 'Port must be between 0 and 10' }
{ type: 'number', min: 0, max: 10, message: ' must be between 0 and 10' }
]
}),
...(settings.shower_alert && {

View File

@@ -15,6 +15,8 @@ export interface MqttStatus {
client_id: string;
disconnect_reason: MqttDisconnectReason;
mqtt_fails: number;
mqtt_queued: number;
connect_count: number;
}
export interface MqttSettings {
@@ -27,13 +29,14 @@ export interface MqttSettings {
client_id: string;
keep_alive: number;
clean_session: boolean;
max_topic_length: number;
entity_format: number;
publish_time_boiler: number;
publish_time_thermostat: number;
publish_time_solar: number;
publish_time_mixer: number;
publish_time_other: number;
publish_time_sensor: number;
publish_time_heartbeat: number;
mqtt_qos: number;
mqtt_retain: boolean;
ha_enabled: boolean;

View File

@@ -48,6 +48,8 @@ export interface NetworkSettings {
dns_ip_1?: string;
dns_ip_2?: string;
enableMDNS: boolean;
enableCORS: boolean;
CORSOrigin: string;
}
export interface WiFiNetworkList {

View File

@@ -1,36 +1,23 @@
export enum EspPlatform {
ESP8266 = 'esp8266',
ESP32 = 'esp32'
}
interface ESPSystemStatus {
export interface SystemStatus {
emsesp_version: string;
esp_platform: EspPlatform;
esp_platform: string;
max_alloc_heap: number;
cpu_freq_mhz: number;
free_heap: number;
sdk_version: string;
flash_chip_size: number;
flash_chip_speed: number;
app_used: number;
app_free: number;
fs_used: number;
fs_total: number;
fs_free: number;
uptime: string;
free_mem: number;
psram_size?: number;
free_psram?: number;
has_loader: boolean;
}
export interface ESP32SystemStatus extends ESPSystemStatus {
esp_platform: EspPlatform.ESP32;
psram_size: number;
free_psram: number;
}
export interface ESP8266SystemStatus extends ESPSystemStatus {
esp_platform: EspPlatform.ESP8266;
heap_fragmentation: number;
}
export type SystemStatus = ESP8266SystemStatus | ESP32SystemStatus;
export interface OTASettings {
enabled: boolean;
port: number;

View File

@@ -1,8 +1,6 @@
import { AxiosError } from 'axios';
export const extractErrorMessage = (error: unknown, defaultMessage: string) => {
if (error instanceof AxiosError) {
return defaultMessage + ' (' + error.request.statusText + ')';
export const extractErrorMessage = (error: any, defaultMessage: string) => {
if (error.request) {
return defaultMessage + ' (' + error.request.status + ': ' + error.request.statusText + ')';
} else if (error instanceof Error) {
return defaultMessage + ' (' + error.message + ')';
}

View File

@@ -1,5 +1,3 @@
import parseMilliseconds from 'parse-ms';
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
day: 'numeric',
month: 'short',
@@ -21,21 +19,6 @@ export const formatLocalDateTime = (date: Date) => {
export const pluralize = (count: number, noun: string) =>
`${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? 's' : ''}`;
export const formatDurationMin = (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;
};
export const formatDurationSec = (duration_sec: number) => {
if (duration_sec === 0) {
return ' ';

View File

@@ -4,12 +4,16 @@ import { AxiosPromise } from 'axios';
import { extractErrorMessage } from '.';
import { useI18nContext } from '../i18n/i18n-react';
export interface RestRequestOptions<D> {
read: () => AxiosPromise<D>;
update?: (value: D) => AxiosPromise<D>;
}
export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [saving, setSaving] = useState<boolean>(false);
@@ -22,12 +26,12 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
setErrorMessage(undefined);
try {
setData((await read()).data);
} catch (error: unknown) {
const message = extractErrorMessage(error, 'Problem loading data');
} catch (error) {
const message = extractErrorMessage(error, LL.PROBLEM_LOADING());
enqueueSnackbar(message, { variant: 'error' });
setErrorMessage(message);
}
}, [read, enqueueSnackbar]);
}, [read, enqueueSnackbar, LL]);
const save = useCallback(
async (toSave: D) => {
@@ -43,17 +47,17 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
if (response.status === 202) {
setRestartNeeded(true);
} else {
enqueueSnackbar('Settings saved', { variant: 'success' });
enqueueSnackbar(LL.SETTINGS_OF('') + ' ' + LL.SAVED(), { variant: 'success' });
}
} catch (error: unknown) {
const message = extractErrorMessage(error, 'Problem saving data');
} catch (error) {
const message = extractErrorMessage(error, LL.PROBLEM_UPDATING());
enqueueSnackbar(message, { variant: 'error' });
setErrorMessage(message);
} finally {
setSaving(false);
}
},
[update, enqueueSnackbar]
[update, enqueueSnackbar, LL]
);
const saveData = () => data && save(data);

View File

@@ -2,11 +2,9 @@ import Schema from 'async-validator';
export const SIGN_IN_REQUEST_VALIDATOR = new Schema({
username: {
required: true,
message: 'Please provide a username'
required: true
},
password: {
required: true,
message: 'Please provide a password'
required: true
}
});

View File

@@ -1,18 +1,23 @@
import Schema from 'async-validator';
import { MqttSettings } from '../types';
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
export const MQTT_SETTINGS_VALIDATOR = new Schema({
export const createMqttSettingsValidator = (mqttSettings: MqttSettings) =>
new Schema({
...(mqttSettings.enabled && {
host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
base: { required: true, message: 'Base is required' },
port: [
{ required: true, message: 'Port is required' },
{ type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' }
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
],
keep_alive: [
{ required: true, message: 'Keep alive is required' },
{ type: 'number', min: 1, max: 86400, message: 'Keep alive must be between 1 and 86400' }
],
max_topic_length: [
{ required: true, message: 'Max topic length is required' },
{ type: 'number', min: 16, max: 1024, message: 'Max topic length must be between 16 and 1024' }
publish_time_heartbeat: [
{ required: true, message: 'Heartbeat is required' },
{ type: 'number', min: 10, max: 86400, message: 'Heartbeat must be between 10 and 86400' }
]
});
})
});

View File

@@ -14,6 +14,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"useUnknownInCatchVariables": false,
"jsx": "react-jsx"
},
"include": ["src"]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,410 @@
/*!
* @file Adafruit_NeoPixel.h
*
* This is part of Adafruit's NeoPixel library for the Arduino platform,
* allowing a broad range of microcontroller boards (most AVR boards,
* many ARM devices, ESP8266 and ESP32, among others) to control Adafruit
* NeoPixels, FLORA RGB Smart Pixels and compatible devices -- WS2811,
* WS2812, WS2812B, SK6812, etc.
*
* Adafruit invests time and resources providing this open source code,
* please support Adafruit and open-source hardware by purchasing products
* from Adafruit!
*
* Written by Phil "Paint Your Dragon" Burgess for Adafruit Industries,
* with contributions by PJRC, Michael Miller and other members of the
* open source community.
*
* This file is part of the Adafruit_NeoPixel library.
*
* Adafruit_NeoPixel is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Adafruit_NeoPixel is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with NeoPixel. If not, see
* <http://www.gnu.org/licenses/>.
*
*/
#ifndef ADAFRUIT_NEOPIXEL_H
#define ADAFRUIT_NEOPIXEL_H
#ifdef ARDUINO
#if (ARDUINO >= 100)
#include <Arduino.h>
#else
#include <WProgram.h>
#include <pins_arduino.h>
#endif
#ifdef USE_TINYUSB // For Serial when selecting TinyUSB
#include <Adafruit_TinyUSB.h>
#endif
#endif
#ifdef TARGET_LPC1768
#include <Arduino.h>
#endif
#if defined(ARDUINO_ARCH_RP2040)
#include <stdlib.h>
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "rp2040_pio.h"
#endif
// The order of primary colors in the NeoPixel data stream can vary among
// device types, manufacturers and even different revisions of the same
// item. The third parameter to the Adafruit_NeoPixel constructor encodes
// the per-pixel byte offsets of the red, green and blue primaries (plus
// white, if present) in the data stream -- the following #defines provide
// an easier-to-use named version for each permutation. e.g. NEO_GRB
// indicates a NeoPixel-compatible device expecting three bytes per pixel,
// with the first byte transmitted containing the green value, second
// containing red and third containing blue. The in-memory representation
// of a chain of NeoPixels is the same as the data-stream order; no
// re-ordering of bytes is required when issuing data to the chain.
// Most of these values won't exist in real-world devices, but it's done
// this way so we're ready for it (also, if using the WS2811 driver IC,
// one might have their pixels set up in any weird permutation).
// Bits 5,4 of this value are the offset (0-3) from the first byte of a
// pixel to the location of the red color byte. Bits 3,2 are the green
// offset and 1,0 are the blue offset. If it is an RGBW-type device
// (supporting a white primary in addition to R,G,B), bits 7,6 are the
// offset to the white byte...otherwise, bits 7,6 are set to the same value
// as 5,4 (red) to indicate an RGB (not RGBW) device.
// i.e. binary representation:
// 0bWWRRGGBB for RGBW devices
// 0bRRRRGGBB for RGB
// RGB NeoPixel permutations; white and red offsets are always same
// Offset: W R G B
#define NEO_RGB ((0 << 6) | (0 << 4) | (1 << 2) | (2)) ///< Transmit as R,G,B
#define NEO_RBG ((0 << 6) | (0 << 4) | (2 << 2) | (1)) ///< Transmit as R,B,G
#define NEO_GRB ((1 << 6) | (1 << 4) | (0 << 2) | (2)) ///< Transmit as G,R,B
#define NEO_GBR ((2 << 6) | (2 << 4) | (0 << 2) | (1)) ///< Transmit as G,B,R
#define NEO_BRG ((1 << 6) | (1 << 4) | (2 << 2) | (0)) ///< Transmit as B,R,G
#define NEO_BGR ((2 << 6) | (2 << 4) | (1 << 2) | (0)) ///< Transmit as B,G,R
// RGBW NeoPixel permutations; all 4 offsets are distinct
// Offset: W R G B
#define NEO_WRGB ((0 << 6) | (1 << 4) | (2 << 2) | (3)) ///< Transmit as W,R,G,B
#define NEO_WRBG ((0 << 6) | (1 << 4) | (3 << 2) | (2)) ///< Transmit as W,R,B,G
#define NEO_WGRB ((0 << 6) | (2 << 4) | (1 << 2) | (3)) ///< Transmit as W,G,R,B
#define NEO_WGBR ((0 << 6) | (3 << 4) | (1 << 2) | (2)) ///< Transmit as W,G,B,R
#define NEO_WBRG ((0 << 6) | (2 << 4) | (3 << 2) | (1)) ///< Transmit as W,B,R,G
#define NEO_WBGR ((0 << 6) | (3 << 4) | (2 << 2) | (1)) ///< Transmit as W,B,G,R
#define NEO_RWGB ((1 << 6) | (0 << 4) | (2 << 2) | (3)) ///< Transmit as R,W,G,B
#define NEO_RWBG ((1 << 6) | (0 << 4) | (3 << 2) | (2)) ///< Transmit as R,W,B,G
#define NEO_RGWB ((2 << 6) | (0 << 4) | (1 << 2) | (3)) ///< Transmit as R,G,W,B
#define NEO_RGBW ((3 << 6) | (0 << 4) | (1 << 2) | (2)) ///< Transmit as R,G,B,W
#define NEO_RBWG ((2 << 6) | (0 << 4) | (3 << 2) | (1)) ///< Transmit as R,B,W,G
#define NEO_RBGW ((3 << 6) | (0 << 4) | (2 << 2) | (1)) ///< Transmit as R,B,G,W
#define NEO_GWRB ((1 << 6) | (2 << 4) | (0 << 2) | (3)) ///< Transmit as G,W,R,B
#define NEO_GWBR ((1 << 6) | (3 << 4) | (0 << 2) | (2)) ///< Transmit as G,W,B,R
#define NEO_GRWB ((2 << 6) | (1 << 4) | (0 << 2) | (3)) ///< Transmit as G,R,W,B
#define NEO_GRBW ((3 << 6) | (1 << 4) | (0 << 2) | (2)) ///< Transmit as G,R,B,W
#define NEO_GBWR ((2 << 6) | (3 << 4) | (0 << 2) | (1)) ///< Transmit as G,B,W,R
#define NEO_GBRW ((3 << 6) | (2 << 4) | (0 << 2) | (1)) ///< Transmit as G,B,R,W
#define NEO_BWRG ((1 << 6) | (2 << 4) | (3 << 2) | (0)) ///< Transmit as B,W,R,G
#define NEO_BWGR ((1 << 6) | (3 << 4) | (2 << 2) | (0)) ///< Transmit as B,W,G,R
#define NEO_BRWG ((2 << 6) | (1 << 4) | (3 << 2) | (0)) ///< Transmit as B,R,W,G
#define NEO_BRGW ((3 << 6) | (1 << 4) | (2 << 2) | (0)) ///< Transmit as B,R,G,W
#define NEO_BGWR ((2 << 6) | (3 << 4) | (1 << 2) | (0)) ///< Transmit as B,G,W,R
#define NEO_BGRW ((3 << 6) | (2 << 4) | (1 << 2) | (0)) ///< Transmit as B,G,R,W
// Add NEO_KHZ400 to the color order value to indicate a 400 KHz device.
// All but the earliest v1 NeoPixels expect an 800 KHz data stream, this is
// the default if unspecified. Because flash space is very limited on ATtiny
// devices (e.g. Trinket, Gemma), v1 NeoPixels aren't handled by default on
// those chips, though it can be enabled by removing the ifndef/endif below,
// but code will be bigger. Conversely, can disable the NEO_KHZ400 line on
// other MCUs to remove v1 support and save a little space.
#define NEO_KHZ800 0x0000 ///< 800 KHz data transmission
#ifndef __AVR_ATtiny85__
#define NEO_KHZ400 0x0100 ///< 400 KHz data transmission
#endif
// If 400 KHz support is enabled, the third parameter to the constructor
// requires a 16-bit value (in order to select 400 vs 800 KHz speed).
// If only 800 KHz is enabled (as is default on ATtiny), an 8-bit value
// is sufficient to encode pixel color order, saving some space.
#ifdef NEO_KHZ400
typedef uint16_t neoPixelType; ///< 3rd arg to Adafruit_NeoPixel constructor
#else
typedef uint8_t neoPixelType; ///< 3rd arg to Adafruit_NeoPixel constructor
#endif
// These two tables are declared outside the Adafruit_NeoPixel class
// because some boards may require oldschool compilers that don't
// handle the C++11 constexpr keyword.
/* A PROGMEM (flash mem) table containing 8-bit unsigned sine wave (0-255).
Copy & paste this snippet into a Python REPL to regenerate:
import math
for x in range(256):
print("{:3},".format(int((math.sin(x/128.0*math.pi)+1.0)*127.5+0.5))),
if x&15 == 15: print
*/
static const uint8_t PROGMEM _NeoPixelSineTable[256] = {
128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 162, 165, 167, 170,
173, 176, 179, 182, 185, 188, 190, 193, 196, 198, 201, 203, 206, 208, 211,
213, 215, 218, 220, 222, 224, 226, 228, 230, 232, 234, 235, 237, 238, 240,
241, 243, 244, 245, 246, 248, 249, 250, 250, 251, 252, 253, 253, 254, 254,
254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 253, 253, 252, 251,
250, 250, 249, 248, 246, 245, 244, 243, 241, 240, 238, 237, 235, 234, 232,
230, 228, 226, 224, 222, 220, 218, 215, 213, 211, 208, 206, 203, 201, 198,
196, 193, 190, 188, 185, 182, 179, 176, 173, 170, 167, 165, 162, 158, 155,
152, 149, 146, 143, 140, 137, 134, 131, 128, 124, 121, 118, 115, 112, 109,
106, 103, 100, 97, 93, 90, 88, 85, 82, 79, 76, 73, 70, 67, 65,
62, 59, 57, 54, 52, 49, 47, 44, 42, 40, 37, 35, 33, 31, 29,
27, 25, 23, 21, 20, 18, 17, 15, 14, 12, 11, 10, 9, 7, 6,
5, 5, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 2, 2, 3, 4, 5, 5, 6, 7, 9, 10, 11,
12, 14, 15, 17, 18, 20, 21, 23, 25, 27, 29, 31, 33, 35, 37,
40, 42, 44, 47, 49, 52, 54, 57, 59, 62, 65, 67, 70, 73, 76,
79, 82, 85, 88, 90, 93, 97, 100, 103, 106, 109, 112, 115, 118, 121,
124};
/* Similar to above, but for an 8-bit gamma-correction table.
Copy & paste this snippet into a Python REPL to regenerate:
import math
gamma=2.6
for x in range(256):
print("{:3},".format(int(math.pow((x)/255.0,gamma)*255.0+0.5))),
if x&15 == 15: print
*/
static const uint8_t PROGMEM _NeoPixelGammaTable[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3,
3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6,
6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17,
17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
25, 26, 27, 27, 28, 29, 29, 30, 31, 31, 32, 33, 34, 34, 35,
36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
64, 65, 66, 68, 69, 70, 71, 72, 73, 75, 76, 77, 78, 80, 81,
82, 84, 85, 86, 88, 89, 90, 92, 93, 94, 96, 97, 99, 100, 102,
103, 105, 106, 108, 109, 111, 112, 114, 115, 117, 119, 120, 122, 124, 125,
127, 129, 130, 132, 134, 136, 137, 139, 141, 143, 145, 146, 148, 150, 152,
154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182,
184, 186, 188, 191, 193, 195, 197, 199, 202, 204, 206, 209, 211, 213, 215,
218, 220, 223, 225, 227, 230, 232, 235, 237, 240, 242, 245, 247, 250, 252,
255};
/*!
@brief Class that stores state and functions for interacting with
Adafruit NeoPixels and compatible devices.
*/
class Adafruit_NeoPixel {
public:
// Constructor: number of LEDs, pin number, LED type
Adafruit_NeoPixel(uint16_t n, int16_t pin = 6,
neoPixelType type = NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel(void);
~Adafruit_NeoPixel();
void begin(void);
void show(void);
void setPin(int16_t p);
void setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b);
void setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b, uint8_t w);
void setPixelColor(uint16_t n, uint32_t c);
void fill(uint32_t c = 0, uint16_t first = 0, uint16_t count = 0);
void setBrightness(uint8_t);
void clear(void);
void updateLength(uint16_t n);
void updateType(neoPixelType t);
/*!
@brief Check whether a call to show() will start sending data
immediately or will 'block' for a required interval. NeoPixels
require a short quiet time (about 300 microseconds) after the
last bit is received before the data 'latches' and new data can
start being received. Usually one's sketch is implicitly using
this time to generate a new frame of animation...but if it
finishes very quickly, this function could be used to see if
there's some idle time available for some low-priority
concurrent task.
@return 1 or true if show() will start sending immediately, 0 or false
if show() would block (meaning some idle time is available).
*/
bool canShow(void) {
// It's normal and possible for endTime to exceed micros() if the
// 32-bit clock counter has rolled over (about every 70 minutes).
// Since both are uint32_t, a negative delta correctly maps back to
// positive space, and it would seem like the subtraction below would
// suffice. But a problem arises if code invokes show() very
// infrequently...the micros() counter may roll over MULTIPLE times in
// that interval, the delta calculation is no longer correct and the
// next update may stall for a very long time. The check below resets
// the latch counter if a rollover has occurred. This can cause an
// extra delay of up to 300 microseconds in the rare case where a
// show() call happens precisely around the rollover, but that's
// neither likely nor especially harmful, vs. other code that might
// stall for 30+ minutes, or having to document and frequently remind
// and/or provide tech support explaining an unintuitive need for
// show() calls at least once an hour.
uint32_t now = micros();
if (endTime > now) {
endTime = now;
}
return (now - endTime) >= 300L;
}
/*!
@brief Get a pointer directly to the NeoPixel data buffer in RAM.
Pixel data is stored in a device-native format (a la the NEO_*
constants) and is not translated here. Applications that access
this buffer will need to be aware of the specific data format
and handle colors appropriately.
@return Pointer to NeoPixel buffer (uint8_t* array).
@note This is for high-performance applications where calling
setPixelColor() on every single pixel would be too slow (e.g.
POV or light-painting projects). There is no bounds checking
on the array, creating tremendous potential for mayhem if one
writes past the ends of the buffer. Great power, great
responsibility and all that.
*/
uint8_t *getPixels(void) const { return pixels; };
uint8_t getBrightness(void) const;
/*!
@brief Retrieve the pin number used for NeoPixel data output.
@return Arduino pin number (-1 if not set).
*/
int16_t getPin(void) const { return pin; };
/*!
@brief Return the number of pixels in an Adafruit_NeoPixel strip object.
@return Pixel count (0 if not set).
*/
uint16_t numPixels(void) const { return numLEDs; }
uint32_t getPixelColor(uint16_t n) const;
/*!
@brief An 8-bit integer sine wave function, not directly compatible
with standard trigonometric units like radians or degrees.
@param x Input angle, 0-255; 256 would loop back to zero, completing
the circle (equivalent to 360 degrees or 2 pi radians).
One can therefore use an unsigned 8-bit variable and simply
add or subtract, allowing it to overflow/underflow and it
still does the expected contiguous thing.
@return Sine result, 0 to 255, or -128 to +127 if type-converted to
a signed int8_t, but you'll most likely want unsigned as this
output is often used for pixel brightness in animation effects.
*/
static uint8_t sine8(uint8_t x) {
return pgm_read_byte(&_NeoPixelSineTable[x]); // 0-255 in, 0-255 out
}
/*!
@brief An 8-bit gamma-correction function for basic pixel brightness
adjustment. Makes color transitions appear more perceptially
correct.
@param x Input brightness, 0 (minimum or off/black) to 255 (maximum).
@return Gamma-adjusted brightness, can then be passed to one of the
setPixelColor() functions. This uses a fixed gamma correction
exponent of 2.6, which seems reasonably okay for average
NeoPixels in average tasks. If you need finer control you'll
need to provide your own gamma-correction function instead.
*/
static uint8_t gamma8(uint8_t x) {
return pgm_read_byte(&_NeoPixelGammaTable[x]); // 0-255 in, 0-255 out
}
/*!
@brief Convert separate red, green and blue values into a single
"packed" 32-bit RGB color.
@param r Red brightness, 0 to 255.
@param g Green brightness, 0 to 255.
@param b Blue brightness, 0 to 255.
@return 32-bit packed RGB value, which can then be assigned to a
variable for later use or passed to the setPixelColor()
function. Packed RGB format is predictable, regardless of
LED strand color order.
*/
static uint32_t Color(uint8_t r, uint8_t g, uint8_t b) {
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
/*!
@brief Convert separate red, green, blue and white values into a
single "packed" 32-bit WRGB color.
@param r Red brightness, 0 to 255.
@param g Green brightness, 0 to 255.
@param b Blue brightness, 0 to 255.
@param w White brightness, 0 to 255.
@return 32-bit packed WRGB value, which can then be assigned to a
variable for later use or passed to the setPixelColor()
function. Packed WRGB format is predictable, regardless of
LED strand color order.
*/
static uint32_t Color(uint8_t r, uint8_t g, uint8_t b, uint8_t w) {
return ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
static uint32_t ColorHSV(uint16_t hue, uint8_t sat = 255, uint8_t val = 255);
/*!
@brief A gamma-correction function for 32-bit packed RGB or WRGB
colors. Makes color transitions appear more perceptially
correct.
@param x 32-bit packed RGB or WRGB color.
@return Gamma-adjusted packed color, can then be passed in one of the
setPixelColor() functions. Like gamma8(), this uses a fixed
gamma correction exponent of 2.6, which seems reasonably okay
for average NeoPixels in average tasks. If you need finer
control you'll need to provide your own gamma-correction
function instead.
*/
static uint32_t gamma32(uint32_t x);
void rainbow(uint16_t first_hue = 0, int8_t reps = 1,
uint8_t saturation = 255, uint8_t brightness = 255,
bool gammify = true);
private:
#if defined(ARDUINO_ARCH_RP2040)
void rp2040Init(uint8_t pin, bool is800KHz);
void rp2040Show(uint8_t pin, uint8_t *pixels, uint32_t numBytes, bool is800KHz);
#endif
protected:
#ifdef NEO_KHZ400 // If 400 KHz NeoPixel support enabled...
bool is800KHz; ///< true if 800 KHz pixels
#endif
bool begun; ///< true if begin() previously called
uint16_t numLEDs; ///< Number of RGB LEDs in strip
uint16_t numBytes; ///< Size of 'pixels' buffer below
int16_t pin; ///< Output pin number (-1 if not yet set)
uint8_t brightness; ///< Strip brightness 0-255 (stored as +1)
uint8_t *pixels; ///< Holds LED color values (3 or 4 bytes each)
uint8_t rOffset; ///< Red index within each 3- or 4-byte pixel
uint8_t gOffset; ///< Index of green byte
uint8_t bOffset; ///< Index of blue byte
uint8_t wOffset; ///< Index of white (==rOffset if no white)
uint32_t endTime; ///< Latch timing reference
#ifdef __AVR__
volatile uint8_t *port; ///< Output PORT register
uint8_t pinMask; ///< Output PORT bitmask
#endif
#if defined(ARDUINO_ARCH_STM32) || defined(ARDUINO_ARCH_ARDUINO_CORE_STM32)
GPIO_TypeDef *gpioPort; ///< Output GPIO PORT
uint32_t gpioPin; ///< Output GPIO PIN
#endif
#if defined(ARDUINO_ARCH_RP2040)
PIO pio = pio0;
int sm = 0;
bool init = true;
#endif
};
#endif // ADAFRUIT_NEOPIXEL_H

View File

@@ -0,0 +1,13 @@
# Contribution Guidelines
This library is the culmination of the expertise of many members of the open source community who have dedicated their time and hard work. The best way to ask for help or propose a new idea is to [create a new issue](https://github.com/adafruit/Adafruit_NeoPixel/issues/new) while creating a Pull Request with your code changes allows you to share your own innovations with the rest of the community.
The following are some guidelines to observe when creating issues or PRs:
- Be friendly; it is important that we can all enjoy a safe space as we are all working on the same project and it is okay for people to have different ideas
- [Use code blocks](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#code); it helps us help you when we can read your code! On that note also refrain from pasting more than 30 lines of code in a post, instead [create a gist](https://gist.github.com/) if you need to share large snippets
- Use reasonable titles; refrain from using overly long or capitalized titles as they are usually annoying and do little to encourage others to help :smile:
- Be detailed; refrain from mentioning code problems without sharing your source code and always give information regarding your board and version of the library

View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1,157 @@
# Adafruit NeoPixel Library [![Build Status](https://github.com/adafruit/Adafruit_NeoPixel/workflows/Arduino%20Library%20CI/badge.svg)](https://github.com/adafruit/Adafruit_NeoPixel/actions)[![Documentation](https://github.com/adafruit/ci-arduino/blob/master/assets/doxygen_badge.svg)](http://adafruit.github.io/Adafruit_NeoPixel/html/index.html)
Arduino library for controlling single-wire-based LED pixels and strip such as the [Adafruit 60 LED/meter Digital LED strip][strip], the [Adafruit FLORA RGB Smart Pixel][flora], the [Adafruit Breadboard-friendly RGB Smart Pixel][pixel], the [Adafruit NeoPixel Stick][stick], and the [Adafruit NeoPixel Shield][shield].
After downloading, rename folder to 'Adafruit_NeoPixel' and install in Arduino Libraries folder. Restart Arduino IDE, then open File->Sketchbook->Library->Adafruit_NeoPixel->strandtest sketch.
Compatibility notes: Port A is not supported on any AVR processors at this time
[flora]: http://adafruit.com/products/1060
[strip]: http://adafruit.com/products/1138
[pixel]: http://adafruit.com/products/1312
[stick]: http://adafruit.com/products/1426
[shield]: http://adafruit.com/products/1430
---
## Installation
### First Method
![image](https://user-images.githubusercontent.com/36513474/68967967-3e37f480-0803-11ea-91d9-601848c306ee.png)
1. In the Arduino IDE, navigate to Sketch > Include Library > Manage Libraries
1. Then the Library Manager will open and you will find a list of libraries that are already installed or ready for installation.
1. Then search for Neopixel strip using the search bar.
1. Click on the text area and then select the specific version and install it.
### Second Method
1. Navigate to the [Releases page](https://github.com/adafruit/Adafruit_NeoPixel/releases).
1. Download the latest release.
1. Extract the zip file
1. In the Arduino IDE, navigate to Sketch > Include Library > Add .ZIP Library
## Features
- ### Simple to use
Controlling NeoPixels “from scratch” is quite a challenge, so we provide a library letting you focus on the fun and interesting bits.
- ### Give back
The library is free; you dont have to pay for anything. Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit!
- ### Supported Chipsets
We have included code for the following chips - sometimes these break for exciting reasons that we can't control in which case please open an issue!
- AVR ATmega and ATtiny (any 8-bit) - 8 MHz, 12 MHz and 16 MHz
- Teensy 3.x and LC
- Arduino Due
- Arduino 101
- ATSAMD21 (Arduino Zero/M0 and other SAMD21 boards) @ 48 MHz
- ATSAMD51 @ 120 MHz
- Adafruit STM32 Feather @ 120 MHz
- ESP8266 any speed
- ESP32 any speed
- Nordic nRF52 (Adafruit Feather nRF52), nRF51 (micro:bit)
- Infineon XMC1100 BootKit @ 32 MHz
- Infineon XMC1100 2Go @ 32 MHz
- Infineon XMC1300 BootKit @ 32 MHz
- Infineon XMC4700 RelaxKit, XMC4800 RelaxKit, XMC4800 IoT Amazon FreeRTOS Kit @ 144 MHz
Check forks for other architectures not listed here!
- ### GNU Lesser General Public License
Adafruit_NeoPixel is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
## Functions
- begin()
- updateLength()
- updateType()
- show()
- delay_ns()
- setPin()
- setPixelColor()
- fill()
- ColorHSV()
- getPixelColor()
- setBrightness()
- getBrightness()
- clear()
- gamma32()
## Examples
There are many examples implemented in this library. One of the examples is below. You can find other examples [here](https://github.com/adafruit/Adafruit_NeoPixel/tree/master/examples)
### Simple
```Cpp
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h>
#endif
#define PIN 6
#define NUMPIXELS 16
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
#define DELAYVAL 500
void setup() {
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
clock_prescale_set(clock_div_1);
#endif
pixels.begin();
}
void loop() {
pixels.clear();
for(int i=0; i<NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 150, 0));
pixels.show();
delay(DELAYVAL);
}
}
```
## Contributing
If you want to contribute to this project:
- Report bugs and errors
- Ask for enhancements
- Create issues and pull requests
- Tell others about this library
- Contribute new protocols
Please read [CONTRIBUTING.md](https://github.com/adafruit/Adafruit_NeoPixel/blob/master/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
### Roadmap
The PRIME DIRECTIVE is to maintain backward compatibility with existing Arduino sketches -- many are hosted elsewhere and don't track changes here, some are in print and can never be changed!
Please don't reformat code for the sake of reformatting code. The resulting large "visual diff" makes it impossible to untangle actual bug fixes from merely rearranged lines. (Exception for first item in wishlist below.)
Things I'd Like To Do But There's No Official Timeline So Please Don't Count On Any Of This Ever Being Canonical:
- For the show() function (with all the delicate pixel timing stuff), break out each architecture into separate source files rather than the current unmaintainable tangle of #ifdef statements!
- Please don't use updateLength() or updateType() in new code. They should not have been implemented this way (use the C++ 'new' operator with the regular constructor instead) and are only sticking around because of the Prime Directive. setPin() is OK for now though, it's a trick we can use to 'recycle' pixel memory across multiple strips.
- In the M0 and M4 code, use the hardware systick counter for bit timing rather than hand-tweaked NOPs (a temporary kludge at the time because I wasn't reading systick correctly). (As of 1.4.2, systick is used on M4 devices and it appears to be overclock-compatible. Not for M0 yet, which is why this item is still here.)
- As currently written, brightness scaling is still a "destructive" operation -- pixel values are altered in RAM and the original value as set can't be accurately read back, only approximated, which has been confusing and frustrating to users. It was done this way at the time because NeoPixel timing is strict, AVR microcontrollers (all we had at the time) are limited, and assembly language is hard. All the 32-bit architectures should have no problem handling nondestructive brightness scaling -- calculating each byte immediately before it's sent out the wire, maintaining the original set value in RAM -- the work just hasn't been done. There's a fair chance even the AVR code could manage it with some intense focus. (The DotStar library achieves nondestructive brightness scaling because it doesn't have to manage data timing so carefully...every architecture, even ATtiny, just takes whatever cycles it needs for the multiply/shift operations.)
## Credits
This library is written by Phil "Paint Your Dragon" Burgess for Adafruit Industries, with contributions by PJRC, Michael Miller and other members of the open source community.
## License
Adafruit_NeoPixel is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Adafruit_NeoPixel is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.en.html) for more details.
You should have received a copy of the GNU Lesser General Public License along with NeoPixel. If not, see [this](https://www.gnu.org/licenses/)

178
lib/Adafruit_NeoPixel/esp.c Normal file
View File

@@ -0,0 +1,178 @@
// Implements the RMT peripheral on Espressif SoCs
// Copyright (c) 2020 Lucian Copeland for Adafruit Industries
/* Uses code from Espressif RGB LED Strip demo and drivers
* Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if defined(ESP32)
#include <Arduino.h>
#include "driver/rmt.h"
#if defined(ESP_IDF_VERSION)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0)
#define HAS_ESP_IDF_4
#endif
#endif
// This code is adapted from the ESP-IDF v3.4 RMT "led_strip" example, altered
// to work with the Arduino version of the ESP-IDF (3.2)
#define WS2812_T0H_NS (400)
#define WS2812_T0L_NS (850)
#define WS2812_T1H_NS (800)
#define WS2812_T1L_NS (450)
#define WS2811_T0H_NS (500)
#define WS2811_T0L_NS (2000)
#define WS2811_T1H_NS (1200)
#define WS2811_T1L_NS (1300)
static uint32_t t0h_ticks = 0;
static uint32_t t1h_ticks = 0;
static uint32_t t0l_ticks = 0;
static uint32_t t1l_ticks = 0;
// Limit the number of RMT channels available for the Neopixels. Defaults to all
// channels (8 on ESP32, 4 on ESP32-S2 and S3). Redefining this value will free
// any channels with a higher number for other uses, such as IR send-and-recieve
// libraries. Redefine as 1 to restrict Neopixels to only a single channel.
#define ADAFRUIT_RMT_CHANNEL_MAX RMT_CHANNEL_MAX
#define RMT_LL_HW_BASE (&RMT)
bool rmt_reserved_channels[ADAFRUIT_RMT_CHANNEL_MAX];
static void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size,
size_t wanted_num, size_t *translated_size, size_t *item_num)
{
if (src == NULL || dest == NULL) {
*translated_size = 0;
*item_num = 0;
return;
}
const rmt_item32_t bit0 = {{{ t0h_ticks, 1, t0l_ticks, 0 }}}; //Logical 0
const rmt_item32_t bit1 = {{{ t1h_ticks, 1, t1l_ticks, 0 }}}; //Logical 1
size_t size = 0;
size_t num = 0;
uint8_t *psrc = (uint8_t *)src;
rmt_item32_t *pdest = dest;
while (size < src_size && num < wanted_num) {
for (int i = 0; i < 8; i++) {
// MSB first
if (*psrc & (1 << (7 - i))) {
pdest->val = bit1.val;
} else {
pdest->val = bit0.val;
}
num++;
pdest++;
}
size++;
psrc++;
}
*translated_size = size;
*item_num = num;
}
void espShow(uint8_t pin, uint8_t *pixels, uint32_t numBytes, boolean is800KHz) {
// Reserve channel
rmt_channel_t channel = ADAFRUIT_RMT_CHANNEL_MAX;
for (size_t i = 0; i < ADAFRUIT_RMT_CHANNEL_MAX; i++) {
if (!rmt_reserved_channels[i]) {
rmt_reserved_channels[i] = true;
channel = i;
break;
}
}
if (channel == ADAFRUIT_RMT_CHANNEL_MAX) {
// Ran out of channels!
return;
}
#if defined(HAS_ESP_IDF_4)
rmt_config_t config = RMT_DEFAULT_CONFIG_TX(pin, channel);
config.clk_div = 2;
#else
// Match default TX config from ESP-IDF version 3.4
rmt_config_t config = {
.rmt_mode = RMT_MODE_TX,
.channel = channel,
.gpio_num = pin,
.clk_div = 2,
.mem_block_num = 1,
.tx_config = {
.carrier_freq_hz = 38000,
.carrier_level = RMT_CARRIER_LEVEL_HIGH,
.idle_level = RMT_IDLE_LEVEL_LOW,
.carrier_duty_percent = 33,
.carrier_en = false,
.loop_en = false,
.idle_output_en = true,
}
};
#endif
rmt_config(&config);
rmt_driver_install(config.channel, 0, 0);
// Convert NS timings to ticks
uint32_t counter_clk_hz = 0;
#if defined(HAS_ESP_IDF_4)
rmt_get_counter_clock(channel, &counter_clk_hz);
#else
// this emulates the rmt_get_counter_clock() function from ESP-IDF 3.4
if (RMT_LL_HW_BASE->conf_ch[config.channel].conf1.ref_always_on == RMT_BASECLK_REF) {
uint32_t div_cnt = RMT_LL_HW_BASE->conf_ch[config.channel].conf0.div_cnt;
uint32_t div = div_cnt == 0 ? 256 : div_cnt;
counter_clk_hz = REF_CLK_FREQ / (div);
} else {
uint32_t div_cnt = RMT_LL_HW_BASE->conf_ch[config.channel].conf0.div_cnt;
uint32_t div = div_cnt == 0 ? 256 : div_cnt;
counter_clk_hz = APB_CLK_FREQ / (div);
}
#endif
// NS to tick converter
float ratio = (float)counter_clk_hz / 1e9;
if (is800KHz) {
t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS);
t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS);
t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS);
t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS);
} else {
t0h_ticks = (uint32_t)(ratio * WS2811_T0H_NS);
t0l_ticks = (uint32_t)(ratio * WS2811_T0L_NS);
t1h_ticks = (uint32_t)(ratio * WS2811_T1H_NS);
t1l_ticks = (uint32_t)(ratio * WS2811_T1L_NS);
}
// Initialize automatic timing translator
rmt_translator_init(config.channel, ws2812_rmt_adapter);
// Write and wait to finish
rmt_write_sample(config.channel, pixels, (size_t)numBytes, true);
rmt_wait_tx_done(config.channel, pdMS_TO_TICKS(100));
// Free channel again
rmt_driver_uninstall(config.channel);
rmt_reserved_channels[channel] = false;
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
}
#endif

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