60 Commits
test ... v3.6.5

Author SHA1 Message Date
proddy
1487f30c43 Merge remote-tracking branch 'origin/dev' for 3.6.5 2024-03-23 17:56:05 +01:00
Proddy
e00eb8e64f 3.6.4 2023-11-26 20:11:36 +01:00
Proddy
f41bb3671c 3.6.4 2023-11-24 07:36:36 +01:00
Proddy
22c75e6df3 3.6.4 2023-11-24 07:36:29 +01:00
proddy
5ab10b7aa6 fixes #1450 2023-11-22 09:48:09 +01:00
Proddy
ee5fd4d0eb 3.6.3 2023-11-21 14:40:47 +01:00
Proddy
46f35bc67c another patch on 3.6.3 2023-11-21 14:40:32 +01:00
Proddy
ec85a7ec24 3.6.3 (refershed) 2023-11-19 21:24:01 +01:00
Proddy
02f2389587 add workflow_dispatch: 2023-11-18 18:51:12 +01:00
Proddy
7f140021aa quick fix - https://github.com/emsesp/EMS-ESP32/pull/1438 2023-11-18 18:47:28 +01:00
Proddy
6796962c1e 3.6.3 2023-11-18 13:35:20 +01:00
Proddy
df6de21cf4 Merge remote-tracking branch 'origin/dev' 2023-11-18 13:35:04 +01:00
Proddy
df9f75a5c9 updated yarn for 3.6.2 2023-10-01 17:42:54 +02:00
Proddy
7bd8710eb6 3.6.2 2023-10-01 17:40:09 +02:00
Proddy
32f2c6d341 Merge remote-tracking branch 'origin/dev' 2023-10-01 17:40:02 +02:00
Proddy
86919c1684 Merge branch 'origin/dev' 2023-09-09 14:12:07 +02:00
Proddy
86e29515e7 build s3 2023-08-15 18:44:24 +02:00
Proddy
46eb4185d7 update with 3.6.0 2023-08-13 14:37:13 +02:00
Proddy
8da6761a48 Merge branch 'dev' 2023-08-13 14:32:41 +02:00
Proddy
9233f0dfcc v3.5.1 - merge with patch 2023-03-11 16:06:05 +01:00
Proddy
292f743b14 Update bug_report.md 2023-02-19 11:18:08 +01:00
Proddy
dd6dfffd57 Delete questions---troubleshooting.md 2023-02-19 10:49:52 +01:00
Proddy
ec705a5307 Delete feature_request.md 2023-02-19 10:49:45 +01:00
Proddy
f45f071710 Create config.yml 2023-02-19 10:49:32 +01:00
Proddy
f3858546de Merge branch 'origin/dev' 2023-02-06 21:58:27 +01:00
Proddy
d0ac0b7804 update with version on dev 2022-10-30 16:58:37 +01:00
Proddy
d8284ec09f Merge pull request #705 from MichaelDvP/main
v3.4.4 Fix for new installations with filesystem not initializing
2022-10-29 10:46:41 +02:00
MichaelDvP
6e982acde8 v3.4.4 Fix for new installations with filesystem not initializing 2022-10-28 10:50:51 +02:00
Proddy
8c94ce99b2 quick fix for filesystem initialization 2022-10-08 09:23:00 +02:00
proddy
fc057d18c9 Merge remote-tracking branch 'origin/dev' 2022-09-18 14:33:23 +02:00
Proddy
18e9b99413 Merge remote-tracking branch 'origin/dev' into main 2022-05-29 16:16:38 +02:00
Proddy
a47e0e8266 update for 3.4.0 2022-05-23 21:20:45 +02:00
Proddy
f412ddc716 Merge remote-tracking branch 'origin/dev' into main 2022-05-23 21:20:36 +02:00
proddy
29110e96e5 Merge remote-tracking branch 'origin/dev' 2022-01-20 10:51:40 +01:00
proddy
b65866217a 3.4.0 2021-11-28 23:03:28 +01:00
proddy
611e3b1243 Merge remote-tracking branch 'origin/dev' 2021-11-28 23:03:15 +01:00
proddy
2ca0a0c634 v3.2.1 merged from dev 2021-08-08 14:46:14 +02:00
proddy
7eb1f061b7 Merge remote-tracking branch 'origin/dev' for 3.2.0 release 2021-08-06 12:06:08 +02:00
proddy
50459a23fe force v16 of nodejs 2021-06-26 11:13:07 +02:00
proddy
5bf53c3389 3.1.1 2021-06-26 11:03:03 +02:00
proddy
4b7aa95be3 Merge remote-tracking branch 'origin/dev' 2021-06-26 11:02:55 +02:00
Proddy
70943f5758 Update pre_release.yml 2021-05-16 15:52:09 +02:00
Proddy
3bc280b817 Delete check_code.yml 2021-05-16 15:51:56 +02:00
Proddy
62b15a5319 Update pre_release.yml 2021-05-16 15:35:06 +02:00
Proddy
8dd18802d6 Update tagged_release.yml 2021-05-16 15:34:45 +02:00
proddy
57a516a83a updated README and images 2021-05-09 15:13:16 +02:00
proddy
a57fdaa4b3 Merge remote-tracking branch 'origin/dev' into main 2021-05-04 12:21:51 +02:00
proddy
4841e42286 Merge remote-tracking branch 'origin/dev' into main 2021-03-30 16:35:18 +02:00
proddy
8c2d2b06ed cleaned up old changelog 2021-03-18 20:59:09 +01:00
proddy
38c8b1b7f0 3.0.0 2021-03-18 20:58:21 +01:00
proddy
6fb5933a02 Merge remote-tracking branch 'origin/dev' into main 2021-03-18 20:58:12 +01:00
proddy
c0944433be remove workspace.code-workspace 2021-03-16 17:41:42 +01:00
Proddy
478e6362c9 Merge pull request #27 from FauthD:main
Add global names to Dallas sensors to avoid ugly <unknown> and other …
2021-03-16 17:39:00 +01:00
fauthd
4d6354db78 Add stuff to gitignore, add vscode workspace 2021-03-16 16:47:09 +01:00
fauthd
beab0f0c77 Add global names to Dallas sensors to avoid ugly <unknown> and other issues in HA 2021-03-16 16:00:23 +01:00
Proddy
c17749bd22 Update README.md 2021-03-14 23:43:33 +01:00
proddy
2bad769c5c build: include assets 2021-03-14 21:20:51 +01:00
proddy
8ad89ca64b move repo 2021-03-14 21:05:15 +01:00
proddy
9244d8daec Semantic Commit Messages 2021-03-14 18:10:57 +01:00
proddy
02d01334b2 update new build 2021-03-14 17:37:18 +01:00
155 changed files with 3394 additions and 4375 deletions

View File

@@ -1,6 +1,7 @@
name: 'github-releases-to-discord' name: 'github-releases-to-discord'
on: on:
workflow_dispatch:
release: release:
types: [published] types: [published]

View File

@@ -13,12 +13,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: '20' node-version: '18'
- name: Get EMS-ESP source code and version - name: Get EMS-ESP source code and version
id: build_info id: build_info

View File

@@ -12,12 +12,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: '20' node-version: '18'
- name: Install PlatformIO - name: Install PlatformIO
run: | run: |

View File

@@ -13,12 +13,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: '20' node-version: '18'
- name: Get EMS-ESP source code and version - name: Get EMS-ESP source code and version
id: build_info id: build_info

7
.gitignore vendored
View File

@@ -1,8 +1,5 @@
# vscode # vscode
.vscode/c_cpp_properties.json .vscode/*
.vscode/extensions.json
.vscode/launch.json
#.vscode/settings.json
# c++ compiling # c++ compiling
.clang_complete .clang_complete
@@ -37,7 +34,7 @@ stats.html
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
yarn.lock yarn.lock
analyse.html interface/analyse.html
interface/vite.config.ts.timestamp* interface/vite.config.ts.timestamp*
# scripts # scripts

91
.vscode/settings.json vendored
View File

@@ -1,91 +0,0 @@
{
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"eslint.nodePath": "interface/.yarn/sdks",
"eslint.workingDirectories": ["interface"],
"prettier.prettierPath": "",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.associations": {
"*.tsx": "typescriptreact",
"*.tcc": "cpp",
"optional": "cpp",
"istream": "cpp",
"ostream": "cpp",
"ratio": "cpp",
"system_error": "cpp",
"array": "cpp",
"functional": "cpp",
"regex": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"string": "cpp",
"string_view": "cpp",
"atomic": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"condition_variable": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"list": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"iterator": "cpp",
"map": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"random": "cpp",
"set": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp"
},
"todo-tree.filtering.excludeGlobs": [
"**/vendor/**",
"**/node_modules/**",
"**/dist/**",
"**/bower_components/**",
"**/build/**",
"**/.vscode/**",
"**/.github/**",
"**/_output/**",
"**/*.min.*",
"**/*.map",
"**/ArduinoJson/**"
],
"cSpell.enableFiletypes": [
"!cpp",
"!typescript"
]
}

View File

@@ -59,7 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## **IMPORTANT! BREAKING CHANGES** ## **IMPORTANT! BREAKING CHANGES**
Writeable Text entities have moved from type `sensor` to `text` in Home Assistant to make them also editable within an HA dashboard. Examples are `datetime`, `holidays`, `switchtime`, `vacations`, `maintenancedate`... You will need to manually remove any old discovery topics from your MQTT broker using an application like MQTT Explorer. Writeable Text entities have moved from type `sensor` to `text` in Home Assistant to make them also editable within an HA dashboard. Examples are `datetime`, `holidays`, `switchtime`, `vacations`, `maintenancedate`. You will need to manually remove any old discovery topics from your MQTT broker using an application like MQTT Explorer.
## Added ## Added

View File

@@ -1,23 +1,11 @@
# Changelog # Changelog
## [3.7.0] ## [3.x]
## **IMPORTANT! BREAKING CHANGES** ## **IMPORTANT! BREAKING CHANGES**
- new device WATER shows dhw entities from MM100 and SM100 in dhw setting
## Added ## Added
- some more entities for dhw with SM100 module
- thermostat second dhw circuit [#1634](https://github.com/emsesp/EMS-ESP32/issues/1634)
- remote thermostat emulation for RC100H, RC200 and FB10 [#1287](https://github.com/emsesp/EMS-ESP32/discussions/1287), [#1602](https://github.com/emsesp/EMS-ESP32/discussions/1602), [#1551](https://github.com/emsesp/EMS-ESP32/discussions/1551)
- heatpump dhw stop temperatures [#1624](https://github.com/emsesp/EMS-ESP32/issues/1624)
## Fixed ## Fixed
## Changed ## Changed
- use flag for BC400 compatible thermostats, manage different mode settings
- use factory partition for 16M flash
- store digital out states to nvs
- Refresh UI - moving settings to one location [#1665](https://github.com/emsesp/EMS-ESP32/issues/1665)

View File

@@ -42,7 +42,7 @@ DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DAR
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST -D__linux__ -DEMC_RX_BUFFER_SIZE=1500 DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST -D__linux__ -DEMC_RX_BUFFER_SIZE=1500
DEFINES += $(ARGS) DEFINES += $(ARGS)
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.0-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\" DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.6.5-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
#---------------------------------------------------------------------- #----------------------------------------------------------------------
# Sources & Files # Sources & Files

View File

@@ -1,9 +1,8 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x005000, nvs, data, nvs, 0x9000, 0x005000,
otadata, data, ota, , 0x002000, otadata, data, ota, , 0x002000,
boot, app, factory, , 0x280000, app0, app, ota_0, , 0x5D0000,
app0, app, ota_0, , 0x590000, app1, app, ota_1, , 0x5D0000,
app1, app, ota_1, , 0x590000,
nvs1, data, nvs, , 0x040000, nvs1, data, nvs, , 0x040000,
spiffs, data, spiffs, , 0x200000, spiffs, data, spiffs, , 0x400000,
coredump, data, coredump,, 0x010000, coredump, data, coredump,, 0x010000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x005000
3 otadata data ota 0x002000
4 boot app0 app factory ota_0 0x280000 0x5D0000
5 app0 app1 app ota_0 ota_1 0x590000 0x5D0000
app1 app ota_1 0x590000
6 nvs1 data nvs 0x040000
7 spiffs data spiffs 0x200000 0x400000
8 coredump data coredump 0x010000

View File

@@ -0,0 +1,9 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x005000,
otadata, data, ota, , 0x002000,
boot, app, factory, , 0x280000,
app0, app, ota_0, , 0x590000,
app1, app, ota_1, , 0x590000,
nvs1, data, nvs, , 0x040000,
spiffs, data, spiffs, , 0x200000,
coredump, data, coredump,, 0x010000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x005000
3 otadata data ota 0x002000
4 boot app factory 0x280000
5 app0 app ota_0 0x590000
6 app1 app ota_1 0x590000
7 nvs1 data nvs 0x040000
8 spiffs data spiffs 0x200000
9 coredump data coredump 0x010000

View File

@@ -1,6 +1,6 @@
{ {
"name": "EMS-ESP", "name": "EMS-ESP",
"version": "3.7", "version": "3.6.5",
"description": "build EMS-ESP WebUI", "description": "build EMS-ESP WebUI",
"homepage": "https://emsesp.github.io/docs", "homepage": "https://emsesp.github.io/docs",
"author": "proddy", "author": "proddy",
@@ -32,7 +32,7 @@
"@types/imagemin": "^8.0.5", "@types/imagemin": "^8.0.5",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.30", "@types/node": "^20.11.30",
"@types/react": "^18.2.72", "@types/react": "^18.2.69",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"alova": "^2.18.0", "alova": "^2.18.0",
@@ -54,8 +54,8 @@
"devDependencies": { "devDependencies": {
"@preact/compat": "^17.1.2", "@preact/compat": "^17.1.2",
"@preact/preset-vite": "^2.8.2", "@preact/preset-vite": "^2.8.2",
"@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.4.0", "@typescript-eslint/parser": "^7.3.1",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@@ -70,7 +70,7 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"terser": "^5.29.2", "terser": "^5.29.2",
"vite": "^5.2.6", "vite": "^5.2.4",
"vite-plugin-imagemin": "^0.6.1", "vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^4.3.2" "vite-tsconfig-paths": "^4.3.2"
}, },

View File

@@ -28,7 +28,7 @@ const App: FC = () => {
<CustomTheme> <CustomTheme>
<AppRouting /> <AppRouting />
<ToastContainer <ToastContainer
position="bottom-right" position="bottom-left"
autoClose={3000} autoClose={3000}
hideProgressBar={false} hideProgressBar={false}
newestOnTop={false} newestOnTop={false}

View File

@@ -1,55 +1,64 @@
import { useContext, type FC } from 'react';
import { Navigate, Routes, Route } from 'react-router-dom'; import { Navigate, Routes, Route } from 'react-router-dom';
import Dashboard from './project/Dashboard';
import Help from './project/Help'; import Help from './project/Help';
import { Layout } from 'components'; import Settings from './project/Settings';
import { AuthenticatedContext } from 'contexts/authentication'; import type { FC } from 'react';
import Settings from 'framework/Settings';
import { Layout, RequireAdmin } from 'components';
import AccessPoint from 'framework/ap/AccessPoint'; import AccessPoint from 'framework/ap/AccessPoint';
import Mqtt from 'framework/mqtt/Mqtt'; import Mqtt from 'framework/mqtt/Mqtt';
import Network from 'framework/network/Network'; import NetworkConnection from 'framework/network/NetworkConnection';
import NetworkTime from 'framework/ntp/NetworkTime'; import NetworkTime from 'framework/ntp/NetworkTime';
import OTASettings from 'framework/ota/OTASettings';
import Security from 'framework/security/Security'; import Security from 'framework/security/Security';
import ESPSystemStatus from 'framework/system/ESPSystemStatus';
import System from 'framework/system/System'; import System from 'framework/system/System';
import UploadDownload from 'framework/system/UploadDownload';
import ApplicationSettings from 'project/ApplicationSettings';
import CustomEntities from 'project/CustomEntities';
import Customization from 'project/Customization';
import Devices from 'project/Devices';
import Scheduler from 'project/Scheduler';
import Sensors from 'project/Sensors';
const AuthenticatedRouting: FC = () => { const AuthenticatedRouting: FC = () => (
const { me } = useContext(AuthenticatedContext); // const location = useLocation();
return ( // const navigate = useNavigate();
<Layout> // const handleApiResponseError = useCallback(
<Routes> // (error: AxiosError) => {
<Route path="/devices/*" element={<Devices />} /> // if (error.response && error.response.status === 401) {
<Route path="/sensors/*" element={<Sensors />} /> // AuthenticationApi.storeLoginRedirect(location);
<Route path="/system/*" element={<System />} /> // navigate('/unauthorized');
<Route path="/help/*" element={<Help />} /> // }
<Route path="/*" element={<Navigate to="/" />} /> // return Promise.reject(error);
{me.admin && ( // },
<> // [location, navigate]
<Route path="/customizations/*" element={<Customization />} /> // );
<Route path="/scheduler/*" element={<Scheduler />} /> // useEffect(() => {
<Route path="/customentities/*" element={<CustomEntities />} /> // const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
<Route path="/settings/*" element={<Settings />} /> // return () => AXIOS.interceptors.response.eject(axiosHandlerId);
<Route path="/settings/network/*" element={<Network />} /> // }, [handleApiResponseError]);
<Route path="/settings/ems-esp/*" element={<ApplicationSettings />} />
<Route path="/settings/ap/*" element={<AccessPoint />} /> <Layout>
<Route path="/settings/ntp/*" element={<NetworkTime />} /> <Routes>
<Route path="/settings/mqtt/*" element={<Mqtt />} /> <Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/settings/ota/*" element={<OTASettings />} /> <Route
<Route path="/settings/security/*" element={<Security />} /> path="/settings/*"
<Route path="/settings/espsystemstatus/*" element={<ESPSystemStatus />} /> element={
<Route path="/settings/upload/*" element={<UploadDownload />} /> <RequireAdmin>
</> <Settings />
)} </RequireAdmin>
</Routes> }
</Layout> />
); <Route path="/help/*" element={<Help />} />
};
<Route path="/network/*" element={<NetworkConnection />} />
<Route path="/ap/*" element={<AccessPoint />} />
<Route path="/ntp/*" element={<NetworkTime />} />
<Route path="/mqtt/*" element={<Mqtt />} />
<Route
path="/security/*"
element={
<RequireAdmin>
<Security />
</RequireAdmin>
}
/>
<Route path="/system/*" element={<System />} />
<Route path="/*" element={<Navigate to="/" />} />
</Routes>
</Layout>
);
export default AuthenticatedRouting; export default AuthenticatedRouting;

View File

@@ -32,7 +32,7 @@ export function fetchLoginRedirect(): Partial<Path> {
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH); const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
clearLoginRedirect(); clearLoginRedirect();
return { return {
pathname: signInPathname || `/devices`, pathname: signInPathname || `/dashboard`,
search: (signInPathname && signInSearch) || undefined search: (signInPathname && signInSearch) || undefined
}; };
} }

View File

@@ -1,7 +1,6 @@
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
import type { MqttSettingsType, MqttStatusType } from 'types'; import type { MqttSettings, MqttStatus } from 'types';
export const readMqttStatus = () => alovaInstance.Get<MqttStatusType>('/rest/mqttStatus'); export const readMqttStatus = () => alovaInstance.Get<MqttStatus>('/rest/mqttStatus');
export const readMqttSettings = () => alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings'); export const readMqttSettings = () => alovaInstance.Get<MqttSettings>('/rest/mqttSettings');
export const updateMqttSettings = (data: MqttSettingsType) => export const updateMqttSettings = (data: MqttSettings) => alovaInstance.Post<MqttSettings>('/rest/mqttSettings', data);
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);

View File

@@ -1,10 +1,7 @@
import { alovaInstance, alovaInstanceGH } from './endpoints'; import { alovaInstance, alovaInstanceGH } from './endpoints';
import type { OTASettings, SystemStatus, LogSettings, ESPSystemStatus } from 'types'; import type { OTASettings, SystemStatus, LogSettings } from 'types';
// ESPSystemStatus - also used to ping in Restart monitor for pinging // SystemStatus - also used to ping in Restart monitor for pinging
export const readESPSystemStatus = () => alovaInstance.Get<ESPSystemStatus>('/rest/ESPSystemStatus');
// SystemStatus
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus'); export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
// commands // commands

View File

@@ -4,7 +4,8 @@ import type { FC } from 'react';
import type { RequiredChildrenProps } from 'utils'; import type { RequiredChildrenProps } from 'utils';
interface SectionContentProps extends RequiredChildrenProps { interface SectionContentProps extends RequiredChildrenProps {
title?: string; title: string;
titleGutter?: boolean;
id?: string; id?: string;
} }
@@ -12,9 +13,7 @@ const SectionContent: FC<SectionContentProps> = (props) => {
const { children, title, id } = props; const { children, title, id } = props;
return ( return (
<Paper id={id} sx={{ p: 2, m: 2 }}> <Paper id={id} sx={{ p: 2, m: 2 }}>
{title && ( <Divider sx={{ pb: 2, borderColor: 'primary.main', fontSize: 20, color: 'primary.main' }}>{title}</Divider>
<Divider sx={{ pb: 2, borderColor: 'primary.main', fontSize: 20, color: 'primary.main' }}>{title}</Divider>
)}
{children} {children}
</Paper> </Paper>
); );

View File

@@ -1,5 +1,6 @@
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import { AppBar, IconButton, Toolbar, Typography } from '@mui/material'; import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
import LayoutAuthMenu from './LayoutAuthMenu';
import type { FC } from 'react'; import type { FC } from 'react';
export const DRAWER_WIDTH = 210; export const DRAWER_WIDTH = 210;
@@ -26,6 +27,8 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => (
<Typography variant="h6" noWrap component="div"> <Typography variant="h6" noWrap component="div">
{title} {title}
</Typography> </Typography>
<Box flexGrow={1} />
<LayoutAuthMenu />
</Toolbar> </Toolbar>
</AppBar> </AppBar>
); );

View File

@@ -0,0 +1,165 @@
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import PersonIcon from '@mui/icons-material/Person';
import {
Box,
Button,
Divider,
IconButton,
Popover,
Typography,
Avatar,
styled,
MenuItem,
TextField
} from '@mui/material';
import { useState, useContext } from 'react';
import type { TypographyProps } from '@mui/material';
import type { Locales } from 'i18n/i18n-types';
import type { FC, ChangeEventHandler } from 'react';
import { AuthenticatedContext } from 'contexts/authentication';
import DEflag from 'i18n/DE.svg';
import FRflag from 'i18n/FR.svg';
import GBflag from 'i18n/GB.svg';
import ITflag from 'i18n/IT.svg';
import NLflag from 'i18n/NL.svg';
import NOflag from 'i18n/NO.svg';
import PLflag from 'i18n/PL.svg';
import SKflag from 'i18n/SK.svg';
import SVflag from 'i18n/SV.svg';
import TRflag from 'i18n/TR.svg';
import { I18nContext } from 'i18n/i18n-react';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
const ItemTypography = styled(Typography)<TypographyProps>({
maxWidth: '250px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
});
const LayoutAuthMenu: FC = () => {
const { me, signOut } = useContext(AuthenticatedContext);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
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);
};
const open = Boolean(anchorEl);
const id = anchorEl ? 'app-menu-popover' : undefined;
return (
<>
<TextField
name="locale"
InputProps={{ style: { fontSize: 10 } }}
variant="outlined"
value={locale}
onChange={onLocaleSelected}
size="small"
select
>
<MenuItem key="de" value="de">
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;DE
</MenuItem>
<MenuItem key="en" value="en">
<img src={GBflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;EN
</MenuItem>
<MenuItem key="fr" value="fr">
<img src={FRflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;FR
</MenuItem>
<MenuItem key="it" value="it">
<img src={ITflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;IT
</MenuItem>
<MenuItem key="nl" value="nl">
<img src={NLflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NL
</MenuItem>
<MenuItem key="no" value="no">
<img src={NOflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NO
</MenuItem>
<MenuItem key="pl" value="pl">
<img src={PLflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;PL
</MenuItem>
<MenuItem key="sk" value="sk">
<img src={SKflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SK
</MenuItem>
<MenuItem key="sv" value="sv">
<img src={SVflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SV
</MenuItem>
<MenuItem key="tr" value="tr">
<img src={TRflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;TR
</MenuItem>
</TextField>
<IconButton
id="open-auth-menu"
sx={{ ml: 1, padding: 0 }}
aria-describedby={id}
color="inherit"
onClick={handleClick}
>
<AccountCircleIcon />
</IconButton>
<Popover
id="app-menu-popover"
sx={{ mt: 1 }}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
>
<Box display="flex" flexDirection="row" alignItems="center" p={2}>
<Avatar sx={{ width: 80, height: 80 }}>
<PersonIcon fontSize="large" />
</Avatar>
<Box pl={2}>
<ItemTypography variant="h6">{me.username}</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)}>
{LL.SIGN_OUT()}
</Button>
</Box>
</Popover>
</>
);
};
export default LayoutAuthMenu;

View File

@@ -1,8 +1,6 @@
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material'; import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
import { DRAWER_WIDTH } from './Layout'; import { DRAWER_WIDTH } from './Layout';
import LayoutMenu from './LayoutMenu'; import LayoutMenu from './LayoutMenu';
import type { FC } from 'react'; import type { FC } from 'react';
import { PROJECT_NAME } from 'api/env'; import { PROJECT_NAME } from 'api/env';

View File

@@ -1,254 +1,53 @@
import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import AssessmentIcon from '@mui/icons-material/Assessment';
import CategoryIcon from '@mui/icons-material/Category'; import DashboardIcon from '@mui/icons-material/Dashboard';
import ConstructionIcon from '@mui/icons-material/Construction'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; import InfoIcon from '@mui/icons-material/Info';
import LiveHelpIcon from '@mui/icons-material/LiveHelp'; import LockIcon from '@mui/icons-material/Lock';
import MoreTimeIcon from '@mui/icons-material/MoreTime';
import PersonIcon from '@mui/icons-material/Person';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import SensorsIcon from '@mui/icons-material/Sensors';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TuneIcon from '@mui/icons-material/Tune';
import { Divider, List } from '@mui/material';
import { useContext } from 'react';
import type { FC } from 'react';
import {
Divider,
List,
Box,
Button,
Popover,
Avatar,
MenuItem,
TextField,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText
} from '@mui/material';
import { useContext, useState } from 'react';
import type { Locales } from 'i18n/i18n-types';
import type { FC, ChangeEventHandler } from 'react';
import LayoutMenuItem from 'components/layout/LayoutMenuItem'; import LayoutMenuItem from 'components/layout/LayoutMenuItem';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import DEflag from 'i18n/DE.svg'; import { useI18nContext } from 'i18n/i18n-react';
import FRflag from 'i18n/FR.svg';
import GBflag from 'i18n/GB.svg';
import ITflag from 'i18n/IT.svg';
import NLflag from 'i18n/NL.svg';
import NOflag from 'i18n/NO.svg';
import PLflag from 'i18n/PL.svg';
import SKflag from 'i18n/SK.svg';
import SVflag from 'i18n/SV.svg';
import TRflag from 'i18n/TR.svg';
import { I18nContext } from 'i18n/i18n-react';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
const LayoutMenu: FC = () => { const LayoutMenu: FC = () => {
const { me, signOut } = useContext(AuthenticatedContext); const authenticatedContext = useContext(AuthenticatedContext);
const { locale, LL, setLocale } = useContext(I18nContext); const { LL } = useI18nContext();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const id = anchorEl ? 'app-menu-popover' : undefined;
const [menuOpen, setMenuOpen] = useState(true);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
const handleClick = (event: any) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return ( return (
<> <>
<List component="nav"> <List disablePadding component="nav">
<LayoutMenuItem icon={CategoryIcon} label={LL.DEVICES()} to={`/devices`} /> <LayoutMenuItem icon={DashboardIcon} label={LL.DASHBOARD()} to={`/dashboard`} />
<LayoutMenuItem icon={SensorsIcon} label={LL.SENSORS()} to={`/sensors`} /> <LayoutMenuItem
icon={TuneIcon}
label={LL.SETTINGS_OF('')}
to={`/settings`}
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={InfoIcon} label={LL.HELP_OF('')} to={`/help`} />
<Divider /> <Divider />
<Box
sx={{
bgcolor: menuOpen ? 'rgba(71, 98, 130, 0.2)' : null,
pb: menuOpen ? 2 : 0
}}
>
<ListItemButton
alignItems="flex-start"
onClick={() => setMenuOpen(!menuOpen)}
sx={{
pt: 2.5,
pb: menuOpen ? 0 : 2.5,
'&:hover, &:focus': { '& svg': { opacity: 1 } }
}}
>
<ListItemText
// TODO: translate
primary="Customize"
primaryTypographyProps={{
fontWeight: '600',
mb: '2px',
color: 'lightblue'
}}
// TODO: translate
secondary="Customizations, Scheduler and Custom Entities"
secondaryTypographyProps={{
noWrap: true,
fontSize: 12,
color: menuOpen ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0.5)'
}}
sx={{ my: 0 }}
/>
<KeyboardArrowDown
sx={{
mr: -1,
opacity: 0,
transform: menuOpen ? 'rotate(-180deg)' : 'rotate(0)',
transition: '0.2s'
}}
/>
</ListItemButton>
{menuOpen && (
<>
<LayoutMenuItem
icon={ConstructionIcon}
label={LL.CUSTOMIZATIONS()}
disabled={!me.admin}
to={`/customizations`}
/>
<LayoutMenuItem icon={MoreTimeIcon} label={LL.SCHEDULER()} disabled={!me.admin} to={`/scheduler`} />
<LayoutMenuItem
icon={PlaylistAddIcon}
label={LL.CUSTOM_ENTITIES(0)}
disabled={!me.admin}
to={`/customentities`}
/>
</>
)}
</Box>
</List> </List>
<List disablePadding component="nav">
<List style={{ marginTop: `auto` }}> <LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
<LayoutMenuItem icon={AssessmentIcon} label={LL.SYSTEM(0)} to="/system" /> <LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
<LayoutMenuItem icon={SettingsIcon} label={LL.SETTINGS(0)} disabled={!me.admin} to="/settings" /> <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP_OF('')} to={`/help`} /> <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />
<LayoutMenuItem
icon={LockIcon}
label={LL.SECURITY(0)}
to="/security"
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={SettingsIcon} label={LL.SYSTEM(0)} to="/system" />
</List> </List>
<Divider />
<List>
<ListItem disablePadding onClick={handleClick}>
<ListItemButton>
<ListItemIcon>
<AccountCircleIcon />
</ListItemIcon>
<ListItemText>{me.username}</ListItemText>
</ListItemButton>
</ListItem>
</List>
<Popover
id={id}
sx={{ mt: 1 }}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
>
<Box
p={2}
sx={{
borderRadius: 2,
border: '2px solid grey'
}}
>
<List>
<ListItem disablePadding>
<Avatar sx={{ bgcolor: '#b1395f', color: 'white' }}>
<PersonIcon />
</Avatar>
<ListItemText
sx={{ pl: 2 }}
primary={me.username}
secondary={(me.admin ? LL.ADMIN() : LL.GUEST()) + ' Account'}
/>
</ListItem>
</List>
<Box p={2}>
<TextField
name="locale"
InputProps={{ style: { fontSize: 10 } }}
variant="outlined"
value={locale}
onChange={onLocaleSelected}
size="small"
select
>
<MenuItem key="de" value="de">
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;DE
</MenuItem>
<MenuItem key="en" value="en">
<img src={GBflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;EN
</MenuItem>
<MenuItem key="fr" value="fr">
<img src={FRflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;FR
</MenuItem>
<MenuItem key="it" value="it">
<img src={ITflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;IT
</MenuItem>
<MenuItem key="nl" value="nl">
<img src={NLflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NL
</MenuItem>
<MenuItem key="no" value="no">
<img src={NOflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NO
</MenuItem>
<MenuItem key="pl" value="pl">
<img src={PLflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;PL
</MenuItem>
<MenuItem key="sk" value="sk">
<img src={SKflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SK
</MenuItem>
<MenuItem key="sv" value="sv">
<img src={SVflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SV
</MenuItem>
<MenuItem key="tr" value="tr">
<img src={TRflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;TR
</MenuItem>
</TextField>
</Box>
<Box>
<Button variant="outlined" fullWidth color="primary" onClick={() => signOut(true)}>
{LL.SIGN_OUT()}
</Button>
</Box>
</Box>
</Popover>
</> </>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; import { ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import type { SvgIconProps } from '@mui/material'; import type { SvgIconProps } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
@@ -18,12 +18,14 @@ const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabl
const selected = routeMatches(to, pathname); const selected = routeMatches(to, pathname);
return ( return (
<ListItemButton component={Link} to={to} disabled={disabled} selected={selected}> <ListItem disablePadding>
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}> <ListItemButton component={Link} to={to} disabled={disabled} selected={selected}>
<Icon /> <ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
</ListItemIcon> <Icon />
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>{label}</ListItemText> </ListItemIcon>
</ListItemButton> <ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>{label}</ListItemText>
</ListItemButton>
</ListItem>
); );
}; };

View File

@@ -1,52 +0,0 @@
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Link } from 'react-router-dom';
import type { SvgIconProps } from '@mui/material';
import type { FC } from 'react';
interface ListMenuItemProps {
icon: React.ComponentType<SvgIconProps>;
bgcolor?: string;
label: string;
text: string;
to?: string;
disabled?: boolean;
}
function RenderIcon({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) {
return (
<>
<ListItemAvatar>
<Avatar sx={{ bgcolor, color: 'white' }}>
<Icon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={label} secondary={text} />
</>
);
}
const LayoutMenuItem: FC<ListMenuItemProps> = ({ icon, bgcolor, label, text, to, disabled }) => (
<>
{to && !disabled ? (
<ListItem
disablePadding
secondaryAction={
<ListItemIcon style={{ justifyContent: 'right', color: 'lightblue', verticalAlign: 'middle' }}>
<NavigateNextIcon />
</ListItemIcon>
}
>
<ListItemButton component={Link} to={to}>
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} to="" />
</ListItemButton>
</ListItem>
) : (
<ListItem>
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} to="" />
</ListItem>
)}
</>
);
export default LayoutMenuItem;

View File

@@ -1,8 +1,12 @@
import { useMatch, useResolvedPath } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
export const useRouterTab = () => { export const useRouterTab = () => {
const routerTabPathMatch = useMatch(useResolvedPath(':tab').pathname); const loc = useLocation().pathname;
const routerTab = routerTabPathMatch?.params?.tab || false; const routerTab = loc.substring(0, loc.lastIndexOf('/')) ? loc : false;
// const routerTabPath = useResolvedPath(':tab');
// const routerTabPathMatch = useMatch(routerTabPath.pathname);
// const routerTab = routerTabPathMatch?.params?.tab || false;
return { routerTab } as const; return { routerTab } as const;
}; };

View File

@@ -63,7 +63,7 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, pr
<Box <Box
{...getRootProps({ {...getRootProps({
sx: { sx: {
py: 4, py: 8,
px: 2, px: 2,
borderWidth: 2, borderWidth: 2,
borderRadius: 2, borderRadius: 2,

View File

@@ -1,250 +0,0 @@
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel';
import CastIcon from '@mui/icons-material/Cast';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import LockIcon from '@mui/icons-material/Lock';
import MemoryIcon from '@mui/icons-material/Memory';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TuneIcon from '@mui/icons-material/Tune';
import { List, Button, Dialog, DialogActions, DialogContent, DialogTitle, Box } from '@mui/material';
import { useRequest } from 'alova';
import { useState, type FC } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from './system/RestartMonitor';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system';
import { ButtonRow, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { useI18nContext } from 'i18n/i18n-react';
const Settings: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS(0));
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false);
const [restarting, setRestarting] = useState<boolean>();
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
immediate: false
});
const { send: partitionCommand } = useRequest(SystemApi.partition(), {
immediate: false
});
const restart = async () => {
setProcessing(true);
await restartCommand()
.then(() => {
setRestarting(true);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmRestart(false);
setProcessing(false);
});
};
const factoryReset = async () => {
setProcessing(true);
await factoryResetCommand()
.then(() => {
setRestarting(true);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmFactoryReset(false);
setProcessing(false);
});
};
const partition = async () => {
setProcessing(true);
await partitionCommand()
.then(() => {
setRestarting(true);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmRestart(false);
setProcessing(false);
});
};
const renderRestartDialog = () => (
<Dialog sx={dialogStyle} open={confirmRestart} onClose={() => setConfirmRestart(false)}>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmRestart(false)}
disabled={processing}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={restart}
disabled={processing}
color="primary"
>
{LL.RESTART()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={partition}
disabled={processing}
color="primary"
>
EMS-ESP Loader
</Button>
</DialogActions>
</Dialog>
);
const renderFactoryResetDialog = () => (
<Dialog sx={dialogStyle} open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
<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"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={factoryReset}
disabled={processing}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => (
<>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
{/* TODO: translate */}
<ListMenuItem
icon={TuneIcon}
bgcolor="#134ba2"
label={LL.APPLICATION_SETTINGS()}
text="Modify EMS-ESP Application Settings"
to="ems-esp"
/>
<ListMenuItem
icon={SettingsEthernetIcon}
bgcolor="#40828f"
label={LL.NETWORK(0)}
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(0)))}
to="network"
/>
<ListMenuItem
icon={SettingsInputAntennaIcon}
bgcolor="#5f9a5f"
label={LL.ACCESS_POINT(0)}
text={LL.CONFIGURE(LL.ACCESS_POINT(0))}
to="ap"
/>
<ListMenuItem
icon={AccessTimeIcon}
bgcolor="#c5572c"
label="NTP"
text={LL.CONFIGURE(LL.LOCAL_TIME())}
to="ntp"
/>
<ListMenuItem icon={DeviceHubIcon} bgcolor="#68374d" label="MQTT" text={LL.CONFIGURE('MQTT')} to="mqtt" />
<ListMenuItem icon={CastIcon} bgcolor="#efc34b" label="OTA" text={LL.CONFIGURE('OTA')} to="ota" />
{/* TODO: translate */}
<ListMenuItem icon={LockIcon} label={LL.SECURITY(0)} text="Add/Remove Users" to="security" />
<ListMenuItem
icon={MemoryIcon}
bgcolor="#b1395f"
label={LL.STATUS_OF('ESP32')}
text="ESP32 Information"
to="espsystemstatus"
/>
{/* TODO: translate */}
<ListMenuItem
icon={ImportExportIcon}
bgcolor="#5d89f7"
label={LL.UPLOAD_DOWNLOAD()}
text="Upload/Download Settings and Firmware"
to="upload"
/>
</List>
{renderRestartDialog()}
{renderFactoryResetDialog()}
<Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
color="primary"
onClick={() => setConfirmRestart(true)}
>
{LL.RESTART()}
</Button>
</ButtonRow>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={() => setConfirmFactoryReset(true)}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</ButtonRow>
</Box>
</Box>
</>
);
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>;
};
export default Settings;

View File

@@ -6,7 +6,7 @@ import { useState } from 'react';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { APSettingsType } from 'types'; import type { APSettings } from 'types';
import * as APApi from 'api/ap'; import * as APApi from 'api/ap';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
@@ -24,10 +24,10 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
import { createAPSettingsValidator, validate } from 'validators'; import { createAPSettingsValidator, validate } from 'validators';
export const isAPEnabled = ({ provision_mode }: APSettingsType) => export const isAPEnabled = ({ provision_mode }: APSettings) =>
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED; provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
const APSettings: FC = () => { const APSettingsForm: FC = () => {
const { const {
loadData, loadData,
saving, saving,
@@ -39,7 +39,7 @@ const APSettings: FC = () => {
blocker, blocker,
saveData, saveData,
errorMessage errorMessage
} = useRest<APSettingsType>({ } = useRest<APSettings>({
read: APApi.readAPSettings, read: APApi.readAPSettings,
update: APApi.updateAPSettings update: APApi.updateAPSettings
}); });
@@ -205,11 +205,11 @@ const APSettings: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()} {content()}
</SectionContent> </SectionContent>
); );
}; };
export default APSettings; export default APSettingsForm;

View File

@@ -7,14 +7,14 @@ import { useRequest } from 'alova';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import type { APStatusType } from 'types'; import type { APStatus } from 'types';
import * as APApi from 'api/ap'; import * as APApi from 'api/ap';
import { ButtonRow, FormLoader, SectionContent } from 'components'; import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { APNetworkStatus } from 'types'; import { APNetworkStatus } from 'types';
export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => { export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
switch (status) { switch (status) {
case APNetworkStatus.ACTIVE: case APNetworkStatus.ACTIVE:
return theme.palette.success.main; return theme.palette.success.main;
@@ -27,14 +27,14 @@ export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
} }
}; };
const APStatus: FC = () => { const APStatusForm: FC = () => {
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus); const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const theme = useTheme(); const theme = useTheme();
const apStatus = ({ status }: APStatusType) => { const apStatus = ({ status }: APStatus) => {
switch (status) { switch (status) {
case APNetworkStatus.ACTIVE: case APNetworkStatus.ACTIVE:
return LL.ACTIVE(); return LL.ACTIVE();
@@ -99,7 +99,11 @@ const APStatus: FC = () => {
); );
}; };
return <SectionContent>{content()}</SectionContent>; return (
<SectionContent title={LL.STATUS_OF(LL.ACCESS_POINT(1))} titleGutter>
{content()}
</SectionContent>
);
}; };
export default APStatus; export default APStatusForm;

View File

@@ -1,10 +1,12 @@
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { useContext } from 'react';
import { Navigate, Routes, Route } from 'react-router-dom'; import { Navigate, Routes, Route } from 'react-router-dom';
import APSettings from './APSettings'; import APSettingsForm from './APSettingsForm';
import APStatus from './APStatus'; import APStatusForm from './APStatusForm';
import type { FC } from 'react'; import type { FC } from 'react';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components'; import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -13,18 +15,32 @@ const AccessPoint: FC = () => {
useLayoutTitle(LL.ACCESS_POINT(0)); useLayoutTitle(LL.ACCESS_POINT(0));
const authenticatedContext = useContext(AuthenticatedContext);
const { routerTab } = useRouterTab(); const { routerTab } = useRouterTab();
return ( return (
<> <>
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab value="settings" label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} /> <Tab value="/ap/status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} />
<Tab value="status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} /> <Tab
value="/ap/settings"
label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))}
disabled={!authenticatedContext.me.admin}
/>
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="status" element={<APStatus />} /> <Route path="status" element={<APStatusForm />} />
<Route path="settings" element={<APSettings />} /> <Route index element={<Navigate to="status" />} />
<Route path="*" element={<Navigate replace to="settings" />} /> <Route
path="settings"
element={
<RequireAdmin>
<APSettingsForm />
</RequireAdmin>
}
/>
<Route path="*" element={<Navigate replace to="/ap/status" />} />
</Routes> </Routes>
</> </>
); );

View File

@@ -1,10 +1,12 @@
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import MqttSettings from './MqttSettings'; import MqttSettingsForm from './MqttSettingsForm';
import MqttStatus from './MqttStatus'; import MqttStatusForm from './MqttStatusForm';
import type { FC } from 'react'; import type { FC } from 'react';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components'; import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -13,18 +15,26 @@ const Mqtt: FC = () => {
useLayoutTitle('MQTT'); useLayoutTitle('MQTT');
const authenticatedContext = useContext(AuthenticatedContext);
const { routerTab } = useRouterTab(); const { routerTab } = useRouterTab();
return ( return (
<> <>
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab value="settings" label={LL.SETTINGS_OF('MQTT')} /> <Tab value="/mqtt/status" label={LL.STATUS_OF('MQTT')} />
<Tab value="status" label={LL.STATUS_OF('MQTT')} /> <Tab value="/mqtt/settings" label={LL.SETTINGS_OF('MQTT')} disabled={!authenticatedContext.me.admin} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="status" element={<MqttStatus />} /> <Route path="status" element={<MqttStatusForm />} />
<Route path="settings" element={<MqttSettings />} /> <Route
<Route path="*" element={<Navigate replace to="settings" />} /> path="settings"
element={
<RequireAdmin>
<MqttSettingsForm />
</RequireAdmin>
}
/>
<Route path="*" element={<Navigate replace to="/mqtt/status" />} />
</Routes> </Routes>
</> </>
); );

View File

@@ -5,7 +5,7 @@ import { useState } from 'react';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { MqttSettingsType } from 'types'; import type { MqttSettings } from 'types';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
@@ -21,7 +21,7 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators'; import { createMqttSettingsValidator, validate } from 'validators';
const MqttSettings: FC = () => { const MqttSettingsForm: FC = () => {
const { const {
loadData, loadData,
saving, saving,
@@ -33,7 +33,7 @@ const MqttSettings: FC = () => {
blocker, blocker,
saveData, saveData,
errorMessage errorMessage
} = useRest<MqttSettingsType>({ } = useRest<MqttSettings>({
read: MqttApi.readMqttSettings, read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings update: MqttApi.updateMqttSettings
}); });
@@ -388,21 +388,6 @@ const MqttSettings: FC = () => {
margin="normal" margin="normal"
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_water"
label={LL.MQTT_INT_WATER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_water)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}> <Grid item xs={12} sm={6} md={4}>
<TextField <TextField
name="publish_time_sensor" name="publish_time_sensor"
@@ -464,11 +449,11 @@ const MqttSettings: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()} {content()}
</SectionContent> </SectionContent>
); );
}; };
export default MqttSettings; export default MqttSettingsForm;

View File

@@ -8,13 +8,13 @@ import { useRequest } from 'alova';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import type { MqttStatusType } from 'types'; import type { MqttStatus } from 'types';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
import { ButtonRow, FormLoader, SectionContent } from 'components'; import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { MqttDisconnectReason } from 'types'; import { MqttDisconnectReason } from 'types';
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatusType, theme: Theme) => { export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
if (!enabled) { if (!enabled) {
return theme.palette.info.main; return theme.palette.info.main;
} }
@@ -24,27 +24,27 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatusType, them
return theme.palette.error.main; return theme.palette.error.main;
}; };
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatusType, theme: Theme) => { export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
if (mqtt_fails === 0) return theme.palette.success.main; if (mqtt_fails === 0) return theme.palette.success.main;
if (mqtt_fails < 10) return theme.palette.warning.main; if (mqtt_fails < 10) return theme.palette.warning.main;
return theme.palette.error.main; return theme.palette.error.main;
}; };
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatusType, theme: Theme) => { export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatus, theme: Theme) => {
if (mqtt_queued <= 1) return theme.palette.success.main; if (mqtt_queued <= 1) return theme.palette.success.main;
return theme.palette.warning.main; return theme.palette.warning.main;
}; };
const MqttStatus: FC = () => { const MqttStatusForm: FC = () => {
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus); const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const theme = useTheme(); const theme = useTheme();
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatusType) => { const mqttStatus = ({ enabled, connected, connect_count }: MqttStatus) => {
if (!enabled) { if (!enabled) {
return LL.NOT_ENABLED(); return LL.NOT_ENABLED();
} }
@@ -54,7 +54,7 @@ const MqttStatus: FC = () => {
return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : ''); return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : '');
}; };
const disconnectReason = ({ disconnect_reason }: MqttStatusType) => { const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
switch (disconnect_reason) { switch (disconnect_reason) {
case MqttDisconnectReason.TCP_DISCONNECTED: case MqttDisconnectReason.TCP_DISCONNECTED:
return 'TCP disconnected'; return 'TCP disconnected';
@@ -146,7 +146,11 @@ const MqttStatus: FC = () => {
); );
}; };
return <SectionContent>{content()}</SectionContent>; return (
<SectionContent title={LL.STATUS_OF('MQTT')} titleGutter>
{content()}
</SectionContent>
);
}; };
export default MqttStatus; export default MqttStatusForm;

View File

@@ -1,59 +0,0 @@
import { Tab } from '@mui/material';
import { useCallback, useState } from 'react';
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
import NetworkSettings from './NetworkSettings';
import NetworkStatus from './NetworkStatus';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import type { FC } from 'react';
import type { WiFiNetwork } from 'types';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Network: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK(0));
const { routerTab } = useRouterTab();
const navigate = useNavigate();
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
const selectNetwork = useCallback(
(network: WiFiNetwork) => {
setSelectedNetwork(network);
navigate('settings');
},
[navigate]
);
const deselectNetwork = useCallback(() => {
setSelectedNetwork(undefined);
}, []);
return (
<WiFiConnectionContext.Provider
value={{
selectedNetwork,
selectNetwork,
deselectNetwork
}}
>
<RouterTabs value={routerTab}>
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} />
<Tab value="status" label={LL.STATUS_OF(LL.NETWORK(1))} />
<Tab value="scan" label={LL.NETWORK_SCAN()} />
</RouterTabs>
<Routes>
<Route path="status" element={<NetworkStatus />} />
<Route path="scan" element={<WiFiNetworkScanner />} />
<Route path="settings" element={<NetworkSettings />} />
<Route path="*" element={<Navigate replace to="settings" />} />
</Routes>
</WiFiConnectionContext.Provider>
);
};
export default Network;

View File

@@ -0,0 +1,79 @@
import { Tab } from '@mui/material';
import { useCallback, useContext, useState } from 'react';
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
import NetworkSettingsForm from './NetworkSettingsForm';
import NetworkStatusForm from './NetworkStatusForm';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import type { FC } from 'react';
import type { WiFiNetwork } from 'types';
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
const NetworkConnection: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK(0));
const { routerTab } = useRouterTab();
const authenticatedContext = useContext(AuthenticatedContext);
const navigate = useNavigate();
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
const selectNetwork = useCallback(
(network: WiFiNetwork) => {
setSelectedNetwork(network);
navigate('settings');
},
[navigate]
);
const deselectNetwork = useCallback(() => {
setSelectedNetwork(undefined);
}, []);
return (
<WiFiConnectionContext.Provider
value={{
selectedNetwork,
selectNetwork,
deselectNetwork
}}
>
<RouterTabs value={routerTab}>
<Tab value="/network/status" label={LL.STATUS_OF(LL.NETWORK(1))} />
<Tab value="/network/scan" label={LL.NETWORK_SCAN()} disabled={!authenticatedContext.me.admin} />
<Tab
value="/network/settings"
label={LL.SETTINGS_OF(LL.NETWORK(1))}
disabled={!authenticatedContext.me.admin}
/>
</RouterTabs>
<Routes>
<Route path="status" element={<NetworkStatusForm />} />
<Route
path="scan"
element={
<RequireAdmin>
<WiFiNetworkScanner />
</RequireAdmin>
}
/>
<Route
path="settings"
element={
<RequireAdmin>
<NetworkSettingsForm />
</RequireAdmin>
}
/>
<Route path="*" element={<Navigate replace to="/network/status" />} />
</Routes>
</WiFiConnectionContext.Provider>
);
};
export default NetworkConnection;

View File

@@ -28,7 +28,7 @@ import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { NetworkSettingsType } from 'types'; import type { NetworkSettings } from 'types';
import * as NetworkApi from 'api/network'; import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { import {
@@ -48,7 +48,7 @@ import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network'; import { createNetworkSettingsValidator } from 'validators/network';
const NetworkSettings: FC = () => { const WiFiSettingsForm: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext); const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
@@ -68,7 +68,7 @@ const NetworkSettings: FC = () => {
saveData, saveData,
errorMessage, errorMessage,
restartNeeded restartNeeded
} = useRest<NetworkSettingsType>({ } = useRest<NetworkSettings>({
read: NetworkApi.readNetworkSettings, read: NetworkApi.readNetworkSettings,
update: NetworkApi.updateNetworkSettings update: NetworkApi.updateNetworkSettings
}); });
@@ -135,7 +135,7 @@ const NetworkSettings: FC = () => {
return ( return (
<> <>
<Typography variant="h6" color="primary"> <Typography sx={{ pt: 2 }} variant="h6" color="primary">
WiFi WiFi
</Typography> </Typography>
{selectedNetwork ? ( {selectedNetwork ? (
@@ -360,11 +360,11 @@ const NetworkSettings: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : content()} {restarting ? <RestartMonitor /> : content()}
</SectionContent> </SectionContent>
); );
}; };
export default NetworkSettings; export default WiFiSettingsForm;

View File

@@ -11,18 +11,18 @@ import { useRequest } from 'alova';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import type { NetworkStatusType } from 'types'; import type { NetworkStatus } from 'types';
import * as NetworkApi from 'api/network'; import * as NetworkApi from 'api/network';
import { ButtonRow, FormLoader, SectionContent } from 'components'; import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { NetworkConnectionStatus } from 'types'; import { NetworkConnectionStatus } from 'types';
const isConnected = ({ status }: NetworkStatusType) => const isConnected = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED || status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED; status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
const networkStatusHighlight = ({ status }: NetworkStatusType, theme: Theme) => { const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
switch (status) { switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_IDLE: case NetworkConnectionStatus.WIFI_STATUS_IDLE:
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED: case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
@@ -39,7 +39,7 @@ const networkStatusHighlight = ({ status }: NetworkStatusType, theme: Theme) =>
} }
}; };
const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => { const networkQualityHighlight = ({ rssi }: NetworkStatus, theme: Theme) => {
if (rssi <= -85) { if (rssi <= -85) {
return theme.palette.error.main; return theme.palette.error.main;
} else if (rssi <= -75) { } else if (rssi <= -75) {
@@ -48,18 +48,17 @@ const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => {
return theme.palette.success.main; return theme.palette.success.main;
}; };
export const isWiFi = ({ status }: NetworkStatusType) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED; export const isWiFi = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
export const isEthernet = ({ status }: NetworkStatusType) => export const isEthernet = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatusType) => { const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatus) => {
if (!dns_ip_1) { if (!dns_ip_1) {
return 'none'; return 'none';
} }
return dns_ip_1 + (!dns_ip_2 || 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: NetworkStatusType) => { const IPs = (status: NetworkStatus) => {
if (!status.local_ipv6 || status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000') { if (!status.local_ipv6 || status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000') {
return status.local_ip; return status.local_ip;
} }
@@ -69,14 +68,14 @@ const IPs = (status: NetworkStatusType) => {
return status.local_ip + ', ' + status.local_ipv6; return status.local_ip + ', ' + status.local_ipv6;
}; };
const NetworkStatus: FC = () => { const NetworkStatusForm: FC = () => {
const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus); const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const theme = useTheme(); const theme = useTheme();
const networkStatus = ({ status }: NetworkStatusType) => { const networkStatus = ({ status }: NetworkStatus) => {
switch (status) { switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD: case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return LL.INACTIVE(1); return LL.INACTIVE(1);
@@ -194,7 +193,11 @@ const NetworkStatus: FC = () => {
); );
}; };
return <SectionContent>{content()}</SectionContent>; return (
<SectionContent title={LL.STATUS_OF(LL.NETWORK(1))} titleGutter>
{content()}
</SectionContent>
);
}; };
export default NetworkStatus; export default NetworkStatusForm;

View File

@@ -56,7 +56,7 @@ const WiFiNetworkScanner: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.NETWORK_SCANNER()}>
{renderNetworkScanner()} {renderNetworkScanner()}
<ButtonRow> <ButtonRow>
<Button <Button

View File

@@ -8,7 +8,7 @@ import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { NTPSettingsType } from 'types'; import type { NTPSettings } from 'types';
import * as NTPApi from 'api/ntp'; import * as NTPApi from 'api/ntp';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
@@ -23,7 +23,7 @@ import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
const NTPSettings: FC = () => { const NTPSettingsForm: FC = () => {
const { const {
loadData, loadData,
saving, saving,
@@ -35,7 +35,7 @@ const NTPSettings: FC = () => {
blocker, blocker,
saveData, saveData,
errorMessage errorMessage
} = useRest<NTPSettingsType>({ } = useRest<NTPSettings>({
read: NTPApi.readNTPSettings, read: NTPApi.readNTPSettings,
update: NTPApi.updateNTPSettings update: NTPApi.updateNTPSettings
}); });
@@ -130,11 +130,11 @@ const NTPSettings: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()} {content()}
</SectionContent> </SectionContent>
); );
}; };
export default NTPSettings; export default NTPSettingsForm;

View File

@@ -22,26 +22,44 @@ import {
Typography Typography
} from '@mui/material'; } from '@mui/material';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { useState } from 'react'; import { useContext, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import type { NTPStatusType } from 'types'; import type { NTPStatus } from 'types';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import * as NTPApi from 'api/ntp'; import * as NTPApi from 'api/ntp';
import { ButtonRow, FormLoader, SectionContent } from 'components'; import { ButtonRow, FormLoader, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { NTPSyncStatus } from 'types'; import { NTPSyncStatus } from 'types';
import { formatDateTime, formatLocalDateTime } from 'utils'; import { formatDateTime, formatLocalDateTime } from 'utils';
const NTPStatus: FC = () => { export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
switch (status) {
case NTPSyncStatus.NTP_DISABLED:
return theme.palette.info.main;
case NTPSyncStatus.NTP_INACTIVE:
return theme.palette.error.main;
case NTPSyncStatus.NTP_ACTIVE:
return theme.palette.success.main;
default:
return theme.palette.error.main;
}
};
const NTPStatusForm: FC = () => {
const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus); const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
const [localTime, setLocalTime] = useState<string>(''); const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false); const [settingTime, setSettingTime] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -51,22 +69,6 @@ const NTPStatus: FC = () => {
NTPApi.updateTime; NTPApi.updateTime;
const isNtpActive = ({ status }: NTPStatusType) => status === NTPSyncStatus.NTP_ACTIVE;
const isNtpEnabled = ({ status }: NTPStatusType) => status !== NTPSyncStatus.NTP_DISABLED;
const ntpStatusHighlight = ({ status }: NTPStatusType, theme: Theme) => {
switch (status) {
case NTPSyncStatus.NTP_DISABLED:
return theme.palette.info.main;
case NTPSyncStatus.NTP_INACTIVE:
return theme.palette.error.main;
case NTPSyncStatus.NTP_ACTIVE:
return theme.palette.success.main;
default:
return theme.palette.error.main;
}
};
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value); const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
const openSetTime = () => { const openSetTime = () => {
@@ -76,7 +78,7 @@ const NTPStatus: FC = () => {
const theme = useTheme(); const theme = useTheme();
const ntpStatus = ({ status }: NTPStatusType) => { const ntpStatus = ({ status }: NTPStatus) => {
switch (status) { switch (status) {
case NTPSyncStatus.NTP_DISABLED: case NTPSyncStatus.NTP_DISABLED:
return LL.NOT_ENABLED(); return LL.NOT_ENABLED();
@@ -199,7 +201,7 @@ const NTPStatus: FC = () => {
</Button> </Button>
</ButtonRow> </ButtonRow>
</Box> </Box>
{data && !isNtpActive(data) && ( {me.admin && data && !isNtpActive(data) && (
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow> <ButtonRow>
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}> <Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}>
@@ -214,7 +216,11 @@ const NTPStatus: FC = () => {
); );
}; };
return <SectionContent>{content()}</SectionContent>; return (
<SectionContent title={LL.STATUS_OF('NTP')} titleGutter>
{content()}
</SectionContent>
);
}; };
export default NTPStatus; export default NTPStatusForm;

View File

@@ -1,10 +1,12 @@
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import NTPSettings from './NTPSettings'; import NTPSettingsForm from './NTPSettingsForm';
import NTPStatus from './NTPStatus'; import NTPStatusForm from './NTPStatusForm';
import type { FC } from 'react'; import type { FC } from 'react';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components'; import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -12,18 +14,26 @@ const NetworkTime: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle('NTP'); useLayoutTitle('NTP');
const authenticatedContext = useContext(AuthenticatedContext);
const { routerTab } = useRouterTab(); const { routerTab } = useRouterTab();
return ( return (
<> <>
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab value="settings" label={LL.SETTINGS_OF('NTP')} /> <Tab value="/ntp/status" label={LL.STATUS_OF('NTP')} />
<Tab value="status" label={LL.STATUS_OF('NTP')} /> <Tab value="/ntp/settings" label={LL.SETTINGS_OF('NTP')} disabled={!authenticatedContext.me.admin} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="status" element={<NTPStatus />} /> <Route path="status" element={<NTPStatusForm />} />
<Route path="settings" element={<NTPSettings />} /> <Route
<Route path="*" element={<Navigate replace to="settings" />} /> path="settings"
element={
<RequireAdmin>
<NTPSettingsForm />
</RequireAdmin>
}
/>
<Route path="*" element={<Navigate replace to="/ntp/status" />} />
</Routes> </Routes>
</> </>
); );

View File

@@ -14,9 +14,9 @@ import { useContext, useState } from 'react';
import { useBlocker } from 'react-router-dom'; import { useBlocker } from 'react-router-dom';
import GenerateToken from './GenerateToken'; import GenerateToken from './GenerateToken';
import User from './User'; import UserForm from './UserForm';
import type { FC } from 'react'; import type { FC } from 'react';
import type { SecuritySettingsType, UserType } from 'types'; import type { SecuritySettings, User } from 'types';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import { ButtonRow, FormLoader, MessageBox, SectionContent, BlockNavigation } from 'components'; import { ButtonRow, FormLoader, MessageBox, SectionContent, BlockNavigation } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
@@ -24,13 +24,13 @@ import { useI18nContext } from 'i18n/i18n-react';
import { useRest } from 'utils'; import { useRest } from 'utils';
import { createUserValidator } from 'validators'; import { createUserValidator } from 'validators';
const ManageUsers: FC = () => { const ManageUsersForm: FC = () => {
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettingsType>({ const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings, read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings update: SecurityApi.updateSecuritySettings
}); });
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<User>();
const [creating, setCreating] = useState<boolean>(false); const [creating, setCreating] = useState<boolean>(false);
const [changed, setChanged] = useState<number>(0); const [changed, setChanged] = useState<number>(0);
const [generatingToken, setGeneratingToken] = useState<string>(); const [generatingToken, setGeneratingToken] = useState<string>();
@@ -86,7 +86,7 @@ const ManageUsers: FC = () => {
const noAdminConfigured = () => !data.users.find((u) => u.admin); const noAdminConfigured = () => !data.users.find((u) => u.admin);
const removeUser = (toRemove: UserType) => { const removeUser = (toRemove: User) => {
const users = data.users.filter((u) => u.username !== toRemove.username); const users = data.users.filter((u) => u.username !== toRemove.username);
updateDataValue({ ...data, users }); updateDataValue({ ...data, users });
setChanged(changed + 1); setChanged(changed + 1);
@@ -101,7 +101,7 @@ const ManageUsers: FC = () => {
}); });
}; };
const editUser = (toEdit: UserType) => { const editUser = (toEdit: User) => {
setCreating(false); setCreating(false);
setUser({ ...toEdit }); setUser({ ...toEdit });
}; };
@@ -219,7 +219,7 @@ const ManageUsers: FC = () => {
</Box> </Box>
<GenerateToken username={generatingToken} onClose={closeGenerateToken} /> <GenerateToken username={generatingToken} onClose={closeGenerateToken} />
<User <UserForm
user={user} user={user}
setUser={setUser} setUser={setUser}
creating={creating} creating={creating}
@@ -232,11 +232,11 @@ const ManageUsers: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.MANAGE_USERS()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()} {content()}
</SectionContent> </SectionContent>
); );
}; };
export default ManageUsers; export default ManageUsersForm;

View File

@@ -1,7 +1,7 @@
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { Navigate, Routes, Route } from 'react-router-dom'; import { Navigate, Routes, Route } from 'react-router-dom';
import ManageUsers from './ManageUsers'; import ManageUsersForm from './ManageUsersForm';
import SecuritySettings from './SecuritySettings'; import SecuritySettingsForm from './SecuritySettingsForm';
import type { FC } from 'react'; import type { FC } from 'react';
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components'; import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
@@ -17,13 +17,13 @@ const Security: FC = () => {
return ( return (
<> <>
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} /> <Tab value="/security/users" label={LL.MANAGE_USERS()} />
<Tab value="users" label={LL.MANAGE_USERS()} /> <Tab value="/security/settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="users" element={<ManageUsers />} /> <Route path="users" element={<ManageUsersForm />} />
<Route path="settings" element={<SecuritySettings />} /> <Route path="settings" element={<SecuritySettingsForm />} />
<Route path="*" element={<Navigate replace to="settings" />} /> <Route path="*" element={<Navigate replace to="/security/users" />} />
</Routes> </Routes>
</> </>
); );

View File

@@ -5,16 +5,16 @@ import { useContext, useState } from 'react';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { SecuritySettingsType } from 'types'; import type { SecuritySettings } from 'types';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField, BlockNavigation } from 'components'; import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField, BlockNavigation } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators'; import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
const SecuritySettings: FC = () => { const SecuritySettingsForm: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -29,7 +29,7 @@ const SecuritySettings: FC = () => {
blocker, blocker,
saveData, saveData,
errorMessage errorMessage
} = useRest<SecuritySettingsType>({ } = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings, read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings update: SecurityApi.updateSecuritySettings
}); });
@@ -96,11 +96,11 @@ const SecuritySettings: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()} {content()}
</SectionContent> </SectionContent>
); );
}; };
export default SecuritySettings; export default SecuritySettingsForm;

View File

@@ -8,7 +8,7 @@ import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { UserType } from 'types'; import type { User } from 'types';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components'; import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -19,14 +19,14 @@ interface UserFormProps {
creating: boolean; creating: boolean;
validator: Schema; validator: Schema;
user?: UserType; user?: User;
setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>; setUser: React.Dispatch<React.SetStateAction<User | undefined>>;
onDoneEditing: () => void; onDoneEditing: () => void;
onCancelEditing: () => void; onCancelEditing: () => void;
} }
const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => { const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const updateFormValue = updateValue(setUser); const updateFormValue = updateValue(setUser);
@@ -104,4 +104,4 @@ const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEdi
); );
}; };
export default User; export default UserForm;

View File

@@ -1,154 +0,0 @@
import AppsIcon from '@mui/icons-material/Apps';
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
import DevicesIcon from '@mui/icons-material/Devices';
import FolderIcon from '@mui/icons-material/Folder';
import MemoryIcon from '@mui/icons-material/Memory';
import RefreshIcon from '@mui/icons-material/Refresh';
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
import SdStorageIcon from '@mui/icons-material/SdStorage';
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
import { useRequest } from 'alova';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
const ESPSystemStatus: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF('ESP32'));
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true });
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<DevicesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="SDK" secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<DeveloperBoardIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="CPU"
secondary={
data.esp_platform +
'/' +
data.cpu_type +
' (rev.' +
data.cpu_rev +
', ' +
(data.cpu_cores == 1 ? 'single-core)' : 'dual-core)') +
' @ ' +
data.cpu_freq_mhz +
' Mhz'
}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<MemoryIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.HEAP()}
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '}
/>
</ListItem>
{data.psram_size !== undefined && data.free_psram !== undefined && (
<>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<AppsIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.PSRAM()}
secondary={formatNumber(data.psram_size) + ' KB / ' + formatNumber(data.free_psram) + ' KB'}
/>
</ListItem>
</>
)}
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<SdStorageIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.FLASH()}
secondary={
formatNumber(data.flash_chip_size) + ' KB / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'
}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<SdCardAlertIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.APPSIZE()}
secondary={
data.partition + ': ' + formatNumber(data.app_used) + ' KB / ' + formatNumber(data.app_free) + ' KB'
}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.FILESYSTEM()}
secondary={formatNumber(data.fs_used) + ' KB / ' + formatNumber(data.fs_free) + ' KB'}
/>
</ListItem>
<Divider variant="inset" component="li" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
</Box>
</>
);
};
return <SectionContent>{content()}</SectionContent>;
};
export default ESPSystemStatus;

View File

@@ -5,7 +5,7 @@ import { useState } from 'react';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { OTASettingsType } from 'types'; import type { OTASettings } from 'types';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
@@ -14,8 +14,7 @@ import {
SectionContent, SectionContent,
ValidatedPasswordField, ValidatedPasswordField,
ValidatedTextField, ValidatedTextField,
BlockNavigation, BlockNavigation
useLayoutTitle
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -24,7 +23,7 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { OTA_SETTINGS_VALIDATOR } from 'validators/system'; import { OTA_SETTINGS_VALIDATOR } from 'validators/system';
const OTASettings: FC = () => { const OTASettingsForm: FC = () => {
const { const {
loadData, loadData,
saveData, saveData,
@@ -36,7 +35,7 @@ const OTASettings: FC = () => {
setDirtyFlags, setDirtyFlags,
blocker, blocker,
errorMessage errorMessage
} = useRest<OTASettingsType>({ } = useRest<OTASettings>({
read: SystemApi.readOTASettings, read: SystemApi.readOTASettings,
update: SystemApi.updateOTASettings update: SystemApi.updateOTASettings
}); });
@@ -62,8 +61,6 @@ const OTASettings: FC = () => {
} }
}; };
useLayoutTitle('OTA');
return ( return (
<> <>
<BlockFormControlLabel <BlockFormControlLabel
@@ -120,11 +117,11 @@ const OTASettings: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()} {content()}
</SectionContent> </SectionContent>
); );
}; };
export default OTASettings; export default OTASettingsForm;

View File

@@ -1,42 +1,53 @@
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { useContext, type FC } from 'react'; import { useContext } from 'react';
import { Navigate, Routes, Route } from 'react-router-dom'; import { Navigate, Routes, Route } from 'react-router-dom';
import OTASettingsForm from './OTASettingsForm';
import SystemLog from './SystemLog'; import SystemLog from './SystemLog';
import SystemStatus from './SystemStatus'; import SystemStatusForm from './SystemStatusForm';
import UploadFileForm from './UploadFileForm';
import type { FC } from 'react';
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from 'components'; import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import SystemActivity from 'project/SystemActivity';
const System: FC = () => { const System: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.SYSTEM(0)); useLayoutTitle(LL.SYSTEM(0));
const { routerTab } = useRouterTab();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
const { routerTab } = useRouterTab();
return ( return (
<> <>
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab value="status" label={LL.STATUS_OF('')} /> <Tab value="/system/status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
<Tab value="activity" label={LL.ACTIVITY()} /> <Tab value="/system/log" label={LL.LOG_OF(LL.SYSTEM(2))} />
<Tab disabled={!me.admin} value="log" label={me.admin ? LL.LOG_OF('') : ''} /> <Tab value="/system/ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />
<Tab value="/system/upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="status" element={<SystemStatus />} /> <Route path="status" element={<SystemStatusForm />} />
<Route path="activity" element={<SystemActivity />} /> <Route path="log" element={<SystemLog />} />
<Route <Route
path="log" path="ota"
element={ element={
<RequireAdmin> <RequireAdmin>
<SystemLog /> <OTASettingsForm />
</RequireAdmin> </RequireAdmin>
} }
/> />
<Route path="*" element={<Navigate replace to="status" />} /> <Route
path="upload"
element={
<RequireAdmin>
<UploadFileForm />
</RequireAdmin>
}
/>
<Route path="*" element={<Navigate replace to="/system/status" />} />
</Routes> </Routes>
</> </>
); );

View File

@@ -11,7 +11,7 @@ import { addAccessTokenParameter } from 'api/authentication';
import { EVENT_SOURCE_ROOT } from 'api/endpoints'; import { EVENT_SOURCE_ROOT } from 'api/endpoints';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation, useLayoutTitle } from 'components'; import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { LogLevel } from 'types'; import { LogLevel } from 'types';
@@ -50,8 +50,6 @@ const levelLabel = (level: LogLevel) => {
const SystemLog: FC = () => { const SystemLog: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.LOG_OF(''));
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest<LogSettings>({ useRest<LogSettings>({
read: SystemApi.readLogSettings, read: SystemApi.readLogSettings,
@@ -234,7 +232,7 @@ const SystemLog: FC = () => {
}; };
return ( return (
<SectionContent id="log-window"> <SectionContent title={LL.LOG_OF(LL.SYSTEM(2))} titleGutter id="log-window">
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()} {content()}
</SectionContent> </SectionContent>

View File

@@ -1,297 +0,0 @@
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel';
import CastIcon from '@mui/icons-material/Cast';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import MemoryIcon from '@mui/icons-material/Memory';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import RefreshIcon from '@mui/icons-material/Refresh';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TimerIcon from '@mui/icons-material/Timer';
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import { useRequest } from 'alova';
import { useContext, type FC, useState } from 'react';
import { toast } from 'react-toastify';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
import { busConnectionStatus } from 'project/types';
import { NTPSyncStatus } from 'types';
const SystemStatus: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(''));
const { me } = useContext(AuthenticatedContext);
const [confirmScan, setConfirmScan] = useState<boolean>(false);
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
immediate: false
});
const theme = useTheme();
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;
};
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
const busStatus = () => {
if (data) {
switch (data.status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (' + formatDurationSec(data.bus_uptime) + ')';
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return LL.TX_ISSUES();
case busConnectionStatus.BUS_STATUS_OFFLINE:
return LL.DISCONNECTED();
}
}
return 'Unknown';
};
const busStatusHighlight = () => {
switch (data.status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main;
case busConnectionStatus.BUS_STATUS_CONNECTED:
return theme.palette.success.main;
case busConnectionStatus.BUS_STATUS_OFFLINE:
return theme.palette.error.main;
default:
return theme.palette.warning.main;
}
};
const ntpStatus = () => {
switch (data.ntp_status) {
case NTPSyncStatus.NTP_DISABLED:
return LL.NOT_ENABLED();
case NTPSyncStatus.NTP_INACTIVE:
return LL.INACTIVE(0);
case NTPSyncStatus.NTP_ACTIVE:
return LL.ACTIVE();
default:
return LL.UNKNOWN();
}
};
const ntpStatusHighlight = () => {
switch (data.ntp_status) {
case NTPSyncStatus.NTP_DISABLED:
return theme.palette.info.main;
case NTPSyncStatus.NTP_INACTIVE:
return theme.palette.error.main;
case NTPSyncStatus.NTP_ACTIVE:
return theme.palette.success.main;
default:
return theme.palette.error.main;
}
};
const activeHighlight = (value: boolean) => (value ? theme.palette.success.main : theme.palette.info.main);
const scan = async () => {
await scanDevices()
.then(() => {
toast.info(LL.SCANNING() + '...');
})
.catch((err) => {
toast.error(err.message);
});
setConfirmScan(false);
};
const renderScanDialog = () => (
<Dialog sx={dialogStyle} open={confirmScan} onClose={() => setConfirmScan(false)}>
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
{LL.CANCEL()}
</Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
{LL.SCAN()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: busStatusHighlight(), color: 'white' }}>
<DirectionsBusIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.EMS_BUS_STATUS()} secondary={busStatus()} />
</ListItem>
<Divider variant="inset" component="li" />
<ListMenuItem
disabled={!me.admin}
icon={TimerIcon}
bgcolor="#c5572c"
label={LL.UPTIME()}
text={formatDurationSec(data.uptime)}
/>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5d89f7', color: 'white' }}>
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.ACTIVE_DEVICES()}
secondary={
LL.NUM_DEVICES({ num: data.num_devices }) +
', ' +
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
', ' +
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
}
/>
{me.admin && (
<Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
color="primary"
onClick={() => setConfirmScan(true)}
>
{LL.SCAN_DEVICES()}
</Button>
)}
</ListItem>
<Divider variant="inset" component="li" />
<ListMenuItem
disabled={!me.admin}
icon={BuildIcon}
bgcolor="#134ba2"
label={LL.EMS_ESP_VER()}
text={data.emsesp_version}
to="/settings/upload"
/>
<Divider variant="inset" component="li" />
{/* TODO: translate */}
<ListMenuItem
disabled={!me.admin}
icon={MemoryIcon}
bgcolor="#68374d"
label="System Memory"
text={formatNumber(data.free_heap) + ' KB'}
to="/settings/espsystemstatus"
/>
<Divider variant="inset" component="li" />
<ListMenuItem
disabled={!me.admin}
icon={DeviceHubIcon}
bgcolor={activeHighlight(data.mqtt_status)}
label={LL.STATUS_OF('MQTT')}
text={data.mqtt_status ? LL.ACTIVE() : LL.INACTIVE(0)}
to="/settings/mqtt/status"
/>
<Divider variant="inset" component="li" />
<ListMenuItem
disabled={!me.admin}
icon={AccessTimeIcon}
bgcolor={ntpStatusHighlight()}
label={LL.STATUS_OF('NTP')}
text={ntpStatus()}
to="/settings/ntp/status"
/>
<Divider variant="inset" component="li" />
<ListMenuItem
disabled={!me.admin}
icon={CastIcon}
bgcolor={activeHighlight(data.ota_status)}
label={LL.STATUS_OF('OTA')}
text={data.ota_status ? LL.ACTIVE() : LL.INACTIVE(0)}
to="/settings/ota"
/>
<Divider variant="inset" component="li" />
<ListMenuItem
disabled={!me.admin}
icon={SettingsInputAntennaIcon}
bgcolor={activeHighlight(data.ota_status)}
label={LL.ACCESS_POINT(0)}
text={data.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)}
to="/settings/ap/status"
/>
<Divider variant="inset" component="li" />
</List>
{renderScanDialog()}
<Box mt={2} display="flex" flexWrap="wrap">
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</Box>
</>
);
};
return <SectionContent>{content()}</SectionContent>;
};
export default SystemStatus;

View File

@@ -0,0 +1,362 @@
import AppsIcon from '@mui/icons-material/Apps';
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel';
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
import DevicesIcon from '@mui/icons-material/Devices';
import FolderIcon from '@mui/icons-material/Folder';
import MemoryIcon from '@mui/icons-material/Memory';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import RefreshIcon from '@mui/icons-material/Refresh';
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
import SdStorageIcon from '@mui/icons-material/SdStorage';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import TimerIcon from '@mui/icons-material/Timer';
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@mui/material';
import { useRequest } from 'alova';
import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from './RestartMonitor';
import SystemStatusVersionDialog from './SystemStatusVersionDialog';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
const SystemStatusForm: FC = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false);
const [restarting, setRestarting] = useState<boolean>();
const [versionDialogOpen, setVersionDialogOpen] = useState<boolean>(false);
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
immediate: false
});
const { send: partitionCommand } = useRequest(SystemApi.partition(), {
immediate: false
});
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
const restart = async () => {
setProcessing(true);
await restartCommand()
.then(() => {
setRestarting(true);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmRestart(false);
setProcessing(false);
});
};
const factoryReset = async () => {
setProcessing(true);
await factoryResetCommand()
.then(() => {
setRestarting(true);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmFactoryReset(false);
setProcessing(false);
});
};
const partition = async () => {
setProcessing(true);
await partitionCommand()
.then(() => {
setRestarting(true);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmRestart(false);
setProcessing(false);
});
};
const renderRestartDialog = () => (
<Dialog sx={dialogStyle} open={confirmRestart} onClose={() => setConfirmRestart(false)}>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmRestart(false)}
disabled={processing}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={restart}
disabled={processing}
color="primary"
>
{LL.RESTART()}
</Button>
{data?.has_loader && (
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={partition}
disabled={processing}
color="primary"
>
EMS-ESP-Loader
</Button>
)}
</DialogActions>
</Dialog>
);
const renderFactoryResetDialog = () => (
<Dialog sx={dialogStyle} open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
<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"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={factoryReset}
disabled={processing}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar>
<BuildIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.EMS_ESP_VER()} secondary={data.emsesp_version} />
<Button color="primary" onClick={() => setVersionDialogOpen(true)}>
{LL.VERSION_CHECK(0)}
</Button>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<TimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.UPTIME()} secondary={data.uptime} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<DevicesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="SDK" secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<DeveloperBoardIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="CPU"
secondary={
data.esp_platform +
'/' +
data.cpu_type +
' (rev.' +
data.cpu_rev +
', ' +
(data.cpu_cores == 1 ? 'single-core)' : 'dual-core)') +
' @ ' +
data.cpu_freq_mhz +
' Mhz'
}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<MemoryIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.HEAP()}
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '}
/>
</ListItem>
{data.psram_size !== undefined && data.free_psram !== undefined && (
<>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<AppsIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.PSRAM()}
secondary={formatNumber(data.psram_size) + ' KB / ' + formatNumber(data.free_psram) + ' KB'}
/>
</ListItem>
</>
)}
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<SdStorageIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.FLASH()}
secondary={
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={
data.partition + ': ' + formatNumber(data.app_used) + ' KB / ' + formatNumber(data.app_free) + ' KB'
}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.FILESYSTEM()}
secondary={formatNumber(data.fs_used) + ' KB / ' + formatNumber(data.fs_free) + ' KB'}
/>
</ListItem>
<Divider variant="inset" component="li" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
{me.admin && (
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
color="primary"
onClick={() => setConfirmRestart(true)}
>
{LL.RESTART()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={() => setConfirmFactoryReset(true)}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</ButtonRow>
</Box>
)}
</Box>
{renderRestartDialog()}
{renderFactoryResetDialog()}
</>
);
};
return (
<SectionContent title={LL.STATUS_OF(LL.SYSTEM(1))} titleGutter>
{restarting ? <RestartMonitor /> : content()}
{data && (
<SystemStatusVersionDialog
open={versionDialogOpen}
onClose={() => setVersionDialogOpen(false)}
version={data.emsesp_version}
platform={data.esp_platform}
/>
)}
</SectionContent>
);
};
export default SystemStatusForm;

View File

@@ -0,0 +1,112 @@
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Link, Typography } from '@mui/material';
import { useRequest } from 'alova';
import { useCallback, useEffect } from 'react';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system';
import MessageBox from 'components/MessageBox';
import { useI18nContext } from 'i18n/i18n-react';
type SystemStatusVersionDialogProps = {
open: boolean;
onClose: () => void;
version: string;
platform: string;
};
const SystemStatusVersionDialog = ({ open, onClose, version, platform }: SystemStatusVersionDialogProps) => {
const { LL } = useI18nContext();
const { send: getLatestVersion, data: latestVersion } = useRequest(SystemApi.getStableVersion, {
immediate: false,
force: true
});
const { send: getLatestDevVersion, data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
immediate: false,
force: true
});
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const STABLE_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const uploadURL = window.location.origin + '/system/upload';
const connected = latestVersion && latestDevVersion;
const getVersions = useCallback(async () => {
await getLatestVersion();
await getLatestDevVersion();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (open) {
void getVersions();
}
}, [getVersions, open]);
const getBinURL = (v: string) => 'EMS-ESP-' + v.replaceAll('.', '_') + '-' + platform.replaceAll('-', '_') + '.bin';
return (
<Dialog sx={dialogStyle} open={open} onClose={onClose}>
<DialogTitle>{LL.VERSION_CHECK(1)}</DialogTitle>
<DialogContent dividers>
<MessageBox my={0} level="info" message={LL.VERSION_ON() + ' ' + version + ' (' + platform + ')'} />
{latestVersion && (
<Box mt={2} mb={2}>
{LL.THE_LATEST()}&nbsp;<b>{LL.OFFICIAL()}</b>&nbsp;{LL.RELEASE_IS()}&nbsp;<b>{latestVersion}</b>
&nbsp;(
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link
target="_blank"
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion)}
color="primary"
>
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
{latestDevVersion && (
<Box mt={2} mb={2}>
{LL.THE_LATEST()}&nbsp;<b>{LL.DEVELOPMENT()}</b>&nbsp;{LL.RELEASE_IS()}&nbsp;
<b>{latestDevVersion}</b>
&nbsp;(
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion)} color="primary">
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
{connected && (
<Box color="warning.main" mt={2}>
<Typography variant="body2">
{LL.USE()}&nbsp;
<Link href={uploadURL} color="primary">
{LL.UPLOAD()}
</Link>
&nbsp;{LL.SYSTEM_APPLY_FIRMWARE()}
</Typography>
</Box>
)}
{!connected && <MessageBox my={2} level="warning" message="No internet connection" />}
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onClose} color="secondary">
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
);
};
export default SystemStatusVersionDialog;

View File

@@ -1,308 +0,0 @@
import DownloadIcon from '@mui/icons-material/GetApp';
import { Typography, Button, Box, Link } from '@mui/material';
import { useRequest } from 'alova';
import { useState, type FC } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from './RestartMonitor';
import * as SystemApi from 'api/system';
import { FormLoader, SectionContent, SingleUpload, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
const UploadDownload: FC = () => {
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>();
const [md5, setMd5] = useState<string>();
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
immediate: false
});
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), {
immediate: false
});
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
immediate: false
});
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
immediate: false
});
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data) => EMSESP.API(data), {
immediate: false
});
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true });
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
immediate: true,
force: true
});
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
immediate: true,
force: true
});
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const STABLE_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const getBinURL = (v: string) =>
'EMS-ESP-' + v.replaceAll('.', '_') + '-' + data.esp_platform.replaceAll('-', '_') + '.bin';
const {
loading: isUploading,
uploading: progress,
send: sendUpload,
onSuccess: onSuccessUpload,
abort: cancelUpload
} = useRequest(SystemApi.uploadFile, {
immediate: false,
force: true
});
onSuccessUpload(({ data }: any) => {
if (data) {
setMd5(data.md5);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
} else {
setRestarting(true);
}
});
const startUpload = async (files: File[]) => {
await sendUpload(files[0]).catch((err) => {
if (err.message === 'The user aborted a request') {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else if (err.message === 'Network Error') {
toast.warning('Invalid file extension or incompatible bin file');
} else {
toast.error(err.message);
}
});
};
const saveFile = (json: any, endpoint: string) => {
const anchor = document.createElement('a');
anchor.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
type: 'text/plain'
})
);
anchor.download = 'emsesp_' + endpoint;
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
onSuccessGetSettings((event) => {
saveFile(event.data, 'settings.json');
});
onSuccessGetCustomizations((event) => {
saveFile(event.data, 'customizations.json');
});
onSuccessGetEntities((event) => {
saveFile(event.data, 'entities.json');
});
onSuccessGetSchedule((event) => {
saveFile(event.data, 'schedule.json');
});
onGetAPI((event) => {
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt');
});
const downloadSettings = async () => {
await getSettings().catch((error) => {
toast.error(error.message);
});
};
const downloadCustomizations = async () => {
await getCustomizations().catch((error) => {
toast.error(error.message);
});
};
const downloadEntities = async () => {
await getEntities().catch((error) => {
toast.error(error.message);
});
};
const downloadSchedule = async () => {
await getSchedule()
.catch((error) => {
toast.error(error.message);
})
.finally(() => {
toast.info(LL.DOWNLOAD_SUCCESSFUL());
});
};
const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error) => {
toast.error(error.message);
});
};
useLayoutTitle(LL.UPLOAD_DOWNLOAD());
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
{LL.EMS_ESP_VER()}
</Typography>
<Box p={2} border="2px solid grey" borderRadius={2}>
{LL.VERSION_ON() + ' '}
<b>{data.emsesp_version}</b>&nbsp;({data.esp_platform})
{latestVersion && (
<Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.OFFICIAL()}&nbsp;{LL.RELEASE_IS()}&nbsp;<b>{latestVersion}</b>
&nbsp;(
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link
target="_blank"
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion)}
color="primary"
>
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
{latestDevVersion && (
<Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.DEVELOPMENT()}&nbsp;{LL.RELEASE_IS()}&nbsp;
<b>{latestDevVersion}</b>
&nbsp;(
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion)} color="primary">
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
</Box>
<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()}
<br />
<br />
{LL.RESTART_TEXT(1)}.
</Typography>
</Box>
{md5 && (
<Box mb={2}>
<Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box>
)}
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
{!isUploading && (
<>
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
{LL.HELP_INFORMATION_4()}
</Typography>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'info')}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'allvalues')}
>
All Values
</Button>
</Box>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadSettings}
>
{LL.SETTINGS_OF('')}
</Button>
</Box>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}
</Typography>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadCustomizations}
>
{LL.CUSTOMIZATIONS()}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadEntities}
>
{LL.CUSTOM_ENTITIES(0)}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SCHEDULE_TEXT()}
</Typography>
</Box>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadSchedule}
>
{LL.SCHEDULE(0)}
</Button>
</Box>
</>
)}
</>
);
};
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>;
};
export default UploadDownload;

View File

@@ -0,0 +1,224 @@
import DownloadIcon from '@mui/icons-material/GetApp';
import { Typography, Button, Box } from '@mui/material';
import { useRequest } from 'alova';
import { useState, type FC } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from './RestartMonitor';
import * as SystemApi from 'api/system';
import { SectionContent, SingleUpload } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
const UploadFileForm: FC = () => {
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>();
const [md5, setMd5] = useState<string>();
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
immediate: false
});
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), {
immediate: false
});
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
immediate: false
});
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
immediate: false
});
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data) => EMSESP.API(data), {
immediate: false
});
const {
loading: isUploading,
uploading: progress,
send: sendUpload,
onSuccess: onSuccessUpload,
abort: cancelUpload
} = useRequest(SystemApi.uploadFile, {
immediate: false,
force: true
});
onSuccessUpload(({ data }: any) => {
if (data) {
setMd5(data.md5);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
} else {
setRestarting(true);
}
});
const startUpload = async (files: File[]) => {
await sendUpload(files[0]).catch((err) => {
if (err.message === 'The user aborted a request') {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else if (err.message === 'Network Error') {
toast.warning('Invalid file extension or incompatible bin file');
} else {
toast.error(err.message);
}
});
};
const saveFile = (json: any, endpoint: string) => {
const anchor = document.createElement('a');
anchor.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
type: 'text/plain'
})
);
anchor.download = 'emsesp_' + endpoint;
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
onSuccessGetSettings((event) => {
saveFile(event.data, 'settings.json');
});
onSuccessGetCustomizations((event) => {
saveFile(event.data, 'customizations.json');
});
onSuccessGetEntities((event) => {
saveFile(event.data, 'entities.json');
});
onSuccessGetSchedule((event) => {
saveFile(event.data, 'schedule.json');
});
onGetAPI((event) => {
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt');
});
const downloadSettings = async () => {
await getSettings().catch((error) => {
toast.error(error.message);
});
};
const downloadCustomizations = async () => {
await getCustomizations().catch((error) => {
toast.error(error.message);
});
};
const downloadEntities = async () => {
await getEntities().catch((error) => {
toast.error(error.message);
});
};
const downloadSchedule = async () => {
await getSchedule()
.catch((error) => {
toast.error(error.message);
})
.finally(() => {
toast.info(LL.DOWNLOAD_SUCCESSFUL());
});
};
const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error) => {
toast.error(error.message);
});
};
const content = () => (
<>
<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()}
<br />
<br />
{LL.RESTART_TEXT(1)}.
</Typography>
</Box>
{md5 && (
<Box mb={2}>
<Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box>
)}
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
{!isUploading && (
<>
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}&nbsp;{LL.SUPPORT_INFORMATION(1)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
{LL.HELP_INFORMATION_4()}
</Typography>
</Box>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'info')}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'allvalues')}
>
All Values
</Button>
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}&nbsp;{LL.SETTINGS(1)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSettings}>
{LL.SETTINGS_OF('')}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadCustomizations}>
{LL.CUSTOMIZATIONS()}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadEntities}
>
{LL.CUSTOM_ENTITIES(0)}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SCHEDULE_TEXT()}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSchedule}>
{LL.SCHEDULE(0)}
</Button>
</>
)}
</>
);
return (
<SectionContent title={LL.UPLOAD_DOWNLOAD()} titleGutter>
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};
export default UploadFileForm;

View File

@@ -12,6 +12,7 @@ const de: Translation = {
USERNAME: 'Nutzername', USERNAME: 'Nutzername',
PASSWORD: 'Passwort', PASSWORD: 'Passwort',
SU_PASSWORD: 'su Passwort', SU_PASSWORD: 'su Passwort',
DASHBOARD: 'Kontrollzentrum',
SETTINGS_OF: '{0} Einstellungen', SETTINGS_OF: '{0} Einstellungen',
HELP_OF: '{0} Hilfe', HELP_OF: '{0} Hilfe',
LOGGED_IN: 'Eingeloggt als {name}', LOGGED_IN: 'Eingeloggt als {name}',
@@ -36,6 +37,8 @@ const de: Translation = {
BRAND: 'Marke', BRAND: 'Marke',
ENTITY_NAME: 'Entitätsname', ENTITY_NAME: 'Entitätsname',
VALUE: '{{Wert|wert}}', VALUE: '{{Wert|wert}}',
DEVICE_DATA: 'Gerätedaten',
SENSOR_DATA: 'Sensordaten',
DEVICES: 'Geräte', DEVICES: 'Geräte',
SENSORS: 'Sensoren', SENSORS: 'Sensoren',
RUN_COMMAND: 'Befehl ausführen', RUN_COMMAND: 'Befehl ausführen',
@@ -80,6 +83,7 @@ const de: Translation = {
FAIL: 'FEHLER', FAIL: 'FEHLER',
QUALITY: 'QUALITÄT', QUALITY: 'QUALITÄT',
SCAN_DEVICES: 'Nach neuen Geräten suchen', SCAN_DEVICES: 'Nach neuen Geräten suchen',
EMS_BUS_STATUS_TITLE: 'EMS-Bus- und Aktivitätsstatus',
SCAN: 'Suche', SCAN: 'Suche',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS-Telegramme empfangen (Rx)', 'EMS-Telegramme empfangen (Rx)',
@@ -159,7 +163,9 @@ const de: Translation = {
OPTIONS: 'Optionen', OPTIONS: 'Optionen',
NAME: 'Name', NAME: 'Name',
CUSTOMIZATIONS_RESET: 'Möchten Sie wirklich alle Anpassungen entfernen, einschließlich der benutzerdefinierten Einstellungen der Temperatur- und Analogsensoren?', CUSTOMIZATIONS_RESET: 'Möchten Sie wirklich alle Anpassungen entfernen, einschließlich der benutzerdefinierten Einstellungen der Temperatur- und Analogsensoren?',
DEVICE_ENTITIES: 'Geräteentitäten',
SUPPORT_INFORMATION: 'Unterstützende Informationen', SUPPORT_INFORMATION: 'Unterstützende Informationen',
CLICK_HERE: 'Hier klicken',
HELP_INFORMATION_1: 'EMS-ESP Konfigurationsanweisungen und mehr finden Sie im Online-Wiki', 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_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_3: 'Um neue Funktionen anzufragen oder Fehler zu melden, eröffnen Sie ein Issue auf Github',
@@ -175,11 +181,13 @@ const de: Translation = {
STATUS_OF: '{0} Status', STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Hoch-/Herunterladen', UPLOAD_DOWNLOAD: 'Hoch-/Herunterladen',
VERSION_ON: 'Sie verwenden derzeit', VERSION_ON: 'Sie verwenden derzeit',
SYSTEM_APPLY_FIRMWARE: 'um die neue Firmware anzuwenden',
CLOSE: 'Schließen', CLOSE: 'Schließen',
USE: 'Verwenden Sie', USE: 'Verwenden Sie',
FACTORY_RESET: 'Werkseinstellung', FACTORY_RESET: 'Werkseinstellung',
SYSTEM_FACTORY_TEXT: 'EMS-ESP wurde auf Werkseinstellung gesetzt und startet als Zugangspunkt neu', 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?', SYSTEM_FACTORY_TEXT_DIALOG: 'Sind Sie sicher alle Einstellungen auf Werkseinstellung zu setzen?',
VERSION_CHECK: 'Versionsprüfung',
THE_LATEST: 'Die neueste', THE_LATEST: 'Die neueste',
OFFICIAL: 'offizielle', OFFICIAL: 'offizielle',
DEVELOPMENT: 'Entwicklungs', DEVELOPMENT: 'Entwicklungs',
@@ -236,7 +244,6 @@ const de: Translation = {
MQTT_INT_THERMOSTATS: 'Thermostate', MQTT_INT_THERMOSTATS: 'Thermostate',
MQTT_INT_SOLAR: 'Solarmodule', MQTT_INT_SOLAR: 'Solarmodule',
MQTT_INT_MIXER: 'Mischermodule', MQTT_INT_MIXER: 'Mischermodule',
MQTT_INT_WATER: 'Warmwassermodule',
MQTT_QUEUE: 'MQTT Queue', MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Standard', DEFAULT: 'Standard',
MQTT_ENTITY_FORMAT: 'Entitäts-ID Format', MQTT_ENTITY_FORMAT: 'Entitäts-ID Format',
@@ -322,9 +329,7 @@ const de: Translation = {
ACTIVEHIGH: 'Aktiv Positiv', ACTIVEHIGH: 'Aktiv Positiv',
ACTIVELOW: 'Aktiv Negativ', ACTIVELOW: 'Aktiv Negativ',
UNCHANGED: 'Unverändert', UNCHANGED: 'Unverändert',
ALWAYS: 'Immer', ALWAYS: 'Immer'
ACTIVITY: 'Activity', // TODO translate
CONFIGURE: 'Configure {0}' // TODO translate
}; };
export default de; export default de;

View File

@@ -12,6 +12,7 @@ const en: Translation = {
USERNAME: 'Username', USERNAME: 'Username',
PASSWORD: 'Password', PASSWORD: 'Password',
SU_PASSWORD: 'su Password', SU_PASSWORD: 'su Password',
DASHBOARD: 'Dashboard',
SETTINGS_OF: '{0} Settings', SETTINGS_OF: '{0} Settings',
HELP_OF: '{0} Help', HELP_OF: '{0} Help',
LOGGED_IN: 'Logged in as {name}', LOGGED_IN: 'Logged in as {name}',
@@ -36,6 +37,8 @@ const en: Translation = {
BRAND: 'Brand', BRAND: 'Brand',
ENTITY_NAME: 'Entity Name', ENTITY_NAME: 'Entity Name',
VALUE: '{{Value|value}}', VALUE: '{{Value|value}}',
DEVICE_DATA: 'Device Data',
SENSOR_DATA: 'Sensor Data',
DEVICES: 'Devices', DEVICES: 'Devices',
SENSORS: 'Sensors', SENSORS: 'Sensors',
RUN_COMMAND: 'Call Command', RUN_COMMAND: 'Call Command',
@@ -80,6 +83,7 @@ const en: Translation = {
FAIL: 'FAIL', FAIL: 'FAIL',
QUALITY: 'QUALITY', QUALITY: 'QUALITY',
SCAN_DEVICES: 'Scan for new devices', SCAN_DEVICES: 'Scan for new devices',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activity Status',
SCAN: 'Scan', SCAN: 'Scan',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS Telegrams Received (Rx)', 'EMS Telegrams Received (Rx)',
@@ -154,16 +158,18 @@ const en: Translation = {
CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API', CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API',
CUSTOMIZATIONS_HELP_5: 'hide from Dashboard', CUSTOMIZATIONS_HELP_5: 'hide from Dashboard',
CUSTOMIZATIONS_HELP_6: 'remove from memory', CUSTOMIZATIONS_HELP_6: 'remove from memory',
SELECT_DEVICE: 'select a device', SELECT_DEVICE: 'Select a device',
SET_ALL: 'set all', SET_ALL: 'set all',
OPTIONS: 'Options', OPTIONS: 'Options',
NAME: 'Name', NAME: 'Name',
CUSTOMIZATIONS_RESET: 'Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors?', CUSTOMIZATIONS_RESET: 'Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors?',
DEVICE_ENTITIES: 'Device Entities',
SUPPORT_INFORMATION: 'Support Information', 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_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_2: 'For live community chat join our Discord server',
HELP_INFORMATION_3: 'To request a feature or report a bug', HELP_INFORMATION_3: 'To request a feature or report a bug',
HELP_INFORMATION_4: 'Download and attach your support information for a faster response when reporting an issue', HELP_INFORMATION_4: 'Remember to download and attach your support 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!', HELP_INFORMATION_5: 'EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!',
UPLOAD: 'Upload', UPLOAD: 'Upload',
DOWNLOAD: '{{D|d|d}}ownload', DOWNLOAD: '{{D|d|d}}ownload',
@@ -175,17 +181,20 @@ const en: Translation = {
STATUS_OF: '{0} Status', STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upload/Download', UPLOAD_DOWNLOAD: 'Upload/Download',
VERSION_ON: 'You are currently on version', VERSION_ON: 'You are currently on version',
SYSTEM_APPLY_FIRMWARE: 'to apply the new firmware',
CLOSE: 'Close', CLOSE: 'Close',
USE: 'Use', USE: 'Use',
FACTORY_RESET: 'Factory Reset', FACTORY_RESET: 'Factory Reset',
SYSTEM_FACTORY_TEXT: 'Device has been factory reset and will now restart', SYSTEM_FACTORY_TEXT: 'Device has been factory reset and will now restart',
SYSTEM_FACTORY_TEXT_DIALOG: 'Are you sure you want to reset EMS-ESP to its factory defaults?', SYSTEM_FACTORY_TEXT_DIALOG: 'Are you sure you want to reset EMS-ESP to its factory defaults?',
VERSION_CHECK: 'Version Check',
THE_LATEST: 'The latest', THE_LATEST: 'The latest',
OFFICIAL: 'official', OFFICIAL: 'official',
DEVELOPMENT: 'development', DEVELOPMENT: 'development',
RELEASE_IS: 'release is', RELEASE_IS: 'release is',
RELEASE_NOTES: 'release notes', RELEASE_NOTES: 'release notes',
EMS_ESP_VER: 'EMS-ESP Version', EMS_ESP_VER: 'EMS-ESP Version',
PLATFORM: 'Device (Platform / SDK)',
UPTIME: 'System Uptime', UPTIME: 'System Uptime',
HEAP: 'Heap (Free / Max Alloc)', HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)', PSRAM: 'PSRAM (Size / Free)',
@@ -236,7 +245,6 @@ const en: Translation = {
MQTT_INT_THERMOSTATS: 'Thermostats', MQTT_INT_THERMOSTATS: 'Thermostats',
MQTT_INT_SOLAR: 'Solar Modules', MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules', MQTT_INT_MIXER: 'Mixer Modules',
MQTT_INT_WATER: 'Water Modules',
MQTT_QUEUE: 'MQTT Queue', MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Default', DEFAULT: 'Default',
MQTT_ENTITY_FORMAT: 'Entity ID format', MQTT_ENTITY_FORMAT: 'Entity ID format',
@@ -322,9 +330,7 @@ const en: Translation = {
ACTIVEHIGH: 'Active High', ACTIVEHIGH: 'Active High',
ACTIVELOW: 'Active Low', ACTIVELOW: 'Active Low',
UNCHANGED: 'Unchanged', UNCHANGED: 'Unchanged',
ALWAYS: 'Always', ALWAYS: 'Always'
ACTIVITY: 'Activity',
CONFIGURE: 'Configure {0}'
}; };
export default en; export default en;

View File

@@ -12,6 +12,7 @@ const fr: Translation = {
USERNAME: 'Nom d\'utilisateur', USERNAME: 'Nom d\'utilisateur',
PASSWORD: 'Mot de passe', PASSWORD: 'Mot de passe',
SU_PASSWORD: 'Mot de passe su', SU_PASSWORD: 'Mot de passe su',
DASHBOARD: 'Tableau de bord',
SETTINGS_OF: 'Paramètres {0}', SETTINGS_OF: 'Paramètres {0}',
HELP_OF: 'Aide {0}', HELP_OF: 'Aide {0}',
LOGGED_IN: 'Connecté en tant que {name}', LOGGED_IN: 'Connecté en tant que {name}',
@@ -36,6 +37,8 @@ const fr: Translation = {
BRAND: 'Marque', BRAND: 'Marque',
ENTITY_NAME: 'Nom de l\'entité', ENTITY_NAME: 'Nom de l\'entité',
VALUE: 'Valeur', VALUE: 'Valeur',
DEVICE_DATA: 'Données des appareils',
SENSOR_DATA: 'Données des capteurs',
DEVICES: 'Appareils', DEVICES: 'Appareils',
SENSORS: 'Capteurs', SENSORS: 'Capteurs',
RUN_COMMAND: 'Lancer une commande', RUN_COMMAND: 'Lancer une commande',
@@ -80,6 +83,7 @@ const fr: Translation = {
FAIL: 'ÉCHEC', FAIL: 'ÉCHEC',
QUALITY: 'QUALITÉ', QUALITY: 'QUALITÉ',
SCAN_DEVICES: 'Rechercher de nouveaux appareils', SCAN_DEVICES: 'Rechercher de nouveaux appareils',
EMS_BUS_STATUS_TITLE: 'Statut du bus et de l\'activité EMS',
SCAN: 'Scan', SCAN: 'Scan',
STATUS_NAMES: [ STATUS_NAMES: [
'Télégrammes EMS reçus (Rx)', 'Télégrammes EMS reçus (Rx)',
@@ -159,7 +163,9 @@ const fr: Translation = {
OPTIONS: 'Options', OPTIONS: 'Options',
NAME: 'Nom', 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 ?', 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',
SUPPORT_INFORMATION: 'Information de support', 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_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_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_3: 'Pour demander une fonctionnalité ou signaler un problème',
@@ -175,11 +181,13 @@ const fr: Translation = {
STATUS_OF: 'Statut {0}', STATUS_OF: 'Statut {0}',
UPLOAD_DOWNLOAD: 'Upload/Download', UPLOAD_DOWNLOAD: 'Upload/Download',
VERSION_ON: 'You are currently on', // TODO translate VERSION_ON: 'You are currently on', // TODO translate
SYSTEM_APPLY_FIRMWARE: 'pour appliquer le nouveau firmware',
CLOSE: 'Fermer', CLOSE: 'Fermer',
USE: 'Utiliser', USE: 'Utiliser',
FACTORY_RESET: 'Réinitialisation', FACTORY_RESET: 'Réinitialisation',
SYSTEM_FACTORY_TEXT: 'L\'appareil a été réinitialisé et va maintenant redémarrer', 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 ?', 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', THE_LATEST: 'La dernière',
OFFICIAL: 'officielle', OFFICIAL: 'officielle',
DEVELOPMENT: 'développement', DEVELOPMENT: 'développement',
@@ -236,7 +244,6 @@ const fr: Translation = {
MQTT_INT_THERMOSTATS: 'Thermostats', MQTT_INT_THERMOSTATS: 'Thermostats',
MQTT_INT_SOLAR: 'Modules solaires', MQTT_INT_SOLAR: 'Modules solaires',
MQTT_INT_MIXER: 'Modules mélangeurs', MQTT_INT_MIXER: 'Modules mélangeurs',
MQTT_INT_WATER: 'Modules eau',
MQTT_QUEUE: 'Queue MQTT', MQTT_QUEUE: 'Queue MQTT',
DEFAULT: 'Défaut', DEFAULT: 'Défaut',
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
@@ -322,9 +329,7 @@ const fr: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always', // TODO translate ALWAYS: 'Always' // TODO translate
ACTIVITY: 'Activity', // TODO translate
CONFIGURE: 'Configure {0}' // TODO translate
}; };
export default fr; export default fr;

View File

@@ -12,6 +12,7 @@ const it: Translation = {
USERNAME: 'Nome Utente', USERNAME: 'Nome Utente',
PASSWORD: 'Password', PASSWORD: 'Password',
SU_PASSWORD: 'su Password', SU_PASSWORD: 'su Password',
DASHBOARD: 'Pannello di Controllo',
SETTINGS_OF: 'Impostazioni {0}', SETTINGS_OF: 'Impostazioni {0}',
HELP_OF: '{0} Aiuto', HELP_OF: '{0} Aiuto',
LOGGED_IN: 'Registrato come {name}', LOGGED_IN: 'Registrato come {name}',
@@ -36,6 +37,8 @@ const it: Translation = {
BRAND: 'Marca', BRAND: 'Marca',
ENTITY_NAME: 'Nome Entità', ENTITY_NAME: 'Nome Entità',
VALUE: '{{Valore|valore}}', VALUE: '{{Valore|valore}}',
DEVICE_DATA: 'Device Data',
SENSOR_DATA: 'Sensor Data',
DEVICES: 'Dispositivi', DEVICES: 'Dispositivi',
SENSORS: 'Sensori', SENSORS: 'Sensori',
RUN_COMMAND: 'Esegui', RUN_COMMAND: 'Esegui',
@@ -48,6 +51,7 @@ const it: Translation = {
REMOVE: 'Elimina', REMOVE: 'Elimina',
PROBLEM_UPDATING: 'Problema aggiornamento', PROBLEM_UPDATING: 'Problema aggiornamento',
PROBLEM_LOADING: 'Problema caricamento', PROBLEM_LOADING: 'Problema caricamento',
ACCESS_DENIED: 'Accesso Negato',
ANALOG_SENSOR: 'Sensore Analogico', ANALOG_SENSOR: 'Sensore Analogico',
ANALOG_SENSORS: 'Sensori Analogici', ANALOG_SENSORS: 'Sensori Analogici',
SETTINGS: 'Settings', SETTINGS: 'Settings',
@@ -67,6 +71,7 @@ const it: Translation = {
TEMP_SENSOR: 'Sensore Temperatura', TEMP_SENSOR: 'Sensore Temperatura',
TEMP_SENSORS: 'Sensori Temperatura', TEMP_SENSORS: 'Sensori Temperatura',
WRITE_CMD_SENT: 'Scrittura comando inviata', WRITE_CMD_SENT: 'Scrittura comando inviata',
WRITE_CMD_FAILED: 'Scittura comando fallita',
EMS_BUS_WARNING: 'EMS bus disconnesso. Se questo avvertimento persiste dopo alcuni secondi prego verificare impostazioni scheda', EMS_BUS_WARNING: 'EMS bus disconnesso. Se questo avvertimento persiste dopo alcuni secondi prego verificare impostazioni scheda',
EMS_BUS_SCANNING: 'Scansione dispositivi EMS ...', EMS_BUS_SCANNING: 'Scansione dispositivi EMS ...',
CONNECTED: 'Connesso', CONNECTED: 'Connesso',
@@ -80,6 +85,7 @@ const it: Translation = {
FAIL: 'FALLITO', FAIL: 'FALLITO',
QUALITY: 'QUALITÂ', QUALITY: 'QUALITÂ',
SCAN_DEVICES: 'Scansione per nuovi dispositivi', SCAN_DEVICES: 'Scansione per nuovi dispositivi',
EMS_BUS_STATUS_TITLE: 'Bus EMS & Stato Attività',
SCAN: 'Scansione', SCAN: 'Scansione',
STATUS_NAMES: [ STATUS_NAMES: [
'Telegrammi EMS Ricevuti (Rx)', 'Telegrammi EMS Ricevuti (Rx)',
@@ -159,7 +165,9 @@ const it: Translation = {
OPTIONS: 'Opzioni', OPTIONS: 'Opzioni',
NAME: 'Nome', NAME: 'Nome',
CUSTOMIZATIONS_RESET: 'Sei sicuro di voler rimuovere tutte le personalizzazioni incluse le impostazioni personalizzate dei sensori di temperatura e analogici?', CUSTOMIZATIONS_RESET: 'Sei sicuro di voler rimuovere tutte le personalizzazioni incluse le impostazioni personalizzate dei sensori di temperatura e analogici?',
DEVICE_ENTITIES: 'Entità Dispositivo',
SUPPORT_INFORMATION: 'Informazioni di Supporto', SUPPORT_INFORMATION: 'Informazioni di Supporto',
CLICK_HERE: 'Clicca qui',
HELP_INFORMATION_1: 'Visita il wiki online per ottenere istruzioni su come configurare EMS-ESP', HELP_INFORMATION_1: 'Visita il wiki online per ottenere istruzioni su come configurare EMS-ESP',
HELP_INFORMATION_2: 'Per la chat della community dal vivo unisciti al nostro server Discord', HELP_INFORMATION_2: 'Per la chat della community dal vivo unisciti al nostro server Discord',
HELP_INFORMATION_3: 'Per richiedere una funzionalità o segnalare un errore', HELP_INFORMATION_3: 'Per richiedere una funzionalità o segnalare un errore',
@@ -175,11 +183,13 @@ const it: Translation = {
STATUS_OF: 'Stato {0}', STATUS_OF: 'Stato {0}',
UPLOAD_DOWNLOAD: 'Caricamento/Scaricamento', UPLOAD_DOWNLOAD: 'Caricamento/Scaricamento',
VERSION_ON: 'Attualmente stai eseguendo la versione', VERSION_ON: 'Attualmente stai eseguendo la versione',
SYSTEM_APPLY_FIRMWARE: 'per applicare il nuovo firmware',
CLOSE: 'Chiudere', CLOSE: 'Chiudere',
USE: 'Usa', USE: 'Usa',
FACTORY_RESET: 'Impostazioni di fabbrica', FACTORY_RESET: 'Impostazioni di fabbrica',
SYSTEM_FACTORY_TEXT: 'Il dispositivo è stato ripristinato alle impostazioni di fabbrica e ora verrà riavviato', SYSTEM_FACTORY_TEXT: 'Il dispositivo è stato ripristinato alle impostazioni di fabbrica e ora verrà riavviato',
SYSTEM_FACTORY_TEXT_DIALOG: 'Sei sicuro di voler ripristinare il dispositivo alle impostazioni di fabbrica??', SYSTEM_FACTORY_TEXT_DIALOG: 'Sei sicuro di voler ripristinare il dispositivo alle impostazioni di fabbrica??',
VERSION_CHECK: 'Verifica Versione',
THE_LATEST: 'Ultima', THE_LATEST: 'Ultima',
OFFICIAL: 'ufficiale', OFFICIAL: 'ufficiale',
DEVELOPMENT: 'sviluppo', DEVELOPMENT: 'sviluppo',
@@ -236,7 +246,6 @@ const it: Translation = {
MQTT_INT_THERMOSTATS: 'Termostati', MQTT_INT_THERMOSTATS: 'Termostati',
MQTT_INT_SOLAR: 'Moduli solari', MQTT_INT_SOLAR: 'Moduli solari',
MQTT_INT_MIXER: 'Moduli Mixer', MQTT_INT_MIXER: 'Moduli Mixer',
MQTT_INT_WATER: 'Moduli Acqua',
MQTT_QUEUE: 'Coda MQTT', MQTT_QUEUE: 'Coda MQTT',
DEFAULT: 'Predefinito', DEFAULT: 'Predefinito',
MQTT_ENTITY_FORMAT: 'Formato ID entità', MQTT_ENTITY_FORMAT: 'Formato ID entità',
@@ -322,9 +331,7 @@ const it: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always', // TODO translate ALWAYS: 'Always' // TODO translate
ACTIVITY: 'Activity', // TODO translate
CONFIGURE: 'Configure {0}' // TODO translate
}; };
export default it; export default it;

View File

@@ -12,6 +12,7 @@ const nl: Translation = {
USERNAME: 'Gebruikersnaam', USERNAME: 'Gebruikersnaam',
PASSWORD: 'Wachtwoord', PASSWORD: 'Wachtwoord',
SU_PASSWORD: 'su Wachtwoord', SU_PASSWORD: 'su Wachtwoord',
DASHBOARD: 'Dashboard',
SETTINGS_OF: '{0} Instellingen', SETTINGS_OF: '{0} Instellingen',
HELP_OF: '{0} Help', HELP_OF: '{0} Help',
LOGGED_IN: 'Ingelogd als {name}', LOGGED_IN: 'Ingelogd als {name}',
@@ -36,6 +37,8 @@ const nl: Translation = {
BRAND: 'Merk', BRAND: 'Merk',
ENTITY_NAME: 'Entiteit', ENTITY_NAME: 'Entiteit',
VALUE: '{{Waarde|waarde}}', VALUE: '{{Waarde|waarde}}',
SENSOR_DATA: 'Sensor data',
DEVICE_DATA: 'Apparaat data',
DEVICES: 'Apparaten', DEVICES: 'Apparaten',
SENSORS: 'Sensoren', SENSORS: 'Sensoren',
RUN_COMMAND: 'Call commando', RUN_COMMAND: 'Call commando',
@@ -80,6 +83,7 @@ const nl: Translation = {
FAIL: 'MISLUKT', FAIL: 'MISLUKT',
QUALITY: 'QUALITEIT', QUALITY: 'QUALITEIT',
SCAN_DEVICES: 'Scannen naar nieuwe apparaten', SCAN_DEVICES: 'Scannen naar nieuwe apparaten',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activiteitenstatus',
SCAN: 'Scan', SCAN: 'Scan',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS Telegrammen ontvangen (Rx)', 'EMS Telegrammen ontvangen (Rx)',
@@ -122,7 +126,7 @@ const nl: Translation = {
BYPASS_TOKEN: 'API Access Token authenticatie uitschakelen', BYPASS_TOKEN: 'API Access Token authenticatie uitschakelen',
READONLY: 'Activeer read-only modus (blokkeert alle outgaande EMS Tx schrijf commandos)', READONLY: 'Activeer read-only modus (blokkeert alle outgaande EMS Tx schrijf commandos)',
UNDERCLOCK_CPU: 'Underclock CPU snelheid', UNDERCLOCK_CPU: 'Underclock CPU snelheid',
HEATINGOFF: 'Start ketel met geforceerde verwarming uit', HEATINGOFF: 'Start boiler with forced heating off', // TODO translate
ENABLE_SHOWER_TIMER: 'Activeer Douche Timer (tijdmeting)', ENABLE_SHOWER_TIMER: 'Activeer Douche Timer (tijdmeting)',
ENABLE_SHOWER_ALERT: 'Activeer Douchemelding', ENABLE_SHOWER_ALERT: 'Activeer Douchemelding',
TRIGGER_TIME: 'Trigger tijd', TRIGGER_TIME: 'Trigger tijd',
@@ -159,7 +163,9 @@ const nl: Translation = {
OPTIONS: 'Opties', OPTIONS: 'Opties',
NAME: 'Naam', NAME: 'Naam',
CUSTOMIZATIONS_RESET: 'Weet je zeker dat je alle custom aanpassingen wilt verwijderen inclusief de custom instellingen voor analoge temperatuursensoren?', CUSTOMIZATIONS_RESET: 'Weet je zeker dat je alle custom aanpassingen wilt verwijderen inclusief de custom instellingen voor analoge temperatuursensoren?',
DEVICE_ENTITIES: 'Apparaat Entiteiten',
SUPPORT_INFORMATION: 'Support Informatie', 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_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_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_3: 'Om een nieuwe feature te vragen of een bug te rapporteren',
@@ -175,11 +181,13 @@ const nl: Translation = {
STATUS_OF: '{0} Status', STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upload/Download', UPLOAD_DOWNLOAD: 'Upload/Download',
VERSION_ON: 'U bevindt zich momenteel op versie', VERSION_ON: 'U bevindt zich momenteel op versie',
SYSTEM_APPLY_FIRMWARE: 'om de nieuwe firmware te activeren',
CLOSE: 'Sluiten', CLOSE: 'Sluiten',
USE: 'Gebruik', USE: 'Gebruik',
FACTORY_RESET: 'Fabrieksinstellingen', FACTORY_RESET: 'Fabrieksinstellingen',
SYSTEM_FACTORY_TEXT: 'Gateway is gereset en start nu weer op met 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?', SYSTEM_FACTORY_TEXT_DIALOG: 'Weet je zeker dat je een reset naar fabrieksinstellingen uit wilt voeren?',
VERSION_CHECK: 'Versie Check',
THE_LATEST: 'De laatste', THE_LATEST: 'De laatste',
OFFICIAL: 'official', OFFICIAL: 'official',
DEVELOPMENT: 'development', DEVELOPMENT: 'development',
@@ -236,7 +244,6 @@ const nl: Translation = {
MQTT_INT_THERMOSTATS: 'Thermostaten', MQTT_INT_THERMOSTATS: 'Thermostaten',
MQTT_INT_SOLAR: 'Solar Modules', MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules', MQTT_INT_MIXER: 'Mixer Modules',
MQTT_INT_WATER: 'Water Modules',
MQTT_QUEUE: 'MQTT Queue', MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Default', DEFAULT: 'Default',
MQTT_ENTITY_FORMAT: 'Entity ID formaat', MQTT_ENTITY_FORMAT: 'Entity ID formaat',
@@ -273,7 +280,7 @@ const nl: Translation = {
NETWORK_SCANNER: 'Netwerk Scanner', NETWORK_SCANNER: 'Netwerk Scanner',
NETWORK_NO_WIFI: 'Geen WiFi networken gevonden', NETWORK_NO_WIFI: 'Geen WiFi networken gevonden',
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen', NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken', NETWORK_BLANK_BSSID: 'leave blank to use only SSID', // TODO translate
TX_POWER: 'Tx Vermogen', TX_POWER: 'Tx Vermogen',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten', NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
@@ -309,22 +316,20 @@ const nl: Translation = {
SCHEDULE_TIMER_2: 'elke minuut', SCHEDULE_TIMER_2: 'elke minuut',
SCHEDULE_TIMER_3: 'elke huur', SCHEDULE_TIMER_3: 'elke huur',
CUSTOM_ENTITIES: 'Aangepaste Entiteiten', CUSTOM_ENTITIES: 'Aangepaste Entiteiten',
ENTITIES_HELP_1: 'Aangepaste entiteiten ophalen uit de EMS-bus', ENTITIES_HELP_1: 'Aangepaste entiteiten ophalen uit de EMS-bus', // TODO translate
ENTITIES_UPDATED: 'Entiteiten bijgewerkt', ENTITIES_UPDATED: 'Entiteiten bijgewerkt',
WRITEABLE: 'Beschrijfbare', WRITEABLE: 'Beschrijfbare',
SHOWING: 'Tonen', SHOWING: 'Tonen',
SEARCH: 'Zoek', SEARCH: 'Zoek',
CERT: 'TLS rootcertificaat (laat leeg om TLS-insecure)', CERT: 'TLS rootcertificaat (laat leeg om TLS-insecure)', // TODO translate
ENABLE_TLS: 'Activeer TLS', ENABLE_TLS: 'Activeer TLS',
ON: 'Aan', ON: 'On', // TODO translate
OFF: 'Uit', OFF: 'Off', // TODO translate
POLARITY: 'Polariteit', POLARITY: 'Polarity', // TODO translate
ACTIVEHIGH: 'Actiev Hoog', ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Actiev Laag', ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Ongewijzigd', UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Altijd', ALWAYS: 'Always' // TODO translate
ACTIVITY: 'Activiteit',
CONFIGURE: '{0} Configureren'
}; };
export default nl; export default nl;

View File

@@ -12,6 +12,7 @@ const no: Translation = {
USERNAME: 'Brukernavn', USERNAME: 'Brukernavn',
PASSWORD: 'Passord', PASSWORD: 'Passord',
SU_PASSWORD: 'su Passord', SU_PASSWORD: 'su Passord',
DASHBOARD: 'Dashboard',
SETTINGS_OF: '{0} Innstillinger', SETTINGS_OF: '{0} Innstillinger',
HELP_OF: '{0} Hjelp', HELP_OF: '{0} Hjelp',
LOGGED_IN: 'Logget in som {name}', LOGGED_IN: 'Logget in som {name}',
@@ -36,6 +37,8 @@ const no: Translation = {
BRAND: 'Fabrikat', BRAND: 'Fabrikat',
ENTITY_NAME: 'Objektsnavn', ENTITY_NAME: 'Objektsnavn',
VALUE: '{{Verdi|verdi}}', VALUE: '{{Verdi|verdi}}',
DEVICE_DATA: 'Enheterdata',
SENSOR_DATA: 'Sensordata',
DEVICES: 'Enheter', DEVICES: 'Enheter',
SENSORS: 'Sensorer', SENSORS: 'Sensorer',
RUN_COMMAND: 'Kjør kommando', RUN_COMMAND: 'Kjør kommando',
@@ -80,6 +83,7 @@ const no: Translation = {
FAIL: 'MISLYKKET', FAIL: 'MISLYKKET',
QUALITY: 'KVALITET', QUALITY: 'KVALITET',
SCAN_DEVICES: 'Søk etter nye enheter', SCAN_DEVICES: 'Søk etter nye enheter',
EMS_BUS_STATUS_TITLE: 'EMS Buss & Aktivitet Status',
SCAN: 'Søk', SCAN: 'Søk',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS Telegrammer Mottatt (Rx)', 'EMS Telegrammer Mottatt (Rx)',
@@ -159,7 +163,9 @@ const no: Translation = {
OPTIONS: 'Alternativ', OPTIONS: 'Alternativ',
NAME: 'Navn', NAME: 'Navn',
CUSTOMIZATIONS_RESET: 'Er du sikker på att du vil fjerne tilpassninger inkludert innstillinger for Temperatur og Analoge sensorer?', CUSTOMIZATIONS_RESET: 'Er du sikker på att du vil fjerne tilpassninger inkludert innstillinger for Temperatur og Analoge sensorer?',
DEVICE_ENTITIES: 'Enhets objekter',
SUPPORT_INFORMATION: 'Supportinformasjon', SUPPORT_INFORMATION: 'Supportinformasjon',
CLICK_HERE: 'Klikk her',
HELP_INFORMATION_1: 'Besøk wiki for instruksjoner for å konfigurere EMS-ESP', 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_2: 'For community-support besøk vår Discord-server',
HELP_INFORMATION_3: 'For å be om en ny funksjon eller melde feil', HELP_INFORMATION_3: 'For å be om en ny funksjon eller melde feil',
@@ -175,11 +181,13 @@ const no: Translation = {
STATUS_OF: '{0} Status', STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Opp/Nedlasting', UPLOAD_DOWNLOAD: 'Opp/Nedlasting',
VERSION_ON: 'You are currently on', // TODO translate VERSION_ON: 'You are currently on', // TODO translate
SYSTEM_APPLY_FIRMWARE: 'for å aktivere ny firmware',
CLOSE: 'Steng', CLOSE: 'Steng',
USE: 'Bruk', USE: 'Bruk',
FACTORY_RESET: 'Sett tilbake til fabrikkinstilling', FACTORY_RESET: 'Sett tilbake til fabrikkinstilling',
SYSTEM_FACTORY_TEXT: 'Enhet har blitt satt tilbake til fabrikkinstilling og vil restarte', 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?', SYSTEM_FACTORY_TEXT_DIALOG: 'Er du sikker på at du vil resette enheten til fabrikkinstillinger?',
VERSION_CHECK: 'Versjonsjekk',
THE_LATEST: 'Den nyeste', THE_LATEST: 'Den nyeste',
OFFICIAL: 'official', OFFICIAL: 'official',
DEVELOPMENT: 'development', DEVELOPMENT: 'development',
@@ -236,7 +244,6 @@ const no: Translation = {
MQTT_INT_THERMOSTATS: 'Termostat', MQTT_INT_THERMOSTATS: 'Termostat',
MQTT_INT_SOLAR: 'Solpaneler', MQTT_INT_SOLAR: 'Solpaneler',
MQTT_INT_MIXER: 'Blandeventil', MQTT_INT_MIXER: 'Blandeventil',
MQTT_INT_WATER: 'Water Modules', // TODO translate
MQTT_QUEUE: 'MQTT Queue', MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Standard', DEFAULT: 'Standard',
MQTT_ENTITY_FORMAT: 'Enhets ID format', MQTT_ENTITY_FORMAT: 'Enhets ID format',
@@ -322,9 +329,7 @@ const no: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always', // TODO translate ALWAYS: 'Always' // TODO translate
ACTIVITY: 'Activity', // TODO translate
CONFIGURE: 'Configure {0}' // TODO translate
}; };
export default no; export default no;

View File

@@ -12,6 +12,7 @@ const pl: BaseTranslation = {
USERNAME: '{{Użytkownik|Nazwa użytkownika|}}', USERNAME: '{{Użytkownik|Nazwa użytkownika|}}',
PASSWORD: 'Hasło', PASSWORD: 'Hasło',
SU_PASSWORD: 'Hasło "su"', SU_PASSWORD: 'Hasło "su"',
DASHBOARD: 'Pulpit',
SETTINGS_OF: 'Ustawienia {0}', SETTINGS_OF: 'Ustawienia {0}',
HELP_OF: 'Pomoc {0}', HELP_OF: 'Pomoc {0}',
LOGGED_IN: 'Zalogowano użytkownika {name}.', LOGGED_IN: 'Zalogowano użytkownika {name}.',
@@ -36,6 +37,8 @@ const pl: BaseTranslation = {
VERSION: 'Wersja', VERSION: 'Wersja',
ENTITY_NAME: '{{N|n|}}azwa encji', ENTITY_NAME: '{{N|n|}}azwa encji',
VALUE: '{{W|w|}}artość', VALUE: '{{W|w|}}artość',
DEVICE_DATA: 'Dane z urządzeń',
SENSOR_DATA: 'Dane z czujników',
DEVICES: 'Urządzenia', DEVICES: 'Urządzenia',
SENSORS: 'Czujniki', SENSORS: 'Czujniki',
RUN_COMMAND: 'Wykonaj komendę', RUN_COMMAND: 'Wykonaj komendę',
@@ -80,6 +83,7 @@ const pl: BaseTranslation = {
FAIL: 'Nieudane', FAIL: 'Nieudane',
QUALITY: 'Jakość', QUALITY: 'Jakość',
SCAN_DEVICES: 'Wyszukiwanie nowych urządzeń', SCAN_DEVICES: 'Wyszukiwanie nowych urządzeń',
EMS_BUS_STATUS_TITLE: 'Aktywność',
SCAN: 'Skanuj', SCAN: 'Skanuj',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS, telegramy odebrane (Rx)', 'EMS, telegramy odebrane (Rx)',
@@ -159,7 +163,9 @@ const pl: BaseTranslation = {
OPTIONS: 'Opcje', OPTIONS: 'Opcje',
NAME: '{{Nazwa|nazwa|}}', NAME: '{{Nazwa|nazwa|}}',
CUSTOMIZATIONS_RESET: 'Na pewno chcesz usunąć wszystkie personalizacje łącznie z ustawieniami dla czujników temperatury 1-Wire® i urządzeń podłączonych do EMS-ESP?', CUSTOMIZATIONS_RESET: 'Na pewno 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',
SUPPORT_INFORMATION: '{{I|i|}}nformacj{{e|i|}} o systemie', SUPPORT_INFORMATION: '{{I|i|}}nformacj{{e|i|}} o systemie',
CLICK_HERE: 'Kliknij tu',
HELP_INFORMATION_1: 'Aby uzyskać instrukcje dotyczące konfiguracji EMS-ESP, skorzystaj z wiki w internecie', 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_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_3: 'Aby zaproponować nową funkcjonalność lub zgłosić problem',
@@ -175,11 +181,13 @@ const pl: BaseTranslation = {
STATUS_OF: 'Status {0}', STATUS_OF: 'Status {0}',
UPLOAD_DOWNLOAD: 'Przesyłanie plików', UPLOAD_DOWNLOAD: 'Przesyłanie plików',
VERSION_ON: 'Aktualnie używasz', VERSION_ON: 'Aktualnie używasz',
SYSTEM_APPLY_FIRMWARE: '',
CLOSE: 'Zamknij', CLOSE: 'Zamknij',
USE: 'Aby zaktualizować firmware skorzystaj z funkcji', USE: 'Aby zaktualizować firmware skorzystaj z funkcji',
FACTORY_RESET: 'Ustawienia fabryczne', FACTORY_RESET: 'Ustawienia fabryczne',
SYSTEM_FACTORY_TEXT: 'Interfejs EMS-ESP został przywrócony do ustawień fabrycznych i zostanie teraz ponownie uruchomiony.', SYSTEM_FACTORY_TEXT: 'Interfejs EMS-ESP został przywrócony do ustawień fabrycznych i zostanie teraz ponownie uruchomiony.',
SYSTEM_FACTORY_TEXT_DIALOG: 'Na pewno chcesz przywrócić ustawienia fabryczne interfejsu EMS-ESP? ', SYSTEM_FACTORY_TEXT_DIALOG: 'Na pewno chcesz przywrócić ustawienia fabryczne interfejsu EMS-ESP? ',
VERSION_CHECK: 'Sprawd{{ź|zanie|}} wersj{{ę|i|}}',
THE_LATEST: 'Najnowsze', THE_LATEST: 'Najnowsze',
OFFICIAL: 'oficjalne', OFFICIAL: 'oficjalne',
DEVELOPMENT: 'testowe', DEVELOPMENT: 'testowe',
@@ -236,7 +244,6 @@ const pl: BaseTranslation = {
MQTT_INT_THERMOSTATS: 'Termostaty', MQTT_INT_THERMOSTATS: 'Termostaty',
MQTT_INT_SOLAR: 'Panele solarne', MQTT_INT_SOLAR: 'Panele solarne',
MQTT_INT_MIXER: 'Mieszacze', MQTT_INT_MIXER: 'Mieszacze',
MQTT_INT_WATER: 'Woda',
MQTT_QUEUE: 'Kolejka MQTT', MQTT_QUEUE: 'Kolejka MQTT',
DEFAULT: '{{Pozostałe|Domyślna|}}', DEFAULT: '{{Pozostałe|Domyślna|}}',
MQTT_ENTITY_FORMAT: 'Format "Entity ID"', MQTT_ENTITY_FORMAT: 'Format "Entity ID"',
@@ -309,7 +316,7 @@ const pl: BaseTranslation = {
SCHEDULE_TIMER_2: 'co minutę', SCHEDULE_TIMER_2: 'co minutę',
SCHEDULE_TIMER_3: 'co godzinę', SCHEDULE_TIMER_3: 'co godzinę',
CUSTOM_ENTITIES: '{{N|n|}}iestandardowe{{|j|}} encj{{e|i|}}', CUSTOM_ENTITIES: '{{N|n|}}iestandardowe{{|j|}} encj{{e|i|}}',
ENTITIES_HELP_1: 'Zdefiniuj niestandardowe encje dla magistrali EMS.', ENTITIES_HELP_1: 'Zdefiniuj niestandardowe encje dla magistrali EMS.', // TODO translate
ENTITIES_UPDATED: 'Niestandardowe encje zostały uaktualnione.', ENTITIES_UPDATED: 'Niestandardowe encje zostały uaktualnione.',
WRITEABLE: 'Zapisywalna', WRITEABLE: 'Zapisywalna',
SHOWING: 'Wyświetlane', SHOWING: 'Wyświetlane',
@@ -322,9 +329,7 @@ const pl: BaseTranslation = {
ACTIVEHIGH: 'Wyzwalany stanem wysokim', ACTIVEHIGH: 'Wyzwalany stanem wysokim',
ACTIVELOW: 'Wyzwalany stanem niskim', ACTIVELOW: 'Wyzwalany stanem niskim',
UNCHANGED: 'Zachowaj stan', UNCHANGED: 'Zachowaj stan',
ALWAYS: 'Zawsze', ALWAYS: 'Zawsze'
ACTIVITY: 'Aktywność',
CONFIGURE: 'Konfiguracja {0}'
}; };
export default pl; export default pl;

View File

@@ -12,6 +12,7 @@ const sk: Translation = {
USERNAME: 'Užívateľské meno', USERNAME: 'Užívateľské meno',
PASSWORD: 'Heslo', PASSWORD: 'Heslo',
SU_PASSWORD: 'su heslo', SU_PASSWORD: 'su heslo',
DASHBOARD: 'Panel',
SETTINGS_OF: '{0} Nastavenia', SETTINGS_OF: '{0} Nastavenia',
HELP_OF: '{0} Pomoc', HELP_OF: '{0} Pomoc',
LOGGED_IN: 'Prihlásený ako {name}', LOGGED_IN: 'Prihlásený ako {name}',
@@ -35,7 +36,9 @@ const sk: Translation = {
VERSION: 'Verzia', VERSION: 'Verzia',
BRAND: 'Značka', BRAND: 'Značka',
ENTITY_NAME: 'Názov entity', ENTITY_NAME: 'Názov entity',
VALUE: '{{Hodnota|hodnota}}', VALUE: '{{Value|value}}',
DEVICE_DATA: 'Dáta zariadenia',
SENSOR_DATA: 'Dáta snímača',
DEVICES: 'Zariadenia', DEVICES: 'Zariadenia',
SENSORS: 'Snímače', SENSORS: 'Snímače',
RUN_COMMAND: 'Volať príkaz', RUN_COMMAND: 'Volať príkaz',
@@ -77,33 +80,34 @@ const sk: Translation = {
ACTIVE_DEVICES: 'Aktívne zariadenia a snímače', ACTIVE_DEVICES: 'Aktívne zariadenia a snímače',
EMS_DEVICE: 'EMS zariadenie', EMS_DEVICE: 'EMS zariadenie',
SUCCESS: 'ÚSPEŠNÉ', SUCCESS: 'ÚSPEŠNÉ',
FAIL: 'ZLÝHANIE', FAIL: 'ZLYHANIE',
QUALITY: 'KVALITA', QUALITY: 'KVALITA',
SCAN_DEVICES: 'Scan pre nové zariadenia', SCAN_DEVICES: 'Scan pre nové zariadenia',
EMS_BUS_STATUS_TITLE: 'EMS zbernica & stav aktivity',
SCAN: 'Scan', SCAN: 'Scan',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS Telegramy prijaté (Rx)', 'EMS Telegramy prijaté (Rx)',
'EMS Čítania (Tx)', 'EMS Čítania (Tx)',
'EMS Zápisy (Tx)', 'EMS Zápisy (Tx)',
'Čítanie snímačov teploty', 'Čítanie snímača teploty',
'Čítanie analógových snímačov', 'Analógové snímanie',
'MQTT Publikovanie', 'MQTT Publikovanie',
'Externé API volania', 'API volania',
'Syslog správy' 'Syslog správy'
], ],
NUM_DEVICES: '{num} Zariaden{{í|ie|ia|ia|í|í}}', NUM_DEVICES: '{num} Zariadenia{{s}}',
NUM_TEMP_SENSORS: '{num} Teplotn{{ých|ý|é|é|ých|ých}} sníma{{čov|č|če|če|čov|čov}}', NUM_TEMP_SENSORS: '{num} Teplotné snímače{{s}}',
NUM_ANALOG_SENSORS: '{num} Analogov{{ých|ý|é|é|ých|ých}} sníma{{čov|č|če|če|čov|čov}}', NUM_ANALOG_SENSORS: '{num} Analógové snímače{{s}}',
NUM_DAYS: '{num} d{{ní|eň|ní|ní|ní|ní}}', NUM_DAYS: '{num} dní{{s}}',
NUM_SECONDS: '{num} sek{{únd|unda|undy|undy|únd|únd}}', NUM_SECONDS: '{num} sekúnd{{s}}',
NUM_HOURS: '{num} hod{{ín|ina|iny|iny|ín|ín}}', NUM_HOURS: '{num} hodín{{s}}',
NUM_MINUTES: '{num} minú{{t|ta|ty|ty|t|t}}', NUM_MINUTES: '{num} minút{{s}}',
APPLICATION_SETTINGS: 'Nastavenia aplikácie', APPLICATION_SETTINGS: 'Nastavenia aplikácie',
CUSTOMIZATIONS: 'Prispôsobenia', CUSTOMIZATIONS: 'Prispôsobenia',
APPLICATION_RESTARTING: 'EMS-ESP sa reštartuje', APPLICATION_RESTARTING: 'EMS-ESP sa reštartuje',
INTERFACE_BOARD_PROFILE: 'Profil dosky rozhrania', INTERFACE_BOARD_PROFILE: 'Profil boardu rozhrania',
BOARD_PROFILE_TEXT: 'Vyberte vopred nakonfigurovaný profil dosky rozhrania zo zoznamu nižšie, alebo vyberte možnosť Vlastné a nakonfigurujte svoje vlastné hardvérové nastavenia', BOARD_PROFILE_TEXT: 'Vyberte vopred nakonfigurovaný profil dosky rozhrania zo zoznamu nižšie alebo vyberte možnosť Vlastné a nakonfigurujte svoje vlastné hardvérové nastavenia',
BOARD_PROFILE: 'Profil dosky', BOARD_PROFILE: 'Board profil',
CUSTOM: 'Vlastné', CUSTOM: 'Vlastné',
GPIO_OF: '{0} GPIO', GPIO_OF: '{0} GPIO',
BUTTON: 'Tlačidlo', BUTTON: 'Tlačidlo',
@@ -118,12 +122,11 @@ const sk: Translation = {
HIDE_LED: 'Skryť LED', HIDE_LED: 'Skryť LED',
ENABLE_TELNET: 'Povoliť Telnet konzolu', ENABLE_TELNET: 'Povoliť Telnet konzolu',
ENABLE_ANALOG: 'Povoliť analógové snímače', ENABLE_ANALOG: 'Povoliť analógové snímače',
CONVERT_FAHRENHEIT: 'Previesť hodnoty teploty na °F', CONVERT_FAHRENHEIT: 'Previesť hodnoty teploty na fahrenheity',
BYPASS_TOKEN: 'Vynechajte autorizáciu prístupového tokenu pri volaniach API', BYPASS_TOKEN: 'Vynechajte autorizáciu prístupového tokenu pri volaniach API',
READONLY: 'Povoliť režim len na čítanie (blokuje všetky odchádzajúce príkazy EMS Tx Write)', READONLY: 'Povoliť režim len na čítanie (blokuje všetky odchádzajúce príkazy EMS Tx Write)',
UNDERCLOCK_CPU: 'Podtaktovanie rýchlosti procesora', UNDERCLOCK_CPU: 'Podtaktovanie rýchlosti procesora',
HEATINGOFF: 'Spustiť kotol s vynúteným vykurovaním', HEATINGOFF: 'Spustite kotol s núteným vykurovaním',
ENABLE_SHOWER_TIMER: 'Povoliť časovač sprchovania', ENABLE_SHOWER_TIMER: 'Povoliť časovač sprchovania',
ENABLE_SHOWER_ALERT: 'Povoliť upozornenie na sprchu', ENABLE_SHOWER_ALERT: 'Povoliť upozornenie na sprchu',
TRIGGER_TIME: 'Čas spustenia', TRIGGER_TIME: 'Čas spustenia',
@@ -133,7 +136,7 @@ const sk: Translation = {
BOOLEAN_FORMAT_API: 'Boolean formát API/MQTT', BOOLEAN_FORMAT_API: 'Boolean formát API/MQTT',
ENUM_FORMAT: 'Enum formát API/MQTT', ENUM_FORMAT: 'Enum formát API/MQTT',
INDEX: 'Index', INDEX: 'Index',
ENABLE_PARASITE: 'Povol parazité napájanie DS18B20', ENABLE_PARASITE: 'Povolenie parazitného napájania',
LOGGING: 'Logovanie', LOGGING: 'Logovanie',
LOG_HEX: 'Záznam telegramov EMS v hexadecimálnej sústave', LOG_HEX: 'Záznam telegramov EMS v hexadecimálnej sústave',
ENABLE_SYSLOG: 'Povoliť Syslog', ENABLE_SYSLOG: 'Povoliť Syslog',
@@ -160,7 +163,9 @@ const sk: Translation = {
OPTIONS: 'Možnosti', OPTIONS: 'Možnosti',
NAME: 'Názov', NAME: 'Názov',
CUSTOMIZATIONS_RESET: 'Naozaj chcete odstrániť všetky prispôsobenia vrátane vlastných nastavení snímačov teploty a analógových snímačov?', CUSTOMIZATIONS_RESET: 'Naozaj chcete odstrániť všetky prispôsobenia vrátane vlastných nastavení snímačov teploty a analógových snímačov?',
SUPPORT_INFORMATION: 'Informácie pre podporu', DEVICE_ENTITIES: 'Entity zariadenia',
SUPPORT_INFORMATION: 'Informácie o podpore',
CLICK_HERE: 'Kliknite tu',
HELP_INFORMATION_1: 'Navštívte online wiki, kde nájdete pokyny na konfiguráciu EMS-ESP', HELP_INFORMATION_1: 'Navštívte online wiki, kde nájdete pokyny na konfiguráciu EMS-ESP',
HELP_INFORMATION_2: 'Pre živý komunitný chat sa pripojte na náš Discord server', HELP_INFORMATION_2: 'Pre živý komunitný chat sa pripojte na náš Discord server',
HELP_INFORMATION_3: 'Ak chcete požiadať o funkciu alebo nahlásiť chybu', HELP_INFORMATION_3: 'Ak chcete požiadať o funkciu alebo nahlásiť chybu',
@@ -175,25 +180,27 @@ const sk: Translation = {
LOG_OF: '{0} Log', LOG_OF: '{0} Log',
STATUS_OF: '{0} Stav', STATUS_OF: '{0} Stav',
UPLOAD_DOWNLOAD: 'Nahrať/Stiahnuť', UPLOAD_DOWNLOAD: 'Nahrať/Stiahnuť',
VERSION_ON: 'Momentálne nainštalovaná verzia: ', VERSION_ON: 'Momentálne ste vo verzii',
SYSTEM_APPLY_FIRMWARE: 'na použitie nového firmvéru',
CLOSE: 'Zatvoriť', CLOSE: 'Zatvoriť',
USE: 'Použiť', USE: 'Použiť',
FACTORY_RESET: 'Továrenské nastavenia', FACTORY_RESET: 'Továrenské nastavenia',
SYSTEM_FACTORY_TEXT: 'Zariadenie bolo obnovené z výroby a teraz sa reštartuje', SYSTEM_FACTORY_TEXT: 'Zariadenie bolo obnovené z výroby a teraz sa reštartuje',
SYSTEM_FACTORY_TEXT_DIALOG: 'Naozaj chcete resetovať EMS-ESP na predvolené výrobné nastavenia?', SYSTEM_FACTORY_TEXT_DIALOG: 'Naozaj chcete resetovať EMS-ESP na predvolené výrobné nastavenia?',
VERSION_CHECK: 'Kontrola verzie',
THE_LATEST: 'Posledná', THE_LATEST: 'Posledná',
OFFICIAL: 'officiálna', OFFICIAL: 'officiálna',
DEVELOPMENT: 'vývojárska', DEVELOPMENT: 'vývojárska',
RELEASE_IS: 'verzia je', RELEASE_IS: 'vydanie je',
RELEASE_NOTES: 'poznámky k verzii', RELEASE_NOTES: 'poznámky k vydaniu',
EMS_ESP_VER: 'EMS-ESP verzia', EMS_ESP_VER: 'EMS-ESP verzia',
UPTIME: 'Beh systému', UPTIME: 'Beh systému',
HEAP: 'Zásobník (voľné / max pridelenie)', HEAP: 'Zásobník (voľné / max pridelenie)',
PSRAM: 'PSRAM (Veľkosť / Voľné)', PSRAM: 'PSRAM (Veľkosť / Voľné)',
FLASH: 'Flash chip (Veľkosť / Rýchlosť)', FLASH: 'Flash chip (Veľkosť / Rýchlosť)',
APPSIZE: 'Applikácia (Oddiel: Použité / Voľné)', APPSIZE: 'Applikácia (Priečka: Použité / Voľné)',
FILESYSTEM: 'Súborový systém (Použité / Voľné)', FILESYSTEM: 'Súborový systém (Použité / Voľné)',
BUFFER_SIZE: 'Buffer-max.veľkosť', BUFFER_SIZE: 'Maximálna veľkosť vyrovnávacej pamäte',
COMPACT: 'Kompaktné', COMPACT: 'Kompaktné',
ENABLE_OTA: 'Povoliť OTA aktualizácie', ENABLE_OTA: 'Povoliť OTA aktualizácie',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Stiahnutie prispôsobení entity', DOWNLOAD_CUSTOMIZATION_TEXT: 'Stiahnutie prispôsobení entity',
@@ -201,7 +208,7 @@ const sk: Translation = {
DOWNLOAD_SETTINGS_TEXT: 'Stiahnite si nastavenia aplikácie. Pri zdieľaní nastavení buďte opatrní, pretože tento súbor obsahuje heslá a iné citlivé systémové informácie.', DOWNLOAD_SETTINGS_TEXT: 'Stiahnite si nastavenia aplikácie. Pri zdieľaní nastavení buďte opatrní, pretože tento súbor obsahuje heslá a iné citlivé systémové informácie.',
UPLOAD_TEXT: 'Najskôr nahrajte nový súbor firmvéru (.bin), nastavenia alebo prispôsobenia (.json), pre voliteľné overenie nahrajte súbor (.md5)', UPLOAD_TEXT: 'Najskôr nahrajte nový súbor firmvéru (.bin), nastavenia alebo prispôsobenia (.json), pre voliteľné overenie nahrajte súbor (.md5)',
UPLOADING: 'Nahrávanie', UPLOADING: 'Nahrávanie',
UPLOAD_DROP_TEXT: 'Potiahnúť a pripnúť súbor alebo kliknúť sem', UPLOAD_DROP_TEXT: 'Zahodiť súbor alebo kliknúť sem',
ERROR: 'Neočakávaná chyba, prosím skúste to znova', ERROR: 'Neočakávaná chyba, prosím skúste to znova',
TIME_SET: 'Nastavený čas', TIME_SET: 'Nastavený čas',
MANAGE_USERS: 'Správa používateľov', MANAGE_USERS: 'Správa používateľov',
@@ -237,7 +244,6 @@ const sk: Translation = {
MQTT_INT_THERMOSTATS: 'Termostaty', MQTT_INT_THERMOSTATS: 'Termostaty',
MQTT_INT_SOLAR: 'Solárne moduly', MQTT_INT_SOLAR: 'Solárne moduly',
MQTT_INT_MIXER: 'Zmiešavacie moduley', MQTT_INT_MIXER: 'Zmiešavacie moduley',
MQTT_INT_WATER: 'Voda moduley',
MQTT_QUEUE: 'Fronta MQTT', MQTT_QUEUE: 'Fronta MQTT',
DEFAULT: 'Predvolené', DEFAULT: 'Predvolené',
MQTT_ENTITY_FORMAT: 'ID formát entity', MQTT_ENTITY_FORMAT: 'ID formát entity',
@@ -259,7 +265,7 @@ const sk: Translation = {
ACCESS_POINT: 'Prístupový bod', ACCESS_POINT: 'Prístupový bod',
AP_PROVIDE: 'Povoliť prístupový bod', AP_PROVIDE: 'Povoliť prístupový bod',
AP_PROVIDE_TEXT_1: 'vždy', AP_PROVIDE_TEXT_1: 'vždy',
AP_PROVIDE_TEXT_2: 'keď je WiFi odpojená', AP_PROVIDE_TEXT_2: 'keď WiFi je odpojená',
AP_PROVIDE_TEXT_3: 'nikdy', AP_PROVIDE_TEXT_3: 'nikdy',
AP_PREFERRED_CHANNEL: 'Preferovaný kanál', AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
AP_HIDE_SSID: 'Skryť SSID', AP_HIDE_SSID: 'Skryť SSID',
@@ -287,11 +293,11 @@ const sk: Translation = {
NETWORK_GATEWAY: 'Brána', NETWORK_GATEWAY: 'Brána',
NETWORK_SUBNET: 'Maska podsiete', NETWORK_SUBNET: 'Maska podsiete',
NETWORK_DNS: 'DNS servery', NETWORK_DNS: 'DNS servery',
ADDRESS_OF: '{0} adresa', ADDRESS_OF: '{0} adries',
ADMIN: 'Admin', ADMIN: 'Admin',
GUEST: 'Hosť', GUEST: 'Hosť',
NEW: 'Nová', NEW: 'Nová',
NEW_NAME_OF: 'Nový názov {0}', NEW_NAME_OF: 'Nových {0} názvov',
ENTITY: 'entita', ENTITY: 'entita',
MIN: 'min', MIN: 'min',
MAX: 'max', MAX: 'max',
@@ -302,7 +308,7 @@ const sk: Translation = {
SCHEDULER: 'Plánovač', SCHEDULER: 'Plánovač',
SCHEDULER_HELP_1: 'Automatizujte príkazy pridaním naplánovaných udalostí nižšie. Nastavte jedinečné meno na aktiváciu/deaktiváciu cez API/MQTT.', SCHEDULER_HELP_1: 'Automatizujte príkazy pridaním naplánovaných udalostí nižšie. Nastavte jedinečné meno na aktiváciu/deaktiváciu cez API/MQTT.',
SCHEDULER_HELP_2: 'Použite 00:00 na jednorazové spustenie pri štarte', SCHEDULER_HELP_2: 'Použite 00:00 na jednorazové spustenie pri štarte',
SCHEDULE: 'Plánovač', SCHEDULE: 'Plánovať',
TIME: 'Čas', TIME: 'Čas',
TIMER: 'Časovač', TIMER: 'Časovač',
SCHEDULE_UPDATED: 'Plánovanie aktualizované', SCHEDULE_UPDATED: 'Plánovanie aktualizované',
@@ -310,7 +316,7 @@ const sk: Translation = {
SCHEDULE_TIMER_2: 'každú minútu', SCHEDULE_TIMER_2: 'každú minútu',
SCHEDULE_TIMER_3: 'každú hodinu', SCHEDULE_TIMER_3: 'každú hodinu',
CUSTOM_ENTITIES: 'Vlastné entity', CUSTOM_ENTITIES: 'Vlastné entity',
ENTITIES_HELP_1: 'Získavanie vlastných entít zo zbernice EMS', ENTITIES_HELP_1: 'Získavanie vlastných entít zo zbernice EMS', // TODO translate
ENTITIES_UPDATED: 'Aktualizované entity', ENTITIES_UPDATED: 'Aktualizované entity',
WRITEABLE: 'Zapísateľný', WRITEABLE: 'Zapísateľný',
SHOWING: 'Zobrazenie', SHOWING: 'Zobrazenie',
@@ -323,9 +329,7 @@ const sk: Translation = {
ACTIVEHIGH: 'Aktívny Vysoký', ACTIVEHIGH: 'Aktívny Vysoký',
ACTIVELOW: 'Aktívny Nízky', ACTIVELOW: 'Aktívny Nízky',
UNCHANGED: 'Nezmenené', UNCHANGED: 'Nezmenené',
ALWAYS: 'Vždy', ALWAYS: 'Vždy'
ACTIVITY: 'Aktivita',
CONFIGURE: 'Konfiguracia {0}'
}; };
export default sk; export default sk;

View File

@@ -12,6 +12,7 @@ const sv: Translation = {
USERNAME: 'Användarnamn', USERNAME: 'Användarnamn',
PASSWORD: 'Lösenord', PASSWORD: 'Lösenord',
SU_PASSWORD: 'su Lösenord', SU_PASSWORD: 'su Lösenord',
DASHBOARD: 'Kontrollpanel',
SETTINGS_OF: '{0} Inställningar', SETTINGS_OF: '{0} Inställningar',
HELP_OF: '{0} Hjälp', HELP_OF: '{0} Hjälp',
LOGGED_IN: 'Inloggad som {name}', LOGGED_IN: 'Inloggad som {name}',
@@ -36,6 +37,8 @@ const sv: Translation = {
BRAND: 'Fabrikat', BRAND: 'Fabrikat',
ENTITY_NAME: 'Entitetsnamn', ENTITY_NAME: 'Entitetsnamn',
VALUE: '{{Värde|värde}}', VALUE: '{{Värde|värde}}',
DEVICE_DATA: 'Enhets data',
SENSOR_DATA: 'Sensor data',
DEVICES: 'Enheter', DEVICES: 'Enheter',
SENSORS: 'Sensorer', SENSORS: 'Sensorer',
RUN_COMMAND: 'Kör Kommando', RUN_COMMAND: 'Kör Kommando',
@@ -80,6 +83,7 @@ const sv: Translation = {
FAIL: 'Misslyckades', FAIL: 'Misslyckades',
QUALITY: 'Kvalitet', QUALITY: 'Kvalitet',
SCAN_DEVICES: 'Sök efter nya enheter', SCAN_DEVICES: 'Sök efter nya enheter',
EMS_BUS_STATUS_TITLE: 'EMS-buss & aktivitetsstatus',
SCAN: 'Sök', SCAN: 'Sök',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS-telegram (Rx)', 'EMS-telegram (Rx)',
@@ -159,7 +163,9 @@ const sv: Translation = {
OPTIONS: 'Alternativ', OPTIONS: 'Alternativ',
NAME: 'Namn', 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?', 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',
SUPPORT_INFORMATION: 'Supportinformation', 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_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_2: 'För community-support besök vår Discord-server',
HELP_INFORMATION_3: 'Önska en ny funktion eller rapportera en bugg', HELP_INFORMATION_3: 'Önska en ny funktion eller rapportera en bugg',
@@ -175,11 +181,13 @@ const sv: Translation = {
STATUS_OF: '{0} Status', STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upp/Nedladdning', UPLOAD_DOWNLOAD: 'Upp/Nedladdning',
VERSION_ON: 'You are currently on', // TODO translate VERSION_ON: 'You are currently on', // TODO translate
SYSTEM_APPLY_FIRMWARE: 'för att aktivera ny firmware',
CLOSE: 'Stäng', CLOSE: 'Stäng',
USE: 'Använd', USE: 'Använd',
FACTORY_RESET: 'Fabriksåterställning', FACTORY_RESET: 'Fabriksåterställning',
SYSTEM_FACTORY_TEXT: 'Enheten har blivit fabriksåterställd och startar nu om', 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?', SYSTEM_FACTORY_TEXT_DIALOG: 'Är du säker att du vill fabriksåterställa enheten?',
VERSION_CHECK: 'Senaste versioner',
THE_LATEST: 'Den senaste', THE_LATEST: 'Den senaste',
OFFICIAL: 'officiell', OFFICIAL: 'officiell',
DEVELOPMENT: 'utveckling', DEVELOPMENT: 'utveckling',
@@ -236,7 +244,6 @@ const sv: Translation = {
MQTT_INT_THERMOSTATS: 'Termostater', MQTT_INT_THERMOSTATS: 'Termostater',
MQTT_INT_SOLAR: 'Solpaneler', MQTT_INT_SOLAR: 'Solpaneler',
MQTT_INT_MIXER: 'Blandningsventiler', MQTT_INT_MIXER: 'Blandningsventiler',
MQTT_INT_WATER: 'Water Modules', // TODO translate
MQTT_QUEUE: 'MQTT-kö', MQTT_QUEUE: 'MQTT-kö',
DEFAULT: 'Standard', DEFAULT: 'Standard',
MQTT_ENTITY_FORMAT: 'Entitets-ID format', MQTT_ENTITY_FORMAT: 'Entitets-ID format',
@@ -322,9 +329,7 @@ const sv: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always', // TODO translate ALWAYS: 'Always' // TODO translate
ACTIVITY: 'Activity', // TODO translate
CONFIGURE: 'Configure {0}' // TODO translate
}; };
export default sv; export default sv;

View File

@@ -12,6 +12,7 @@ const tr: Translation = {
USERNAME: 'Kullanıcı Adı', USERNAME: 'Kullanıcı Adı',
PASSWORD: 'Şifre', PASSWORD: 'Şifre',
SU_PASSWORD: 'SK Şifresi', SU_PASSWORD: 'SK Şifresi',
DASHBOARD: 'Gösterge Paneli',
SETTINGS_OF: '{0} Ayarlar', SETTINGS_OF: '{0} Ayarlar',
HELP_OF: '{0} Yardım', HELP_OF: '{0} Yardım',
LOGGED_IN: '{name} olarak giriş yapıldı', LOGGED_IN: '{name} olarak giriş yapıldı',
@@ -36,6 +37,8 @@ const tr: Translation = {
BRAND: 'Marka', BRAND: 'Marka',
ENTITY_NAME: 'Valık Adı', ENTITY_NAME: 'Valık Adı',
VALUE: '{{Değer|değer}}', VALUE: '{{Değer|değer}}',
DEVICE_DATA: 'Cihaz Bilgisi',
SENSOR_DATA: 'Sensör Bilgisi',
DEVICES: 'Cihazlar', DEVICES: 'Cihazlar',
SENSORS: 'Sensörler', SENSORS: 'Sensörler',
RUN_COMMAND: 'Çalıştırma Komutu', RUN_COMMAND: 'Çalıştırma Komutu',
@@ -80,6 +83,7 @@ const tr: Translation = {
FAIL: 'HATA', FAIL: 'HATA',
QUALITY: 'KALİTE', QUALITY: 'KALİTE',
SCAN_DEVICES: 'Yeni cihaz taraması', SCAN_DEVICES: 'Yeni cihaz taraması',
EMS_BUS_STATUS_TITLE: 'EMS Hattı ve Aktivite Durumu',
SCAN: 'Tara', SCAN: 'Tara',
STATUS_NAMES: [ STATUS_NAMES: [
'EMS Telegramlar Alındı (Rx)', 'EMS Telegramlar Alındı (Rx)',
@@ -159,7 +163,9 @@ const tr: Translation = {
OPTIONS: 'Seçenekler', OPTIONS: 'Seçenekler',
NAME: 'İsim', NAME: 'İsim',
CUSTOMIZATIONS_RESET: 'Sıcaklık ve Analog Sensörlerin özelleştirilmiş seçenekleri dahil bütün özelleştirmeleri kaldırmak istediğinizden emin misiniz?', CUSTOMIZATIONS_RESET: 'Sıcaklık ve Analog Sensörlerin özelleştirilmiş seçenekleri dahil bütün özelleştirmeleri kaldırmak istediğinizden emin misiniz?',
DEVICE_ENTITIES: 'Cihaz Varlıkları',
SUPPORT_INFORMATION: 'Destek Bilgileri', SUPPORT_INFORMATION: 'Destek Bilgileri',
CLICK_HERE: 'Buraya Tıklayın',
HELP_INFORMATION_1: 'EMS-ESPnin nasıl ayarlanacağı ile ilgili bilgileri edinmek için çevrimiçi WIKI sayfasını ziyaret edin', HELP_INFORMATION_1: 'EMS-ESPnin nasıl ayarlanacağı ile ilgili bilgileri edinmek için çevrimiçi WIKI sayfasını ziyaret edin',
HELP_INFORMATION_2: 'Canlı topluluk sohbeti için Discord sunucumuza katılın', HELP_INFORMATION_2: 'Canlı topluluk sohbeti için Discord sunucumuza katılın',
HELP_INFORMATION_3: 'Yeni bir özellik talep etmek yada hata bildirmek için', HELP_INFORMATION_3: 'Yeni bir özellik talep etmek yada hata bildirmek için',
@@ -175,11 +181,13 @@ const tr: Translation = {
STATUS_OF: '{0} Durumu', STATUS_OF: '{0} Durumu',
UPLOAD_DOWNLOAD: 'Yükleme/İndirme', UPLOAD_DOWNLOAD: 'Yükleme/İndirme',
VERSION_ON: 'You are currently on', // TODO translate VERSION_ON: 'You are currently on', // TODO translate
SYSTEM_APPLY_FIRMWARE: 'yeni bellenimi uygulamak için',
CLOSE: 'Kapat', CLOSE: 'Kapat',
USE: 'KUllan', USE: 'KUllan',
FACTORY_RESET: 'Fabrika ayarına dönme', FACTORY_RESET: 'Fabrika ayarına dönme',
SYSTEM_FACTORY_TEXT: 'Cihaz fabrika ayarlarına döndü ve şimdi yendiden başlatılacak', SYSTEM_FACTORY_TEXT: 'Cihaz fabrika ayarlarına döndü ve şimdi yendiden başlatılacak',
SYSTEM_FACTORY_TEXT_DIALOG: 'Cihazı fabrika ayarlarına döndürmek istediğinize emin misiniz?', SYSTEM_FACTORY_TEXT_DIALOG: 'Cihazı fabrika ayarlarına döndürmek istediğinize emin misiniz?',
VERSION_CHECK: 'Sürüm Kontrolü',
THE_LATEST: 'En son', THE_LATEST: 'En son',
OFFICIAL: 'resmi', OFFICIAL: 'resmi',
DEVELOPMENT: 'geliştirme', DEVELOPMENT: 'geliştirme',
@@ -236,7 +244,6 @@ const tr: Translation = {
MQTT_INT_THERMOSTATS: 'Termostatlar', MQTT_INT_THERMOSTATS: 'Termostatlar',
MQTT_INT_SOLAR: 'Güneş Enerjisi Modülleri', MQTT_INT_SOLAR: 'Güneş Enerjisi Modülleri',
MQTT_INT_MIXER: 'Karışım Modülleri', MQTT_INT_MIXER: 'Karışım Modülleri',
MQTT_INT_WATER: 'Water Modules', // TODO translate
MQTT_QUEUE: 'MQTT Sırası', MQTT_QUEUE: 'MQTT Sırası',
DEFAULT: 'Varsayılan', DEFAULT: 'Varsayılan',
MQTT_ENTITY_FORMAT: 'Varlık Kimlik biçimi', MQTT_ENTITY_FORMAT: 'Varlık Kimlik biçimi',
@@ -322,9 +329,7 @@ const tr: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always', // TODO translate ALWAYS: 'Always' // TODO translate
ACTIVITY: 'Activity', // TODO translate
CONFIGURE: 'Configure {0}' // TODO translate
}; };
export default tr; export default tr;

View File

@@ -0,0 +1,37 @@
import { Tab } from '@mui/material';
import { Navigate, Route, Routes } from 'react-router-dom';
import DashboardDevices from './DashboardDevices';
import DashboardSensors from './DashboardSensors';
import DashboardStatus from './DashboardStatus';
import type { FC } from 'react';
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Dashboard: FC = () => {
const { routerTab } = useRouterTab();
const { LL } = useI18nContext();
useLayoutTitle(LL.DASHBOARD());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="/dashboard/devices" label={LL.DEVICES()} />
<Tab value="/dashboard/sensors" label={LL.SENSORS()} />
<Tab value="/dashboard/status" label="Status" />
</RouterTabs>
<Routes>
<Route path="devices" element={<DashboardDevices />} />
<Route path="sensors" element={<DashboardSensors />} />
<Route path="status" element={<DashboardStatus />} />
<Route path="*" element={<Navigate replace to="/dashboard/devices" />} />
</Routes>
</>
);
};
export default Dashboard;

View File

@@ -33,13 +33,13 @@ import { useSort, SortToggleType } from '@table-library/react-table-library/sort
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { useState, useEffect, useCallback, useLayoutEffect, useContext } from 'react'; import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react';
import { IconContext } from 'react-icons'; import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DashboardDevicesDialog from './DashboardDevicesDialog';
import DeviceIcon from './DeviceIcon'; import DeviceIcon from './DeviceIcon';
import DashboardDevicesDialog from './DevicesDialog';
import * as EMSESP from './api'; import * as EMSESP from './api';
import { formatValue } from './deviceValue'; import { formatValue } from './deviceValue';
@@ -49,15 +49,14 @@ import { deviceValueItemValidation } from './validators';
import type { Device, DeviceValue } from './types'; import type { Device, DeviceValue } from './types';
import type { FC } from 'react'; import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { ButtonRow, SectionContent, MessageBox, useLayoutTitle } from 'components'; import { ButtonRow, SectionContent, MessageBox } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const Devices: FC = () => { const DashboardDevices: FC = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const [size, setSize] = useState([0, 0]); const [size, setSize] = useState([0, 0]);
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>(); const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
const [onlyFav, setOnlyFav] = useState(false); const [onlyFav, setOnlyFav] = useState(false);
@@ -67,8 +66,6 @@ const Devices: FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useLayoutTitle(LL.DEVICES());
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), { const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
initialData: { initialData: {
connected: true, connected: true,
@@ -284,9 +281,9 @@ const Devices: FC = () => {
const customize = () => { const customize = () => {
if (selectedDevice == 99) { if (selectedDevice == 99) {
navigate('/customentities'); navigate('/settings/customentities');
} else { } else {
navigate('/customizations', { state: selectedDevice }); navigate('/settings/customization', { state: selectedDevice });
} }
}; };
@@ -423,8 +420,11 @@ const Devices: FC = () => {
}; };
const renderCoreData = () => ( const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '18', style: { verticalAlign: 'middle' } }}> <IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />} {!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()} />
)} */}
{coreData.connected && ( {coreData.connected && (
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}> <Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
@@ -523,11 +523,9 @@ const Devices: FC = () => {
<IconButton onClick={() => setShowDeviceInfo(true)}> <IconButton onClick={() => setShowDeviceInfo(true)}>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton> </IconButton>
{me.admin && ( <IconButton onClick={customize}>
<IconButton onClick={customize}> <FormatListNumberedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<FormatListNumberedIcon sx={{ fontSize: 18, verticalAlign: 'middle' }} /> </IconButton>
</IconButton>
)}
<IconButton onClick={handleDownloadCsv}> <IconButton onClick={handleDownloadCsv}>
<DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton> </IconButton>
@@ -589,7 +587,7 @@ const Devices: FC = () => {
<Cell>{renderNameCell(dv)}</Cell> <Cell>{renderNameCell(dv)}</Cell>
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell> <Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
<Cell stiff> <Cell stiff>
{me.admin && dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( {dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<IconButton size="small" onClick={() => showDeviceValue(dv)}> <IconButton size="small" onClick={() => showDeviceValue(dv)}>
{dv.v === '' && dv.c ? ( {dv.v === '' && dv.c ? (
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} /> <PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
@@ -610,7 +608,7 @@ const Devices: FC = () => {
}; };
return ( return (
<SectionContent id="devices-window"> <SectionContent title={LL.DEVICE_DATA()} titleGutter id="devices-window">
{renderCoreData()} {renderCoreData()}
{renderDeviceData()} {renderDeviceData()}
{renderDeviceDetails()} {renderDeviceDetails()}
@@ -621,13 +619,15 @@ const Devices: FC = () => {
onSave={deviceValueDialogSave} onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue} selectedItem={selectedDeviceValue}
writeable={ writeable={
selectedDeviceValue.c !== undefined && !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY) me.admin &&
selectedDeviceValue.c !== undefined &&
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
} }
validator={deviceValueItemValidation(selectedDeviceValue)} validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting} progress={submitting}
/> />
)} )}
<ButtonRow mt={1}> <ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}> <Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
@@ -636,4 +636,4 @@ const Devices: FC = () => {
); );
}; };
export default Devices; export default DashboardDevices;

View File

@@ -40,7 +40,7 @@ type DashboardDevicesDialogProps = {
progress: boolean; progress: boolean;
}; };
const DevicesDialog = ({ const DashboardDevicesDialog = ({
open, open,
onClose, onClose,
onSave, onSave,
@@ -204,4 +204,4 @@ const DevicesDialog = ({
); );
}; };
export default DevicesDialog; export default DashboardDevicesDialog;

View File

@@ -8,27 +8,26 @@ import { useSort, SortToggleType } from '@table-library/react-table-library/sort
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { useState, useEffect, useContext } from 'react'; import { useState, useContext, useEffect } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog'; import DashboardSensorsAnalogDialog from './DashboardSensorsAnalogDialog';
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog'; import DashboardSensorsTemperatureDialog from './DashboardSensorsTemperatureDialog';
import * as EMSESP from './api'; import * as EMSESP from './api';
import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames, AnalogType } from './types'; import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames, AnalogType } from './types';
import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators'; import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators';
import type { TemperatureSensor, AnalogSensor } from './types'; import type { TemperatureSensor, AnalogSensor } from './types';
import type { FC } from 'react'; import type { FC } from 'react';
import { ButtonRow, SectionContent, useLayoutTitle } from 'components'; import { ButtonRow, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const Sensors: FC = () => { const DashboardSensors: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>(); const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>(); const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false); const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
@@ -52,6 +51,8 @@ const Sensors: FC = () => {
immediate: false immediate: false
}); });
const isAdmin = me.admin;
const common_theme = useTheme({ const common_theme = useTheme({
BaseRow: ` BaseRow: `
font-size: 14px; font-size: 14px;
@@ -169,8 +170,6 @@ const Sensors: FC = () => {
}; };
}); });
useLayoutTitle(LL.SENSORS());
const formatDurationMin = (duration_min: number) => { const formatDurationMin = (duration_min: number) => {
const days = Math.trunc((duration_min * 60000) / 86400000); const days = Math.trunc((duration_min * 60000) / 86400000);
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24; const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
@@ -221,7 +220,7 @@ const Sensors: FC = () => {
} }
const updateTemperatureSensor = (ts: TemperatureSensor) => { const updateTemperatureSensor = (ts: TemperatureSensor) => {
if (me.admin) { if (isAdmin) {
setSelectedTemperatureSensor(ts); setSelectedTemperatureSensor(ts);
setTemperatureDialogOpen(true); setTemperatureDialogOpen(true);
} }
@@ -247,7 +246,7 @@ const Sensors: FC = () => {
}; };
const updateAnalogSensor = (as: AnalogSensor) => { const updateAnalogSensor = (as: AnalogSensor) => {
if (me.admin) { if (isAdmin) {
setCreating(false); setCreating(false);
setSelectedAnalogSensor(as); setSelectedAnalogSensor(as);
setAnalogDialogOpen(true); setAnalogDialogOpen(true);
@@ -407,20 +406,25 @@ const Sensors: FC = () => {
); );
return ( return (
<SectionContent> <SectionContent title={LL.SENSOR_DATA()} titleGutter>
<Typography sx={{ pb: 1 }} variant="h6" color="secondary"> {sensorData.ts.length > 0 && (
{LL.TEMP_SENSORS()} <>
</Typography> <Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
<RenderTemperatureSensors /> {LL.TEMP_SENSORS()}
{selectedTemperatureSensor && ( </Typography>
<DashboardSensorsTemperatureDialog <RenderTemperatureSensors />
open={temperatureDialogOpen} {selectedTemperatureSensor && (
onClose={onTemperatureDialogClose} <DashboardSensorsTemperatureDialog
onSave={onTemperatureDialogSave} open={temperatureDialogOpen}
selectedItem={selectedTemperatureSensor} onClose={onTemperatureDialogClose}
validator={temperatureSensorItemValidation()} onSave={onTemperatureDialogSave}
/> selectedItem={selectedTemperatureSensor}
validator={temperatureSensorItemValidation()}
/>
)}
</>
)} )}
{sensorData?.analog_enabled === true && ( {sensorData?.analog_enabled === true && (
<> <>
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary"> <Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
@@ -439,14 +443,15 @@ const Sensors: FC = () => {
)} )}
</> </>
)} )}
<ButtonRow> <ButtonRow>
<Box mt={1} display="flex" flexWrap="wrap"> <Box mt={2} display="flex" flexWrap="wrap">
<Box flexGrow={1}> <Box flexGrow={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}> <Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</Box> </Box>
{sensorData?.analog_enabled === true && me.admin && ( {sensorData?.analog_enabled === true && (
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
@@ -462,4 +467,4 @@ const Sensors: FC = () => {
); );
}; };
export default Sensors; export default DashboardSensors;

View File

@@ -38,7 +38,7 @@ type DashboardSensorsAnalogDialogProps = {
validator: Schema; validator: Schema;
}; };
const SensorsAnalogDialog = ({ const DashboardSensorsAnalogDialog = ({
open, open,
onClose, onClose,
onSave, onSave,
@@ -296,4 +296,4 @@ const SensorsAnalogDialog = ({
); );
}; };
export default SensorsAnalogDialog; export default DashboardSensorsAnalogDialog;

View File

@@ -26,7 +26,7 @@ import { numberValue, updateValue } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
type SensorsTemperatureDialogProps = { type DashboardSensorsTemperatureDialogProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSave: (ts: TemperatureSensor) => void; onSave: (ts: TemperatureSensor) => void;
@@ -34,13 +34,13 @@ type SensorsTemperatureDialogProps = {
validator: Schema; validator: Schema;
}; };
const SensorsTemperatureDialog = ({ const DashboardSensorsTemperatureDialog = ({
open, open,
onClose, onClose,
onSave, onSave,
selectedItem, selectedItem,
validator validator
}: SensorsTemperatureDialogProps) => { }: DashboardSensorsTemperatureDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem); const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
@@ -119,4 +119,4 @@ const SensorsTemperatureDialog = ({
); );
}; };
export default SensorsTemperatureDialog; export default DashboardSensorsTemperatureDialog;

View File

@@ -0,0 +1,282 @@
import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import RefreshIcon from '@mui/icons-material/Refresh';
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import * as EMSESP from './api';
import { busConnectionStatus } from './types';
import type { Stat, Status } from './types';
import type { Theme } from '@mui/material';
import type { Translation } from 'i18n/i18n-types';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
const busStatusHighlight = ({ status }: Status, theme: Theme) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main;
case busConnectionStatus.BUS_STATUS_CONNECTED:
return theme.palette.success.main;
case busConnectionStatus.BUS_STATUS_OFFLINE:
return theme.palette.error.main;
default:
return theme.palette.warning.main;
}
};
const showQuality = (stat: Stat) => {
if (stat.q === 0 || stat.s + stat.f === 0) {
return;
}
if (stat.q === 100) {
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
}
if (stat.q >= 95) {
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
} else {
return <div style={{ color: 'red' }}>{stat.q}%</div>;
}
};
const DashboardStatus: FC = () => {
const { data: data, send: loadData, error } = useRequest(EMSESP.readStatus);
const { LL } = useI18nContext();
const theme = useTheme();
const [confirmScan, setConfirmScan] = useState<boolean>(false);
const { me } = useContext(AuthenticatedContext);
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
immediate: false
});
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
`,
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
height: 36px;
border-bottom: 1px solid #565656;
}
`,
Row: `
.td {
padding: 8px;
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
&:nth-of-type(even) .td {
background-color: #1e1e1e;
}
`,
BaseCell: `
&:not(:first-of-type) {
text-align: center;
}
`
});
useEffect(() => {
const timer = setInterval(() => loadData(), 30000);
return () => {
clearInterval(timer);
};
});
const showName = (id: any) => {
const name: keyof Translation['STATUS_NAMES'] = id;
return LL.STATUS_NAMES[name]();
};
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 busStatus = () => {
if (data) {
switch (data.status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (' + formatDurationSec(data.uptime) + ')';
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return LL.TX_ISSUES();
case busConnectionStatus.BUS_STATUS_OFFLINE:
return LL.DISCONNECTED();
}
}
return 'Unknown';
};
const scan = async () => {
await scanDevices()
.then(() => {
toast.info(LL.SCANNING() + '...');
})
.catch((err) => {
toast.error(err.message);
});
setConfirmScan(false);
};
const renderScanDialog = () => (
<Dialog sx={dialogStyle} open={confirmScan} onClose={() => setConfirmScan(false)}>
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
{LL.CANCEL()}
</Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
{LL.SCAN()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: busStatusHighlight(data, theme) }}>
<DirectionsBusIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.EMS_BUS_STATUS()} secondary={busStatus()} />
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.ACTIVE_DEVICES()}
secondary={
LL.NUM_DEVICES({ num: data.num_devices }) +
', ' +
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
', ' +
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
}
/>
</ListItem>
<Box m={3} />
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize />
<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>{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>
</Row>
))}
</Body>
</>
)}
</Table>
</List>
{renderScanDialog()}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
color="primary"
disabled={!me.admin}
onClick={() => setConfirmScan(true)}
>
{LL.SCAN_DEVICES()}
</Button>
</ButtonRow>
</Box>
</Box>
</>
);
};
return (
<SectionContent title={LL.EMS_BUS_STATUS_TITLE()} titleGutter>
{content()}
</SectionContent>
);
};
export default DashboardStatus;

View File

@@ -1,13 +1,12 @@
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import { AiOutlineControl, AiOutlineGateway, AiOutlineAlert } from 'react-icons/ai'; import { AiOutlineControl, AiOutlineGateway, AiOutlineAlert } from 'react-icons/ai';
import { CgSmartHomeBoiler } from 'react-icons/cg'; import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa'; import { FaSolarPanel } from 'react-icons/fa';
import { GiHeatHaze, GiTap } from 'react-icons/gi'; import { GiHeatHaze } from 'react-icons/gi';
import { MdThermostatAuto, MdOutlineSensors, MdOutlineDevices, MdOutlinePool } from 'react-icons/md'; import { MdThermostatAuto, MdOutlineSensors, MdOutlineExtension, MdOutlineDevices } from 'react-icons/md';
import { TiFlowSwitch } from 'react-icons/ti'; import { TiFlowSwitch } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc'; import { VscVmConnect } from 'react-icons/vsc';
import { DeviceType } from './types'; import { DeviceType } from './types';
import type { FC } from 'react'; import type { FC } from 'react';
interface DeviceIconProps { interface DeviceIconProps {
@@ -41,12 +40,8 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
return <AiOutlineAlert />; return <AiOutlineAlert />;
case DeviceType.EXTENSION: case DeviceType.EXTENSION:
return <MdOutlineDevices />; return <MdOutlineDevices />;
case DeviceType.WATER:
return <GiTap />;
case DeviceType.POOL:
return <MdOutlinePool />;
case DeviceType.CUSTOM: case DeviceType.CUSTOM:
return <PlaylistAddIcon sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }} />; return <MdOutlineExtension />;
default: default:
return null; return null;
} }

View File

@@ -1,19 +1,9 @@
import CommentIcon from '@mui/icons-material/CommentTwoTone'; import CommentIcon from '@mui/icons-material/CommentTwoTone';
import EastIcon from '@mui/icons-material/East';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
import GitHubIcon from '@mui/icons-material/GitHub'; import GitHubIcon from '@mui/icons-material/GitHub';
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone'; import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
import { import { Box, List, ListItem, ListItemAvatar, ListItemText, Link, Typography, Button } from '@mui/material';
Box,
List,
ListItem,
ListItemAvatar,
ListItemText,
Link,
Typography,
Button,
ListItemButton,
Avatar
} from '@mui/material';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import type { FC } from 'react'; import type { FC } from 'react';
@@ -49,56 +39,59 @@ const Help: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SUPPORT_INFORMATION(0)} titleGutter>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}> <List>
<ListItem> <ListItem>
<ListItemButton component="a" href="https://emsesp.github.io/docs"> <ListItemAvatar>
<ListItemAvatar> <MenuBookIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<Avatar sx={{ bgcolor: '#72caf9' }}> </ListItemAvatar>
<MenuBookIcon /> <ListItemText>
</Avatar> {LL.HELP_INFORMATION_1()}&nbsp;
</ListItemAvatar> <EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<ListItemText primary={LL.HELP_INFORMATION_1()} /> &nbsp;
</ListItemButton> <Link target="_blank" href="https://emsesp.github.io/docs" color="primary">
{LL.CLICK_HERE()}
</Link>
</ListItemText>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemButton component="a" href="https://discord.gg/3J3GgnzpyT"> <ListItemAvatar>
<ListItemAvatar> <CommentIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<Avatar sx={{ bgcolor: '#72caf9' }}> </ListItemAvatar>
<CommentIcon /> <ListItemText>
</Avatar> {LL.HELP_INFORMATION_2()}&nbsp;
</ListItemAvatar> <EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<ListItemText primary={LL.HELP_INFORMATION_2()} /> &nbsp;
</ListItemButton> <Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
{LL.CLICK_HERE()}
</Link>
</ListItemText>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemButton component="a" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"> <ListItemAvatar>
<ListItemAvatar> <GitHubIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<Avatar sx={{ bgcolor: '#72caf9' }}> </ListItemAvatar>
<GitHubIcon /> <ListItemText>
</Avatar> {LL.HELP_INFORMATION_3()}&nbsp;
</ListItemAvatar> <EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<ListItemText primary={LL.HELP_INFORMATION_3()} /> <Link target="_blank" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">
</ListItemButton> {LL.CLICK_HERE()}
</Link>
<br />
</ListItemText>
</ListItem> </ListItem>
</List> </List>
<Box p={2} color="warning.main"> <Box color="warning.main">
<Typography mb={1} variant="body2"> <Typography mb={1} variant="body2">
{LL.HELP_INFORMATION_4()} {LL.HELP_INFORMATION_4()}
</Typography> </Typography>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'info')}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
</Box> </Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => callAPI('system', 'info')}>
{LL.SUPPORT_INFORMATION(0)}
</Button>
<Button <Button
sx={{ ml: 2 }} sx={{ ml: 2 }}
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
@@ -109,7 +102,7 @@ const Help: FC = () => {
All Values All Values
</Button> </Button>
<Box border={1} p={1} mt={4}> <Box border={1} p={1} mt={4} color="orange">
<Typography align="center" variant="subtitle1" color="orange"> <Typography align="center" variant="subtitle1" color="orange">
<b>{LL.HELP_INFORMATION_5()}</b> <b>{LL.HELP_INFORMATION_5()}</b>
</Typography> </Typography>

View File

@@ -0,0 +1,37 @@
import { Tab } from '@mui/material';
import { Navigate, Route, Routes } from 'react-router-dom';
import SettingsApplication from './SettingsApplication';
import SettingsCustomEntities from './SettingsCustomEntities';
import SettingsCustomization from './SettingsCustomization';
import SettingsScheduler from './SettingsScheduler';
import type { FC } from 'react';
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Settings: FC = () => {
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.SETTINGS_OF(''));
return (
<>
<RouterTabs value={routerTab}>
<Tab value="/settings/application" label={LL.APPLICATION_SETTINGS()} />
<Tab value="/settings/customization" label={LL.CUSTOMIZATIONS()} />
<Tab value="/settings/scheduler" label={LL.SCHEDULER()} />
<Tab value="/settings/customentities" label={LL.CUSTOM_ENTITIES(0)} />
</RouterTabs>
<Routes>
<Route path="application" element={<SettingsApplication />} />
<Route path="customization" element={<SettingsCustomization />} />
<Route path="scheduler" element={<SettingsScheduler />} />
<Route path="customentities" element={<SettingsCustomEntities />} />
<Route path="*" element={<Navigate replace to="/settings/application" />} />
</Routes>
</>
);
};
export default Settings;

View File

@@ -20,8 +20,7 @@ import {
ValidatedTextField, ValidatedTextField,
ButtonRow, ButtonRow,
MessageBox, MessageBox,
BlockNavigation, BlockNavigation
useLayoutTitle
} from 'components'; } from 'components';
import RestartMonitor from 'framework/system/RestartMonitor'; import RestartMonitor from 'framework/system/RestartMonitor';
@@ -37,7 +36,7 @@ export function boardProfileSelectItems() {
)); ));
} }
const ApplicationSettings: FC = () => { const SettingsApplication: FC = () => {
const { const {
loadData, loadData,
saveData, saveData,
@@ -98,8 +97,6 @@ const ApplicationSettings: FC = () => {
}); });
}; };
useLayoutTitle(LL.APPLICATION_SETTINGS());
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -139,7 +136,7 @@ const ApplicationSettings: FC = () => {
return ( return (
<> <>
<Typography sx={{ pb: 1 }} variant="h6" color="primary"> <Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.INTERFACE_BOARD_PROFILE()} {LL.INTERFACE_BOARD_PROFILE()}
</Typography> </Typography>
<Box color="warning.main"> <Box color="warning.main">
@@ -683,11 +680,11 @@ const ApplicationSettings: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : content()} {restarting ? <RestartMonitor /> : content()}
</SectionContent> </SectionContent>
); );
}; };
export default ApplicationSettings; export default SettingsApplication;

View File

@@ -13,17 +13,17 @@ import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog'; import SettingsCustomEntitiesDialog from './SettingsCustomEntitiesDialog';
import * as EMSESP from './api'; import * as EMSESP from './api';
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types'; import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import { entityItemValidation } from './validators'; import { entityItemValidation } from './validators';
import type { EntityItem } from './types'; import type { EntityItem } from './types';
import type { FC } from 'react'; import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation, useLayoutTitle } from 'components'; import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const CustomEntities: FC = () => { const SettingsCustomEntities: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
@@ -31,8 +31,6 @@ const CustomEntities: FC = () => {
const [creating, setCreating] = useState<boolean>(false); const [creating, setCreating] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
useLayoutTitle(LL.CUSTOM_ENTITIES(0));
const { const {
data: entities, data: entities,
send: fetchEntities, send: fetchEntities,
@@ -248,7 +246,7 @@ const CustomEntities: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main"> <Box mb={2} color="warning.main">
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography> <Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
@@ -267,7 +265,7 @@ const CustomEntities: FC = () => {
/> />
)} )}
<Box mt={1} display="flex" flexWrap="wrap"> <Box display="flex" flexWrap="wrap">
<Box flexGrow={1}> <Box flexGrow={1}>
{numChanges > 0 && ( {numChanges > 0 && (
<ButtonRow> <ButtonRow>
@@ -300,4 +298,4 @@ const CustomEntities: FC = () => {
); );
}; };
export default CustomEntities; export default SettingsCustomEntities;

View File

@@ -30,7 +30,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils'; import { numberValue, updateValue } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
type CustomEntitiesDialogProps = { type SettingsCustomEntitiesDialogProps = {
open: boolean; open: boolean;
creating: boolean; creating: boolean;
onClose: () => void; onClose: () => void;
@@ -39,14 +39,14 @@ type CustomEntitiesDialogProps = {
validator: Schema; validator: Schema;
}; };
const CustomEntitiesDialog = ({ const SettingsCustomEntitiesDialog = ({
open, open,
creating, creating,
onClose, onClose,
onSave, onSave,
selectedItem, selectedItem,
validator validator
}: CustomEntitiesDialogProps) => { }: SettingsCustomEntitiesDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<EntityItem>(selectedItem); const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -281,4 +281,4 @@ const CustomEntitiesDialog = ({
); );
}; };
export default CustomEntitiesDialog; export default SettingsCustomEntitiesDialog;

View File

@@ -26,9 +26,9 @@ import { useState, useEffect, useCallback } from 'react';
import { useBlocker, useLocation } from 'react-router-dom'; import { useBlocker, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import SettingsCustomizationDialog from './CustomizationDialog';
import EntityMaskToggle from './EntityMaskToggle'; import EntityMaskToggle from './EntityMaskToggle';
import OptionIcon from './OptionIcon'; import OptionIcon from './OptionIcon';
import SettingsCustomizationDialog from './SettingsCustomizationDialog';
import * as EMSESP from './api'; import * as EMSESP from './api';
@@ -37,14 +37,14 @@ import type { DeviceShort, DeviceEntity } from './types';
import type { FC } from 'react'; import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { ButtonRow, SectionContent, MessageBox, BlockNavigation, useLayoutTitle } from 'components'; import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
import RestartMonitor from 'framework/system/RestartMonitor'; import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
export const APIURL = window.location.origin + '/api/'; export const APIURL = window.location.origin + '/api/';
const Customization: FC = () => { const SettingsCustomization: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
@@ -58,8 +58,6 @@ const Customization: FC = () => {
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>(); const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
useLayoutTitle(LL.CUSTOMIZATIONS());
// fetch devices first // fetch devices first
const { data: devices } = useRequest(EMSESP.readDevices); const { data: devices } = useRequest(EMSESP.readDevices);
@@ -510,6 +508,9 @@ const Customization: FC = () => {
const renderContent = () => ( const renderContent = () => (
<> <>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DEVICE_ENTITIES()}
</Typography>
{devices && renderDeviceList()} {devices && renderDeviceList()}
{deviceEntities && renderDeviceData()} {deviceEntities && renderDeviceData()}
{restartNeeded && ( {restartNeeded && (
@@ -543,7 +544,7 @@ const Customization: FC = () => {
</ButtonRow> </ButtonRow>
)} )}
</Box> </Box>
<ButtonRow mt={1}> <ButtonRow>
<Button <Button
startIcon={<SettingsBackupRestoreIcon />} startIcon={<SettingsBackupRestoreIcon />}
variant="outlined" variant="outlined"
@@ -560,7 +561,7 @@ const Customization: FC = () => {
); );
return ( return (
<SectionContent> <SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : renderContent()} {restarting ? <RestartMonitor /> : renderContent()}
{selectedDeviceEntity && ( {selectedDeviceEntity && (
@@ -575,4 +576,4 @@ const Customization: FC = () => {
); );
}; };
export default Customization; export default SettingsCustomization;

View File

@@ -31,7 +31,7 @@ type SettingsCustomizationDialogProps = {
selectedItem: DeviceEntity; selectedItem: DeviceEntity;
}; };
const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => { const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem); const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
@@ -152,4 +152,4 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
); );
}; };
export default CustomizationDialog; export default SettingsCustomizationDialog;

View File

@@ -11,18 +11,18 @@ import { updateState, useRequest } from 'alova';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useBlocker } from 'react-router-dom'; import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import SettingsSchedulerDialog from './SchedulerDialog'; import SettingsSchedulerDialog from './SettingsSchedulerDialog';
import * as EMSESP from './api'; import * as EMSESP from './api';
import { ScheduleFlag } from './types'; import { ScheduleFlag } from './types';
import { schedulerItemValidation } from './validators'; import { schedulerItemValidation } from './validators';
import type { ScheduleItem } from './types'; import type { ScheduleItem } from './types';
import type { FC } from 'react'; import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation, useLayoutTitle } from 'components'; import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const Scheduler: FC = () => { const SettingsScheduler: FC = () => {
const { LL, locale } = useI18nContext(); const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
@@ -194,8 +194,6 @@ const Scheduler: FC = () => {
</> </>
); );
useLayoutTitle(LL.SCHEDULER());
return ( return (
<Table <Table
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }} data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
@@ -251,7 +249,7 @@ const Scheduler: FC = () => {
}; };
return ( return (
<SectionContent> <SectionContent title={LL.SCHEDULER()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main"> <Box mb={2} color="warning.main">
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography> <Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
@@ -270,7 +268,7 @@ const Scheduler: FC = () => {
/> />
)} )}
<Box mt={1} display="flex" flexWrap="wrap"> <Box display="flex" flexWrap="wrap">
<Box flexGrow={1}> <Box flexGrow={1}>
{numChanges !== 0 && ( {numChanges !== 0 && (
<ButtonRow> <ButtonRow>
@@ -300,4 +298,4 @@ const Scheduler: FC = () => {
); );
}; };
export default Scheduler; export default SettingsScheduler;

View File

@@ -32,7 +32,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils'; import { updateValue } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
type SchedulerDialogProps = { type SettingsSchedulerDialogProps = {
open: boolean; open: boolean;
creating: boolean; creating: boolean;
onClose: () => void; onClose: () => void;
@@ -42,7 +42,15 @@ type SchedulerDialogProps = {
dow: string[]; dow: string[];
}; };
const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, validator, dow }: SchedulerDialogProps) => { const SettingsSchedulerDialog = ({
open,
creating,
onClose,
onSave,
selectedItem,
validator,
dow
}: SettingsSchedulerDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem); const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -238,4 +246,4 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
); );
}; };
export default SchedulerDialog; export default SettingsSchedulerDialog;

View File

@@ -1,130 +0,0 @@
import RefreshIcon from '@mui/icons-material/Refresh';
import { Button } from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useEffect } from 'react';
import * as EMSESP from './api';
import type { Stat } from './types';
import type { Translation } from 'i18n/i18n-types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const SystemActivity: FC = () => {
const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity);
const { LL } = useI18nContext();
useLayoutTitle(LL.ACTIVITY());
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
`,
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
height: 36px;
border-bottom: 1px solid #565656;
}
`,
Row: `
.td {
padding: 8px;
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
&:nth-of-type(even) .td {
background-color: #1e1e1e;
}
`,
BaseCell: `
&:not(:first-of-type) {
text-align: center;
}
`
});
useEffect(() => {
const timer = setInterval(() => loadData(), 30000);
return () => {
clearInterval(timer);
};
});
const showName = (id: any) => {
const name: keyof Translation['STATUS_NAMES'] = id;
return LL.STATUS_NAMES[name]();
};
const showQuality = (stat: Stat) => {
if (stat.q === 0 || stat.s + stat.f === 0) {
return;
}
if (stat.q === 100) {
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
}
if (stat.q >= 95) {
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
} else {
return <div style={{ color: 'red' }}>{stat.q}%</div>;
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize />
<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>{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>
</Row>
))}
</Body>
</>
)}
</Table>
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
);
};
return <SectionContent>{content()}</SectionContent>;
};
export default SystemActivity;

View File

@@ -1,7 +1,7 @@
import type { import type {
APIcall, APIcall,
Settings, Settings,
Activity, Status,
CoreData, CoreData,
Devices, Devices,
DeviceEntity, DeviceEntity,
@@ -25,7 +25,7 @@ export const readDeviceData = (id: number) =>
}); });
export const writeDeviceValue = (data: any) => alovaInstance.Post('/rest/writeDeviceValue', data); export const writeDeviceValue = (data: any) => alovaInstance.Post('/rest/writeDeviceValue', data);
// Application Settings // SettingsApplication
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings'); export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
export const writeSettings = (data: any) => alovaInstance.Post('/rest/settings', data); export const writeSettings = (data: any) => alovaInstance.Post('/rest/settings', data);
export const getBoardProfile = (boardProfile: string) => export const getBoardProfile = (boardProfile: string) =>
@@ -33,18 +33,17 @@ export const getBoardProfile = (boardProfile: string) =>
params: { boardProfile } params: { boardProfile }
}); });
// Sensors // DashboardSensors
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData'); export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) => export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
alovaInstance.Post('/rest/writeTemperatureSensor', ts); alovaInstance.Post('/rest/writeTemperatureSensor', ts);
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as); export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as);
// Activity // DashboardStatus
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity'); export const readStatus = () => alovaInstance.Get<Status>('/rest/status');
export const scanDevices = () => alovaInstance.Post('/rest/scanDevices'); export const scanDevices = () => alovaInstance.Post('/rest/scanDevices');
// API, used in HelpInformation // HelpInformation
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall); export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
// UploadFileForm // UploadFileForm

View File

@@ -50,7 +50,13 @@ export interface Stat {
q: number; // quality q: number; // quality
} }
export interface Activity { export interface Status {
status: busConnectionStatus;
tx_mode: number;
uptime: number;
num_devices: number;
num_sensors: number;
num_analogs: number;
stats: Stat[]; stats: Stat[];
} }
@@ -354,7 +360,6 @@ export const enum DeviceType {
TEMPERATURESENSOR, TEMPERATURESENSOR,
ANALOGSENSOR, ANALOGSENSOR,
SCHEDULER, SCHEDULER,
CUSTOM,
BOILER, BOILER,
THERMOSTAT, THERMOSTAT,
MIXER, MIXER,
@@ -368,9 +373,7 @@ export const enum DeviceType {
EXTENSION, EXTENSION,
GENERIC, GENERIC,
HEATSOURCE, HEATSOURCE,
VENTILATION, CUSTOM,
WATER,
POOL,
UNKNOWN UNKNOWN
} }

View File

@@ -22,26 +22,6 @@ export const GPIO_VALIDATOR = {
} }
}; };
export const GPIO_VALIDATORR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
if (
value &&
(value === 1 ||
(value >= 6 && value <= 11) ||
(value >= 16 && value <= 17) ||
value === 20 ||
value === 24 ||
(value >= 28 && value <= 31) ||
value > 40 ||
value < 0)
) {
callback('Must be an valid GPIO port');
} else {
callback();
}
}
};
export const GPIO_VALIDATORC3 = { export const GPIO_VALIDATORC3 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) { if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
@@ -89,14 +69,6 @@ export const createSettingsValidator = (settings: Settings) =>
tx_gpio: [{ required: true, message: 'Tx 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] rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
}), }),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32R' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORR],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORR],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORR],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORR],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORR]
}),
...(settings.board_profile === 'CUSTOM' && ...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-C3' && { settings.platform === 'ESP32-C3' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORC3], led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORC3],
@@ -221,15 +193,7 @@ export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: bo
n: [{ required: true, message: 'Name is required' }], n: [{ required: true, message: 'Name is required' }],
g: [ g: [
{ required: true, message: 'GPIO is required' }, { required: true, message: 'GPIO is required' },
platform === 'ESP32-S3' platform === 'ESP32-S3' ? GPIO_VALIDATORS3 : platform === 'ESP32-C3' ? GPIO_VALIDATORC3 : GPIO_VALIDATOR,
? GPIO_VALIDATORS3
: platform === 'ESP32-S2'
? GPIO_VALIDATORS2
: platform === 'ESP32-C3'
? GPIO_VALIDATORC3
: platform === 'ESP32R'
? GPIO_VALIDATORR
: GPIO_VALIDATOR,
...(creating ? [isGPIOUniqueValidator(sensors)] : []) ...(creating ? [isGPIOUniqueValidator(sensors)] : [])
] ]
}); });

View File

@@ -10,14 +10,14 @@ export enum APNetworkStatus {
LINGERING = 2 LINGERING = 2
} }
export interface APStatusType { export interface APStatus {
status: APNetworkStatus; status: APNetworkStatus;
ip_address: string; ip_address: string;
mac_address: string; mac_address: string;
station_num: number; station_num: number;
} }
export interface APSettingsType { export interface APSettings {
provision_mode: APProvisionMode; provision_mode: APProvisionMode;
ssid: string; ssid: string;
password: string; password: string;

View File

@@ -9,7 +9,7 @@ export enum MqttDisconnectReason {
TCP_DISCONNECTED = 7 TCP_DISCONNECTED = 7
} }
export interface MqttStatusType { export interface MqttStatus {
enabled: boolean; enabled: boolean;
connected: boolean; connected: boolean;
client_id: string; client_id: string;
@@ -19,7 +19,7 @@ export interface MqttStatusType {
connect_count: number; connect_count: number;
} }
export interface MqttSettingsType { export interface MqttSettings {
enabled: boolean; enabled: boolean;
host: string; host: string;
port: number; port: number;
@@ -36,7 +36,6 @@ export interface MqttSettingsType {
publish_time_thermostat: number; publish_time_thermostat: number;
publish_time_solar: number; publish_time_solar: number;
publish_time_mixer: number; publish_time_mixer: number;
publish_time_water: number;
publish_time_other: number; publish_time_other: number;
publish_time_sensor: number; publish_time_sensor: number;
publish_time_heartbeat: number; publish_time_heartbeat: number;

View File

@@ -20,7 +20,7 @@ export enum WiFiEncryptionType {
WIFI_AUTH_WPA2_WPA3_PSK = 7 WIFI_AUTH_WPA2_WPA3_PSK = 7
} }
export interface NetworkStatusType { export interface NetworkStatus {
status: NetworkConnectionStatus; status: NetworkConnectionStatus;
local_ip: string; local_ip: string;
local_ipv6: string; local_ipv6: string;
@@ -36,7 +36,7 @@ export interface NetworkStatusType {
hostname: string; hostname: string;
} }
export interface NetworkSettingsType { export interface NetworkSettings {
ssid: string; ssid: string;
bssid: string; bssid: string;
password: string; password: string;

View File

@@ -4,14 +4,14 @@ export enum NTPSyncStatus {
NTP_ACTIVE = 2 NTP_ACTIVE = 2
} }
export interface NTPStatusType { export interface NTPStatus {
status: NTPSyncStatus; status: NTPSyncStatus;
utc_time: string; utc_time: string;
local_time: string; local_time: string;
server: string; server: string;
} }
export interface NTPSettingsType { export interface NTPSettings {
enabled: boolean; enabled: boolean;
server: string; server: string;
tz_label: string; tz_label: string;

View File

@@ -1,11 +1,11 @@
export interface UserType { export interface User {
username: string; username: string;
password: string; password: string;
admin: boolean; admin: boolean;
} }
export interface SecuritySettingsType { export interface SecuritySettings {
users: UserType[]; users: User[];
jwt_secret: string; jwt_secret: string;
} }

View File

@@ -1,6 +1,4 @@
import type { busConnectionStatus } from 'project/types'; export interface SystemStatus {
export interface ESPSystemStatus {
emsesp_version: string; emsesp_version: string;
esp_platform: string; esp_platform: string;
max_alloc_heap: number; max_alloc_heap: number;
@@ -18,29 +16,14 @@ export interface ESPSystemStatus {
app_free: number; app_free: number;
fs_used: number; fs_used: number;
fs_free: number; fs_free: number;
uptime: string;
free_mem: number; free_mem: number;
psram_size?: number; psram_size?: number;
free_psram?: number; free_psram?: number;
has_loader: boolean; has_loader: boolean;
} }
export interface SystemStatus { export interface OTASettings {
emsesp_version: string;
esp_platform: string;
status: busConnectionStatus;
uptime: number;
bus_uptime: number;
num_devices: number;
num_sensors: number;
num_analogs: number;
free_heap: number;
ntp_status: number;
ota_status: boolean;
mqtt_status: boolean;
ap_status: boolean;
}
export interface OTASettingsType {
enabled: boolean; enabled: boolean;
port: number; port: number;
password: string; password: string;

View File

@@ -1,9 +1,9 @@
import Schema from 'async-validator'; import Schema from 'async-validator';
import { IP_ADDRESS_VALIDATOR } from './shared'; import { IP_ADDRESS_VALIDATOR } from './shared';
import type { APSettingsType } from 'types'; import type { APSettings } from 'types';
import { isAPEnabled } from 'framework/ap/APSettings'; import { isAPEnabled } from 'framework/ap/APSettingsForm';
export const createAPSettingsValidator = (apSettings: APSettingsType) => export const createAPSettingsValidator = (apSettings: APSettings) =>
new Schema({ new Schema({
provision_mode: { required: true, message: 'Please provide a provision mode' }, provision_mode: { required: true, message: 'Please provide a provision mode' },
...(isAPEnabled(apSettings) && { ...(isAPEnabled(apSettings) && {

View File

@@ -1,8 +1,8 @@
import Schema from 'async-validator'; import Schema from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from './shared'; import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
import type { MqttSettingsType } from 'types'; import type { MqttSettings } from 'types';
export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) => export const createMqttSettingsValidator = (mqttSettings: MqttSettings) =>
new Schema({ new Schema({
...(mqttSettings.enabled && { ...(mqttSettings.enabled && {
host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR], host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],

View File

@@ -1,8 +1,8 @@
import Schema from 'async-validator'; import Schema from 'async-validator';
import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared'; import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared';
import type { NetworkSettingsType } from 'types'; import type { NetworkSettings } from 'types';
export const createNetworkSettingsValidator = (networkSettings: NetworkSettingsType) => export const createNetworkSettingsValidator = (networkSettings: NetworkSettings) =>
new Schema({ new Schema({
ssid: [{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }], ssid: [{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }],
bssid: [{ type: 'string', max: 17, message: 'BSSID must be 17 characters or empty' }], bssid: [{ type: 'string', max: 17, message: 'BSSID must be 17 characters or empty' }],

View File

@@ -1,6 +1,6 @@
import Schema from 'async-validator'; import Schema from 'async-validator';
import type { InternalRuleItem } from 'async-validator'; import type { InternalRuleItem } from 'async-validator';
import type { UserType } from 'types'; import type { User } from 'types';
export const SECURITY_SETTINGS_VALIDATOR = new Schema({ export const SECURITY_SETTINGS_VALIDATOR = new Schema({
jwt_secret: [ jwt_secret: [
@@ -9,7 +9,7 @@ export const SECURITY_SETTINGS_VALIDATOR = new Schema({
] ]
}); });
export const createUniqueUsernameValidator = (users: UserType[]) => ({ export const createUniqueUsernameValidator = (users: User[]) => ({
validator(rule: InternalRuleItem, username: string, callback: (error?: string) => void) { validator(rule: InternalRuleItem, username: string, callback: (error?: string) => void) {
if (username && users.find((u) => u.username === username)) { if (username && users.find((u) => u.username === username)) {
callback('Username already in use'); callback('Username already in use');
@@ -19,7 +19,7 @@ export const createUniqueUsernameValidator = (users: UserType[]) => ({
} }
}); });
export const createUserValidator = (users: UserType[], creating: boolean) => export const createUserValidator = (users: User[], creating: boolean) =>
new Schema({ new Schema({
username: [ username: [
{ required: true, message: 'Username is required' }, { required: true, message: 'Username is required' },

View File

@@ -29,7 +29,7 @@ export default defineConfig(({ command, mode }) => {
}; };
} }
if (mode === 'hosted') { if (command === 'build' && mode === 'hosted') {
return { return {
plugins: [preact(), viteTsconfigPaths()], plugins: [preact(), viteTsconfigPaths()],
build: { build: {
@@ -38,94 +38,97 @@ export default defineConfig(({ command, mode }) => {
}; };
} }
return { // production build, both for hosted and building the firmware
plugins: [ if (command === 'build') {
preact(), return {
viteTsconfigPaths(), plugins: [
splitVendorChunkPlugin(), preact(),
{ viteTsconfigPaths(),
...viteImagemin({ splitVendorChunkPlugin(),
verbose: false, {
gifsicle: { ...viteImagemin({
optimizationLevel: 7, verbose: false,
interlaced: false gifsicle: {
}, optimizationLevel: 7,
optipng: { interlaced: false
optimizationLevel: 7 },
}, optipng: {
mozjpeg: { optimizationLevel: 7
quality: 20 },
}, mozjpeg: {
pngquant: { quality: 20
quality: [0.8, 0.9], },
speed: 4 pngquant: {
}, quality: [0.8, 0.9],
svgo: { speed: 4
plugins: [ },
{ svgo: {
name: 'removeViewBox' plugins: [
}, {
{ name: 'removeViewBox'
name: 'removeEmptyAttrs', },
active: false {
} name: 'removeEmptyAttrs',
] active: false
} }
}), ]
enforce: 'pre' }
}, }),
visualizer({ enforce: 'pre'
template: 'treemap', // or sunburst
open: false,
gzipSize: true,
brotliSize: true,
filename: '../analyse.html' // will be saved in project's root
})
],
build: {
// target: 'es2022',
chunkSizeWarningLimit: 1024,
minify: 'terser',
terserOptions: {
compress: {
passes: 4,
arrows: true,
drop_console: true,
drop_debugger: true,
sequences: true
}, },
mangle: { visualizer({
// toplevel: true template: 'treemap', // or sunburst
// module: true open: false,
// properties: { gzipSize: true,
// regex: /^_/ brotliSize: true,
// } filename: 'analyse.html' // will be saved in project's root
}, })
ecma: 5, ],
enclose: false,
keep_classnames: false,
keep_fnames: false,
ie8: false,
module: false,
nameCache: null,
safari10: false,
toplevel: false
},
rollupOptions: { build: {
output: { // target: 'es2022',
manualChunks(id: string) { chunkSizeWarningLimit: 1024,
if (id.includes('node_modules')) { minify: 'terser',
// creating a chunk to react routes deps. Reducing the vendor chunk size terserOptions: {
if (id.includes('react-router-dom') || id.includes('@remix-run') || id.includes('react-router')) { compress: {
return '@react-router'; passes: 4,
arrows: true,
drop_console: true,
drop_debugger: true,
sequences: true
},
mangle: {
// toplevel: true
// module: true
// properties: {
// regex: /^_/
// }
},
ecma: 5,
enclose: false,
keep_classnames: false,
keep_fnames: false,
ie8: false,
module: false,
nameCache: null,
safari10: false,
toplevel: false
},
rollupOptions: {
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
// creating a chunk to react routes deps. Reducing the vendor chunk size
if (id.includes('react-router-dom') || id.includes('@remix-run') || id.includes('react-router')) {
return '@react-router';
}
return 'vendor';
} }
return 'vendor';
} }
} }
} }
} }
} };
}; }
}); });

View File

@@ -1459,7 +1459,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react@npm:*": "@types/react@npm:*, @types/react@npm:^18.2.69":
version: 18.2.69 version: 18.2.69
resolution: "@types/react@npm:18.2.69" resolution: "@types/react@npm:18.2.69"
dependencies: dependencies:
@@ -1470,16 +1470,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react@npm:^18.2.72":
version: 18.2.72
resolution: "@types/react@npm:18.2.72"
dependencies:
"@types/prop-types": "npm:*"
csstype: "npm:^3.0.2"
checksum: 10/0c15461eeb8cd5153b48446d4aae681ae1d4f45fa5828fc53c12aaac9dfe426a64259a284737f6abfc3bea36df9507c129d7065ebb390b21349ad30128385ac1
languageName: node
linkType: hard
"@types/responselike@npm:^1.0.0": "@types/responselike@npm:^1.0.0":
version: 1.0.3 version: 1.0.3
resolution: "@types/responselike@npm:1.0.3" resolution: "@types/responselike@npm:1.0.3"
@@ -1512,15 +1502,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/eslint-plugin@npm:^7.4.0": "@typescript-eslint/eslint-plugin@npm:^7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/eslint-plugin@npm:7.4.0" resolution: "@typescript-eslint/eslint-plugin@npm:7.3.1"
dependencies: dependencies:
"@eslint-community/regexpp": "npm:^4.5.1" "@eslint-community/regexpp": "npm:^4.5.1"
"@typescript-eslint/scope-manager": "npm:7.4.0" "@typescript-eslint/scope-manager": "npm:7.3.1"
"@typescript-eslint/type-utils": "npm:7.4.0" "@typescript-eslint/type-utils": "npm:7.3.1"
"@typescript-eslint/utils": "npm:7.4.0" "@typescript-eslint/utils": "npm:7.3.1"
"@typescript-eslint/visitor-keys": "npm:7.4.0" "@typescript-eslint/visitor-keys": "npm:7.3.1"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
graphemer: "npm:^1.4.0" graphemer: "npm:^1.4.0"
ignore: "npm:^5.2.4" ignore: "npm:^5.2.4"
@@ -1533,44 +1523,44 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 10/9bd8852c7e4e9608c3fded94f7c60506cc7d2b6d8a8c1cad6d48969a7363751b20282874e55ccdf180635cf204cb10b3e1e5c3d1cff34d4fcd07762be3fc138e checksum: 10/8ed276113a714d93ab3ababb1179e4785bd9378e6d97726519ea1d2ac502a94475e0be988c2ec427dcfc1e6950329d58da6e64131ee87028fce63493461cc51a
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/parser@npm:^7.4.0": "@typescript-eslint/parser@npm:^7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/parser@npm:7.4.0" resolution: "@typescript-eslint/parser@npm:7.3.1"
dependencies: dependencies:
"@typescript-eslint/scope-manager": "npm:7.4.0" "@typescript-eslint/scope-manager": "npm:7.3.1"
"@typescript-eslint/types": "npm:7.4.0" "@typescript-eslint/types": "npm:7.3.1"
"@typescript-eslint/typescript-estree": "npm:7.4.0" "@typescript-eslint/typescript-estree": "npm:7.3.1"
"@typescript-eslint/visitor-keys": "npm:7.4.0" "@typescript-eslint/visitor-keys": "npm:7.3.1"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
peerDependencies: peerDependencies:
eslint: ^8.56.0 eslint: ^8.56.0
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 10/142a9e1187d305ed43b4fef659c36fa4e28359467198c986f0955c70b4067c9799f4c85d9881fbf099c55dfb265e30666e28b3ef290520e242b45ca7cb8e4ca9 checksum: 10/018326010fec1dcefd75809ccac5102a475bf1e052d824b898d707e7c0bf3e51e101164b410d1b2a513628985c96eb412538644d2005e26b99a22db6eb9402df
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/scope-manager@npm:7.4.0": "@typescript-eslint/scope-manager@npm:7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/scope-manager@npm:7.4.0" resolution: "@typescript-eslint/scope-manager@npm:7.3.1"
dependencies: dependencies:
"@typescript-eslint/types": "npm:7.4.0" "@typescript-eslint/types": "npm:7.3.1"
"@typescript-eslint/visitor-keys": "npm:7.4.0" "@typescript-eslint/visitor-keys": "npm:7.3.1"
checksum: 10/8cf9292444f9731017a707cac34bef5ae0eb33b5cd42ed07fcd046e981d97889d9201d48e02f470f2315123f53771435e10b1dc81642af28a11df5352a8e8be2 checksum: 10/7384d1f46d7f3678a1135a1ac0bd8b6dfa2f01e93b19e2510c7082766cf6983a1bf80b4ccf498651199a81d9f2bdb65101fd7a19226a723260514204d0c30b34
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/type-utils@npm:7.4.0": "@typescript-eslint/type-utils@npm:7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/type-utils@npm:7.4.0" resolution: "@typescript-eslint/type-utils@npm:7.3.1"
dependencies: dependencies:
"@typescript-eslint/typescript-estree": "npm:7.4.0" "@typescript-eslint/typescript-estree": "npm:7.3.1"
"@typescript-eslint/utils": "npm:7.4.0" "@typescript-eslint/utils": "npm:7.3.1"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
ts-api-utils: "npm:^1.0.1" ts-api-utils: "npm:^1.0.1"
peerDependencies: peerDependencies:
@@ -1578,23 +1568,23 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 10/a8bd0929d8237679b2b8a7817f070a4b9658ee976882fba8ff37e4a70dd33f87793e1b157771104111fe8054eaa8ad437a010b6aa465072fbdb932647125db2d checksum: 10/fae9003a76a8f2a2a4bb88dc0f82c0a1ca0688633183fac391920e7124a12807aac84bb287a21f61e99523c15223d1c08e7680685ebf21d07429604cba6c420b
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/types@npm:7.4.0": "@typescript-eslint/types@npm:7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/types@npm:7.4.0" resolution: "@typescript-eslint/types@npm:7.3.1"
checksum: 10/2782c5bf65cd3dfa9cd32bc3023676bbca22144987c3f6c6b67fd96c73d4a60b85a57458c49fd11b9971ac6531824bb3ae0664491e7a6de25d80c523c9be92b7 checksum: 10/c9c8eae1cf937cececd99a253bd65eb71b40206e79cf917ad9c3b3ab80cc7ce5fefb2804f9fd2a70e7438951f0d1e63df3031fc61e3a08dfef5fde208a12e0ed
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/typescript-estree@npm:7.4.0": "@typescript-eslint/typescript-estree@npm:7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/typescript-estree@npm:7.4.0" resolution: "@typescript-eslint/typescript-estree@npm:7.3.1"
dependencies: dependencies:
"@typescript-eslint/types": "npm:7.4.0" "@typescript-eslint/types": "npm:7.3.1"
"@typescript-eslint/visitor-keys": "npm:7.4.0" "@typescript-eslint/visitor-keys": "npm:7.3.1"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
globby: "npm:^11.1.0" globby: "npm:^11.1.0"
is-glob: "npm:^4.0.3" is-glob: "npm:^4.0.3"
@@ -1604,34 +1594,34 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 10/162ec9d7582f45588342e1be36fdb60e41f50bbdfbc3035c91b517ff5d45244f776921c88d88e543e1c7d0f1e6ada5474a8316b78f1b0e6d2233b101bc45b166 checksum: 10/363ad9864b56394b4000dff7c2b77d0ea52042c3c20e3b86c0f3c66044915632d9890255527c6f3a5ef056886dec72e38fbcfce49d4ad092c160440f54128230
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/utils@npm:7.4.0": "@typescript-eslint/utils@npm:7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/utils@npm:7.4.0" resolution: "@typescript-eslint/utils@npm:7.3.1"
dependencies: dependencies:
"@eslint-community/eslint-utils": "npm:^4.4.0" "@eslint-community/eslint-utils": "npm:^4.4.0"
"@types/json-schema": "npm:^7.0.12" "@types/json-schema": "npm:^7.0.12"
"@types/semver": "npm:^7.5.0" "@types/semver": "npm:^7.5.0"
"@typescript-eslint/scope-manager": "npm:7.4.0" "@typescript-eslint/scope-manager": "npm:7.3.1"
"@typescript-eslint/types": "npm:7.4.0" "@typescript-eslint/types": "npm:7.3.1"
"@typescript-eslint/typescript-estree": "npm:7.4.0" "@typescript-eslint/typescript-estree": "npm:7.3.1"
semver: "npm:^7.5.4" semver: "npm:^7.5.4"
peerDependencies: peerDependencies:
eslint: ^8.56.0 eslint: ^8.56.0
checksum: 10/ffed27e770c486cd000ff892d9049b0afe8b9d6318452a5355b78a37436cbb414bceacae413a2ac813f3e584684825d5e0baa2e6376b7ad6013a108ac91bc19d checksum: 10/234d9d65fe5d0f4a31345bd8f5a6f2879a578b3a531a14c2b3edaa7fb587c71d26249f86c41857382c0405384dc104955c02b588b3cee6fc2734f1ae40aef07b
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/visitor-keys@npm:7.4.0": "@typescript-eslint/visitor-keys@npm:7.3.1":
version: 7.4.0 version: 7.3.1
resolution: "@typescript-eslint/visitor-keys@npm:7.4.0" resolution: "@typescript-eslint/visitor-keys@npm:7.3.1"
dependencies: dependencies:
"@typescript-eslint/types": "npm:7.4.0" "@typescript-eslint/types": "npm:7.3.1"
eslint-visitor-keys: "npm:^3.4.1" eslint-visitor-keys: "npm:^3.4.1"
checksum: 10/70dc99f2ad116c6e2d9e55af249e4453e06bba2ceea515adef2d2e86e97e557865bb1b1d467667462443eb0d624baba36f7442fd1082f3874339bbc381c26e93 checksum: 10/163a93597c1d696920a19b3c1627d02368bdd52059f811c0fadd680c38034bb6418ebefe99d8ce26e0dd44ae184f18fab186af775de1a8771256be1a7905c174
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1658,11 +1648,11 @@ __metadata:
"@types/imagemin": "npm:^8.0.5" "@types/imagemin": "npm:^8.0.5"
"@types/lodash-es": "npm:^4.17.12" "@types/lodash-es": "npm:^4.17.12"
"@types/node": "npm:^20.11.30" "@types/node": "npm:^20.11.30"
"@types/react": "npm:^18.2.72" "@types/react": "npm:^18.2.69"
"@types/react-dom": "npm:^18.2.22" "@types/react-dom": "npm:^18.2.22"
"@types/react-router-dom": "npm:^5.3.3" "@types/react-router-dom": "npm:^5.3.3"
"@typescript-eslint/eslint-plugin": "npm:^7.4.0" "@typescript-eslint/eslint-plugin": "npm:^7.3.1"
"@typescript-eslint/parser": "npm:^7.4.0" "@typescript-eslint/parser": "npm:^7.3.1"
alova: "npm:^2.18.0" alova: "npm:^2.18.0"
async-validator: "npm:^4.2.5" async-validator: "npm:^4.2.5"
concurrently: "npm:^8.2.2" concurrently: "npm:^8.2.2"
@@ -1692,7 +1682,7 @@ __metadata:
terser: "npm:^5.29.2" terser: "npm:^5.29.2"
typesafe-i18n: "npm:^5.26.2" typesafe-i18n: "npm:^5.26.2"
typescript: "npm:^5.4.3" typescript: "npm:^5.4.3"
vite: "npm:^5.2.6" vite: "npm:^5.2.4"
vite-plugin-imagemin: "npm:^0.6.1" vite-plugin-imagemin: "npm:^0.6.1"
vite-tsconfig-paths: "npm:^4.3.2" vite-tsconfig-paths: "npm:^4.3.2"
languageName: unknown languageName: unknown
@@ -8352,9 +8342,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite@npm:^5.2.6": "vite@npm:^5.2.4":
version: 5.2.6 version: 5.2.4
resolution: "vite@npm:5.2.6" resolution: "vite@npm:5.2.4"
dependencies: dependencies:
esbuild: "npm:^0.20.1" esbuild: "npm:^0.20.1"
fsevents: "npm:~2.3.3" fsevents: "npm:~2.3.3"
@@ -8388,7 +8378,7 @@ __metadata:
optional: true optional: true
bin: bin:
vite: bin/vite.js vite: bin/vite.js
checksum: 10/0409acd4bbad1bca42b2015ac5d0f710bbc84b86f6b518add9a9c13adf1aab02fd40fcca854dc08ff2a2226c1df77d5d5b4a958c6c4c04ca27a6bfb0b4f60615 checksum: 10/ed3d4fa2023642dd578e90eb02876e01729fda0af196304bb1c3a7f037b457f1a505bfa36c10f28d0466945b9da7d7972219716554d43ff3c025f26ed3d89e11
languageName: node languageName: node
linkType: hard linkType: hard

View File

@@ -190,7 +190,7 @@ void AsyncEventSourceClient::_queueMessage(AsyncEventSourceMessage *dataMessage)
//length() is not thread-safe, thus acquiring the lock before this call.. //length() is not thread-safe, thus acquiring the lock before this call..
_lockmq.lock(); _lockmq.lock();
if(_messageQueue.length() >= SSE_MAX_QUEUED_MESSAGES){ if(_messageQueue.length() >= SSE_MAX_QUEUED_MESSAGES){
// ets_printf(String(F("ERROR: Too many messages queued\n")).c_str()); ets_printf(String(F("ERROR: Too many messages queued\n")).c_str());
delete dataMessage; delete dataMessage;
} else { } else {
_messageQueue.add(dataMessage); _messageQueue.add(dataMessage);

View File

@@ -460,7 +460,7 @@ void AsyncWebSocketClient::_queueMessage(std::shared_ptr<std::vector<uint8_t>> b
if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES)
{ {
l.unlock(); l.unlock();
//ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued, closing connection\n"); ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued, closing connection\n");
_status = WS_DISCONNECTED; _status = WS_DISCONNECTED;
if (_client) _client->close(true); if (_client) _client->close(true);
return; return;
@@ -1272,9 +1272,9 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String& key, AsyncWebSocket
(String&)key += WS_STR_UUID; (String&)key += WS_STR_UUID;
mbedtls_sha1_context ctx; mbedtls_sha1_context ctx;
mbedtls_sha1_init(&ctx); mbedtls_sha1_init(&ctx);
mbedtls_sha1_starts(&ctx); mbedtls_sha1_starts_ret(&ctx);
mbedtls_sha1_update(&ctx, (const unsigned char*)key.c_str(), key.length()); mbedtls_sha1_update_ret(&ctx, (const unsigned char*)key.c_str(), key.length());
mbedtls_sha1_finish(&ctx, hash); mbedtls_sha1_finish_ret(&ctx, hash);
mbedtls_sha1_free(&ctx); mbedtls_sha1_free(&ctx);
#endif #endif
base64_encodestate _state; base64_encodestate _state;

View File

@@ -76,17 +76,10 @@ static bool getMD5(uint8_t * data, uint16_t len, char * output){//33 bytes or mo
return false; return false;
memset(_buf, 0x00, 16); memset(_buf, 0x00, 16);
#ifdef ESP32 #ifdef ESP32
#if ESP_ARDUINO_VERSION_MAJOR < 3
mbedtls_md5_init(&_ctx); mbedtls_md5_init(&_ctx);
mbedtls_md5_starts_ret(&_ctx); mbedtls_md5_starts_ret(&_ctx);
mbedtls_md5_update_ret(&_ctx, data, len); mbedtls_md5_update_ret(&_ctx, data, len);
mbedtls_md5_finish_ret(&_ctx, _buf); mbedtls_md5_finish_ret(&_ctx, _buf);
#else
mbedtls_md5_init(&_ctx);
mbedtls_md5_starts(&_ctx);
mbedtls_md5_update(&_ctx, data, len);
mbedtls_md5_finish(&_ctx, _buf);
#endif
#else #else
MD5Init(&_ctx); MD5Init(&_ctx);
MD5Update(&_ctx, data, len); MD5Update(&_ctx, data, len);

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