62 Commits

Author SHA1 Message Date
proddy
f444ca31e0 Merge remote-tracking branch 'origin/dev' 2024-10-27 11:41:47 +01:00
proddy
1b70b55989 update demo url 2024-08-10 14:58:48 +02:00
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
387 changed files with 38590 additions and 29268 deletions

View File

@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -21,7 +21,7 @@ _Make sure your have performed every step and checked the applicable boxes befor
- [ ] Searched the issue in [issues](https://github.com/emsesp/EMS-ESP32/issues) - [ ] Searched the issue in [issues](https://github.com/emsesp/EMS-ESP32/issues)
- [ ] Searched the issue in [discussions](https://github.com/emsesp/EMS-ESP32/discussions) - [ ] Searched the issue in [discussions](https://github.com/emsesp/EMS-ESP32/discussions)
- [ ] Searched the issue in the [docs](https://docs.emsesp.org/Troubleshooting/) - [ ] Searched the issue in the [docs](https://emsesp.org/Troubleshooting/)
- [ ] Searched the issue in the [chat](https://discord.gg/3J3GgnzpyT) - [ ] Searched the issue in the [chat](https://discord.gg/3J3GgnzpyT)
- [ ] Provide the System information in the area below, taken from `http://<IP>/api/system` - [ ] Provide the System information in the area below, taken from `http://<IP>/api/system`

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: EMS-ESP Docs - name: EMS-ESP Docs
url: https://docs.emsesp.org url: https://emsesp.org
about: All the information related to EMS-ESP. about: All the information related to EMS-ESP.
- name: EMS-ESP Discussions and Support - name: EMS-ESP Discussions and Support
url: https://github.com/emsesp/EMS-ESP32/discussions url: https://github.com/emsesp/EMS-ESP32/discussions

1
.github/SUPPORT.md vendored
View File

@@ -1 +0,0 @@
# Support

View File

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

View File

@@ -1,37 +0,0 @@
name: 'Pre-check on PR'
on:
workflow_dispatch:
pull_request:
branches: dev
paths:
- '**.c'
- '**.cpp'
- '**.h'
- '**.hpp'
- '**.json'
- '**.py'
- '**.md'
- '.github/workflows/pr_check.yml'
jobs:
pre-release:
name: 'Automatic pre-release build'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PlatformIO
run: |
pip install wheel
pip install -U platformio
- name: Build native
run: |
platformio run -e native

View File

@@ -1,64 +1,55 @@
name: 'Build dev release' name: 'pre-release'
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
paths:
- 'src/emsesp_version.h'
branches: branches:
- 'dev' - 'dev'
permissions:
contents: write
jobs: jobs:
pre-release: pre-release:
name: 'Build Dev Release' name: 'Automatic pre-release build'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install python 3.13
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Enable Corepack - name: Enable Corepack
run: corepack enable pnpm run: corepack enable
- name: Get the EMS-ESP version - name: Install python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Get EMS-ESP version
id: build_info id: build_info
run: | run: |
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'` version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
echo "VERSION=$version" >> $GITHUB_OUTPUT echo "VERSION=$version" >> $GITHUB_OUTPUT
- name: Install PlatformIO - name: Install PlatformIO
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -U platformio pip install -U platformio
python -m pip install intelhex
- name: Build the WebUI - name: Build WebUI
run: | run: |
cd interface cd interface
pnpm install yarn install
pnpm typesafe-i18n --no-watch yarn typesafe-i18n --no-watch
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
pnpm build yarn build
pnpm webUI yarn webUI
- name: Build all PIO target environments - name: Build all PIO target environments from default_envs
run: | run: |
platformio run platformio run
env:
NO_BUILD_WEBUI: true
- name: Create GitHub Release - name: Create GitHub Release
id: 'automatic_releases' id: 'automatic_releases'

View File

@@ -20,12 +20,15 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install Build Wrapper
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@master
- name: Run Build Wrapper
run: build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Install sonar-scanner and build-wrapper
uses: SonarSource/sonarcloud-github-c-cpp@v2
- name: Run build-wrapper
run: build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
- name: Run sonar-scanner
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: sonar-scanner --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json"

View File

@@ -1,23 +0,0 @@
name: "Mark or close stale issues and PRs"
on:
schedule:
- cron: "30 1 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30
days-before-close: 5
stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment otherwise this will be closed in 5 days."
stale-pr-message: "This PR has been automatically marked as stale because there has been no activity in last 30 days. It will be closed if no further activity occurs. Thank you for your contributions."
close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity."
close-pr-message: "This PR was automatically closed because of being stale."
stale-pr-label: "stale"
stale-issue-label: "stale"
exempt-issue-labels: "bug,enhancement,pinned,security"
exempt-pr-labels: "bug,enhancement,pinned,security"

View File

@@ -1,7 +1,4 @@
name: 'Build stable release' name: 'tagged-release'
permissions:
contents: write
on: on:
workflow_dispatch: workflow_dispatch:
@@ -11,46 +8,42 @@ on:
jobs: jobs:
tagged-release: tagged-release:
name: 'Build Stable Release' name: 'Tagged Release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install python 3.13
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Enable Corepack - name: Enable Corepack
run: corepack enable pnpm run: corepack enable
- name: Install python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install PlatformIO - name: Install PlatformIO
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -U platformio pip install -U platformio
python -m pip install intelhex
- name: Build the WebUI - name: Build WebUI
run: | run: |
cd interface cd interface
pnpm install yarn install
pnpm typesafe-i18n --no-watch yarn typesafe-i18n --no-watch
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
pnpm build yarn build
pnpm webUI yarn webUI
- name: Build all PIO target environments - name: Build all PIO target environments from default_envs
run: | run: |
platformio run platformio run
env:
NO_BUILD_WEBUI: true
- name: Create GitHub Release - name: Create GitHub Release
uses: emsesp/action-automatic-releases@v1.0.0 uses: emsesp/action-automatic-releases@v1.0.0

View File

@@ -1,4 +1,4 @@
name: 'Build test release' name: 'test-release'
on: on:
workflow_dispatch: workflow_dispatch:
@@ -6,60 +6,41 @@ on:
branches: branches:
- 'dev2' - 'dev2'
permissions:
contents: read
jobs: jobs:
pre-release: pre-release:
name: 'Build Test Release' name: 'Automatic test-release build'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
steps: steps:
- uses: actions/checkout@v4
- name: Install python 3.13 - name: Enable Corepack
uses: actions/setup-python@v5 run: corepack enable
- uses: actions/setup-python@v5
with: with:
python-version: '3.13' python-version: '3.11'
- name: Use Node.js 20.x
- name: Install Node.js 22
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: '20.x'
- name: Get EMS-ESP source code and version
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable pnpm
- name: Get the EMS-ESP version
id: build_info id: build_info
run: | run: |
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'` version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
echo "VERSION=$version" >> $GITHUB_OUTPUT echo "VERSION=$version" >> $GITHUB_OUTPUT
- name: Install PlatformIO - name: Install PlatformIO
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -U platformio pip install -U platformio
python -m pip install intelhex - name: Build WebUI
- name: Build the WebUI
run: | run: |
cd interface cd interface
pnpm install yarn install
pnpm typesafe-i18n --no-watch yarn typesafe-i18n --no-watch
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
pnpm build yarn build
pnpm webUI yarn webUI
- name: Build all target environments from default_envs
- name: Build all target environments
run: | run: |
platformio run platformio run
env:
NO_BUILD_WEBUI: true
- name: Create GitHub Release - name: Create GitHub Release
id: 'automatic_releases' id: 'automatic_releases'
uses: emsesp/action-automatic-releases@v1.0.0 uses: emsesp/action-automatic-releases@v1.0.0
@@ -71,4 +52,3 @@ jobs:
files: | files: |
CHANGELOG_LATEST.md CHANGELOG_LATEST.md
./build/firmware/*.* ./build/firmware/*.*

22
.gitignore vendored
View File

@@ -12,15 +12,17 @@ cppcheck.out.xml
# platformio # platformio
.pio .pio
pio_local.ini pio_local.ini
*_old
# OS specific # OS specific
.DS_Store .DS_Store
*Thumbs.db *Thumbs.db
# web specific # web specfic
build/ build/
dist/ dist/
/data/www /data/www
/lib/framework/WWWData.h
/interface/build /interface/build
node_modules node_modules
/interface/.eslintcache /interface/.eslintcache
@@ -28,10 +30,16 @@ stats.html
*.sln *.sln
*.sw? *.sw?
.pnp.* .pnp.*
*/.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
yarn.lock
analyse.html analyse.html
interface/vite.config.ts.timestamp* interface/vite.config.ts.timestamp*
*.local *.local
src/ESP32React/WWWData.h
# i18n generated files # i18n generated files
interface/src/i18n/i18n-react.tsx interface/src/i18n/i18n-react.tsx
@@ -63,12 +71,4 @@ words-found-verbose.txt
# sonarlint # sonarlint
compile_commands.json compile_commands.json
package.json
# pioarduino + hybrid
managed_components
dependencies.lock
CMakeLists.txt
.dummy/*
logs/*
sdkconfig.*
sdkconfig_tasmota_esp32

View File

@@ -1,6 +0,0 @@
{
"MD033": false,
"MD013": false,
"MD045": false,
"MD041": false
}

View File

@@ -5,78 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.7.2] 22 March 2025 ## [3.7.0] October 27 2024
## Added
- change enum_heatingtype for remote control [#2268](https://github.com/emsesp/EMS-ESP32/issues/2268)
- system service commands [#2182](https://github.com/emsesp/EMS-ESP32/issues/2182)
- read 0x02A5 for thermostat CT200 [#2277](https://github.com/emsesp/EMS-ESP32/issues/2277)
- add "duplicate" option to Custom Entities [#2266](https://github.com/emsesp/EMS-ESP32/discussion/2266)
- mask bits for bool custom entities
- thermostat `reduce threshold` [#2288](https://github.com/emsesp/EMS-ESP32/issues/2288)
- thermostat `absent` [#1957](https://github.com/emsesp/EMS-ESP32/issues/1957)
- CR11 thermostat [#2295](https://github.com/emsesp/EMS-ESP32/issues/2295)
- Show ESP32's CPU temp in Hardware Status
- vacation mode for the CR50 [#2403](https://github.com/emsesp/EMS-ESP32/issues/2403)
- new Console command "set admin password" to set WebUI admin password
- support nested conditions in scheduler [#2451](https://github.com/emsesp/EMS-ESP32/issues/2451)
- allow mixed case in scheduler expressions [#2457](https://github.com/emsesp/EMS-ESP32/issues/2457)
- Suprapur-o [#2470](https://github.com/emsesp/EMS-ESP32/issues/2470)
## Fixed
- long numbers of custom entities [#2267](https://github.com/emsesp/EMS-ESP32/issues/2267)
- modbus command path to `api/` [#2276](https://github.com/emsesp/EMS-ESP32/issues/2276)
- info command for devices without entity-commands [#2274](https://github.com/emsesp/EMS-ESP32/issues/2274)
- CW100 settings telegram 0x241 [#2290](https://github.com/emsesp/EMS-ESP32/issues/2290)
- modbus signed 8bit values [#2294](https://github.com/emsesp/EMS-ESP32/issues/2294)
- thermostat date [#2313](https://github.com/emsesp/EMS-ESP32/issues/2313)
- Updated unknown compressor stati "enum_hpactivity" [#2311](https://github.com/emsesp/EMS-ESP32/pull/2311)
- Underline Tab headers in WebUI
- console unit tests fixed due to changed shell output
- tx-queue overflow in some heatpump systems [#2455](https://github.com/emsesp/EMS-ESP32/issues/2455)
## Changed
- show operation in pretty telegram between src and dst [#2263](https://github.com/emsesp/EMS-ESP32/discussions/2263)
- update eModbus to 1.7.2 [#2254](https://github.com/emsesp/EMS-ESP32/issues/2254)
- modbus timeout default to 300 sec, change setting from ms to sec [#2254](https://github.com/emsesp/EMS-ESP32/issues/2254)
- update AsyncTCP and ESPAsyncWebServer to latest versions
- update Arduino pio platform to 3.10.0 and optimized flash using build flags
- Version checker in WebUI improved
- rename `remoteseltemp` to `cooltemp` [#2456](https://github.com/emsesp/EMS-ESP32/issues/2456)
## [3.7.1] 29 November 2024
## Added
- include HA "unit_of_meas", "stat_cla" and "dev_cla" attributes for Number sensors [#2149](https://github.com/emsesp/EMS-ESP32/issues/2149)
- Bosch CS6800i AW - Silent Mode + Electrical Power Reduction (HP) [#2147](https://github.com/emsesp/EMS-ESP32/issues/2147)
- `/api/system/showeralert` and `/api/system/showertimer` [#2182](https://github.com/emsesp/EMS-ESP32/issues/2182)
- MX400 [#2198](https://github.com/emsesp/EMS-ESP32/issues/2198)
- SM200 values [#2212](https://github.com/emsesp/EMS-ESP32/discussions/2212)
## Fixed
- Modbus integration in 3.7.0 missing offset [#2148](https://github.com/emsesp/EMS-ESP32/issues/2148)
- fix changing TZ in NTPsettings without clearing enable+server, added DST support [#2142](https://github.com/emsesp/EMS-ESP32/issues/2142)
- Support MQTT Discovery (AD) with Domoticz [#2177](https://github.com/emsesp/EMS-ESP32/issues/2177)
- wwExtra (dhw extra) changed from temperature reading to number
- auxheaterstatus [#2192](https://github.com/emsesp/EMS-ESP32/issues/2192)
- lastCode character check [#2189](https://github.com/emsesp/EMS-ESP32/issues/2189)
- reading too many telegram parts
- heatpump cost UOMs [#2188](https://github.com/emsesp/EMS-ESP32/issues/2188)
- analog dac output and inputs on dac pins [#2201](https://github.com/emsesp/EMS-ESP32/discussions/2201)
- api memory leak [#2216](https://github.com/emsesp/EMS-ESP32/issues/2216)
- modbus multiple mixers [#2229](https://github.com/emsesp/EMS-ESP32/issues/2229)
- Last Will (LWT) not set on MQTT Connect [#2247](https://github.com/emsesp/EMS-ESP32/issues/2247)
## Changed
- name of wwstarts2 [#2217](https://github.com/emsesp/EMS-ESP32/discussions/2217)
## [3.7.0] 27 October 2024
## **IMPORTANT! BREAKING CHANGES with 3.6.5** ## **IMPORTANT! BREAKING CHANGES with 3.6.5**
@@ -85,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The automatically generated temperature sensor ID has replaced dashes (`-`) with underscores (`_`) to be compatible with Home Assistant. - The automatically generated temperature sensor ID has replaced dashes (`-`) with underscores (`_`) to be compatible with Home Assistant.
- `api/system/info` has it's JSON key names changed to camelCase syntax. - `api/system/info` has it's JSON key names changed to camelCase syntax.
For more details go to [docs.emsesp.org](https://docs.emsesp.org/). For more details go to [www.emsesp.org](https://www.emsesp.org/).
## Added ## Added
@@ -217,7 +146,7 @@ For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
## **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,56 +1 @@
# Changelog # Changelog
For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
## [3.7.3]
## Added
- analogsensor types: NTC and RGB-Led
- Flag for HMC310 [#2465](https://github.com/emsesp/EMS-ESP32/issues/2465)
- boiler auxheatersource [#2489](https://github.com/emsesp/EMS-ESP32/discussions/2489)
- thermostat last error for RC100/300 [#2501](https://github.com/emsesp/EMS-ESP32/issues/2501)
- boiler 0xC6 telegram [#1963](https://github.com/emsesp/EMS-ESP32/issues/1963)
- CS6800i changes [#2448](https://github.com/emsesp/EMS-ESP32/issues/2448), [#2449](https://github.com/emsesp/EMS-ESP32/issues/2449)
- charging pump [#2544](https://github.com/emsesp/EMS-ESP32/issues/2544)
- hybrid CSH5800iG [#2569](https://github.com/emsesp/EMS-ESP32/issues/2569)
- add EMS Device details to Home Assistant MQTT Discovery
- disinfection command [#2601](https://github.com/emsesp/EMS-ESP32/issues/2601)
- added new board profile for upcoming BBQKees E32V2.2
- set differential pressure entity in Mixer device
- set set climate action cooling/heating in HA [#2583](https://github.com/emsesp/EMS-ESP32/issues/2583)
- Internal sensors of E32V2_2
- FW200 display options [#2610](https://github.com/emsesp/EMS-ESP32/discussions/2610)
- CR11 mode settings OFF/MANUAL depends on selTemp [#2437](https://github.com/emsesp/EMS-ESP32/issues/2437)
- Fuse settings for BBQKees boards
- Analogsensors for pulse output [#2624](https://github.com/emsesp/EMS-ESP32/discussions/2624)
- Analogsensors frequency input [#2631](https://github.com/emsesp/EMS-ESP32/discussions/2631)
- SRC plus thermostats [#2636](https://github.com/emsesp/EMS-ESP32/issues/2636)
- Greenstar 2000 [#2645](https://github.com/emsesp/EMS-ESP32/issues/2645)
- RC3xx `dhw modetype` [#2659](https://github.com/emsesp/EMS-ESP32/discussions/2659)
## Fixed
- dhw/switchtime [#2490](https://github.com/emsesp/EMS-ESP32/issues/2490)
- switch to secure mqtt [#2492](https://github.com/emsesp/EMS-ESP32/issues/2492)
- update link buttons [#2497](https://github.com/emsesp/EMS-ESP32/issues/2497)
- refresh scheduler states [#2502](https://github.com/emsesp/EMS-ESP32/discussions/2502)
- also rebuild HA config on mqtt connect for scheduler, custom and shower
- FB100 controls the hc, not the master [#2510](https://github.com/emsesp/EMS-ESP32/issues/2510)
- IPM DHW module, [#2524](https://github.com/emsesp/EMS-ESP32/issues/2524)
- charge optimization [#2543](https://github.com/emsesp/EMS-ESP32/issues/2543)
- shower active state retained, shows correctly in HA
- MQTT Command Topic with slashes [#2571](https://github.com/emsesp/EMS-ESP32/issues/2571)
- Add pulsed water meter input to V1.3 gateway with Lilygo S3 [#2550](https://github.com/emsesp/EMS-ESP32/issues/2550)
- fix missing long 10-second press of Button to perform a factory reset
- fix wwMaxPower on Junkers ZBS14 [#2609](https://github.com/emsesp/EMS-ESP32/issues/2609)
- ventilation bypass state from telegram 0x55C [#1197](https://github.com/emsesp/EMS-ESP32/issues/1197)
- set selflowtemp for ems+ boilers [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)
## Changed
- show console log with ISO date/time [#2533](https://github.com/emsesp/EMS-ESP32/discussions/2533)
- remove ESP32 CPU temperature
- updated core libraries like AsyncTCP, AsyncWebServer and Modbus
- remove command `scan deep`
- ignore repeated `forceheatingoff` commands [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)

View File

@@ -6,7 +6,7 @@ Everybody is welcome and invited to contribute to the EMS-ESP Project by:
- providing Pull Requests (Features, Fixes, suggestions) - providing Pull Requests (Features, Fixes, suggestions)
- testing new released features and report issues on your EMS equipment - testing new released features and report issues on your EMS equipment
- contributing to missing [documentation](https://docs.emsesp.org) - contributing to missing [documentation](https://emsesp.org)
This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs. This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs.
@@ -69,7 +69,7 @@ Format: `<type>(<scope>): <subject>`
## Example ## Example
```text ```
feat: add hat wobble feat: add hat wobble
^--^ ^------------^ ^--^ ^------------^
| | | |
@@ -96,7 +96,7 @@ References:
## Contributor License Agreement (CLA) ## Contributor License Agreement (CLA)
```text ```
By making a contribution to this project, I certify that: By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I (a) The contribution was created in whole or in part by me and I

View File

@@ -1,37 +1,10 @@
# #
# GNUMakefile for EMS-ESP # GNUMakefile for EMS-ESP
# This is mainly used to generate the .o files for SonarQube analysis
# #
_mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) # NUMJOBS=${NUMJOBS:-" -j10 "}
I := $(patsubst %/,%,$(dir $(_mkfile_path))) # MAKEFLAGS+="j "
ifneq ($(words $(MAKECMDGOALS)),1)
.DEFAULT_GOAL = all
%:
@$(MAKE) $@ --no-print-directory -rRf $(firstword $(MAKEFILE_LIST))
else
ifndef ECHO
T := $(shell $(MAKE) $(MAKECMDGOALS) --no-print-directory \
-nrRf $(firstword $(MAKEFILE_LIST)) \
ECHO="COUNTTHIS" | grep -c "COUNTTHIS")
N := x
C = $(words $N)$(eval N := x $N)
ECHO = python3 $(I)/scripts/echo_progress.py --stepno=$C --nsteps=$T
endif
# determine number of parallel compiles based on OS
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
EXTRA_CPPFLAGS = -D LINUX
JOBS ?= $(shell nproc)
endif
ifeq ($(UNAME_S),Darwin)
EXTRA_CPPFLAGS = -D OSX -Wno-tautological-constant-out-of-range-compare
JOBS ?= $(shell sysctl -n hw.ncpu)
endif
MAKEFLAGS += -j $(JOBS) -l $(JOBS)
# $(info Number of jobs: $(JOBS))
#---------------------------------------------------------------------- #----------------------------------------------------------------------
# Project Structure # Project Structure
@@ -42,20 +15,26 @@ MAKEFLAGS += -j $(JOBS) -l $(JOBS)
# INCLUDES is a list of directories containing header files # INCLUDES is a list of directories containing header files
# LIBRARIES is a list of directories containing libraries, this must be the top level containing include and lib # LIBRARIES is a list of directories containing libraries, this must be the top level containing include and lib
#---------------------------------------------------------------------- #----------------------------------------------------------------------
#TARGET := $(notdir $(CURDIR))
TARGET := emsesp TARGET := emsesp
BUILD := build BUILD := build
SOURCES := src/core src/devices src/web src/test lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton lib/semver lib/espMqttClient/src lib/espMqttClient/src/*
INCLUDES := src/core src/devices src/web src/test lib/* lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src INCLUDES := src lib_standalone lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/semver lib/* src/devices
LIBRARIES := LIBRARIES :=
CPPCHECK = cppcheck CPPCHECK = cppcheck
CHECKFLAGS = -q --force --std=gnu++17 # CHECKFLAGS = -q --force --std=c++17
CHECKFLAGS = -q --force --std=c++11
#---------------------------------------------------------------------- #----------------------------------------------------------------------
# Languages Standard # Languages Standard
#---------------------------------------------------------------------- #----------------------------------------------------------------------
C_STANDARD := -std=c17 C_STANDARD := -std=c17
CXX_STANDARD := -std=gnu++17 CXX_STANDARD := -std=gnu++14
# C_STANDARD := -std=c11
# CXX_STANDARD := -std=c++11
#---------------------------------------------------------------------- #----------------------------------------------------------------------
# Defined Symbols # Defined Symbols
@@ -64,7 +43,7 @@ DEFINES += -DARDUINOJSON_ENABLE -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSO
DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500 DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500
DEFINES += $(ARGS) DEFINES += $(ARGS)
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32S3\" DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.0-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
#---------------------------------------------------------------------- #----------------------------------------------------------------------
# Sources & Files # Sources & Files
@@ -98,16 +77,14 @@ CXX := /usr/bin/g++
# LDFLAGS Linker Flags # LDFLAGS Linker Flags
#---------------------------------------------------------------------- #----------------------------------------------------------------------
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE) CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
CPPFLAGS += -ggdb -g3 -MMD CPPFLAGS += -ggdb
CPPFLAGS += -flto=auto -fno-lto CPPFLAGS += -g3
CPPFLAGS += -Wall -Wextra -Werror -Wswitch-enum CPPFLAGS += -Os
CPPFLAGS += -Wno-unused-parameter -Wno-missing-braces -Wno-vla-cxx-extension
CPPFLAGS += $(EXTRA_CPPFLAGS)
CFLAGS += $(CPPFLAGS) CFLAGS += $(CPPFLAGS)
CXXFLAGS += $(CPPFLAGS) CFLAGS += -Wall -Wextra -Werror -Wswitch-enum
LDFLAGS = CFLAGS += -Wno-tautological-constant-out-of-range-compare -Wno-unused-parameter -Wno-inconsistent-missing-override -Wno-missing-braces -Wno-unused-lambda-capture -Wno-sign-compare
CXXFLAGS += $(CFLAGS) -MMD
#---------------------------------------------------------------------- #----------------------------------------------------------------------
# Compiler & Linker Commands # Compiler & Linker Commands
@@ -148,27 +125,23 @@ COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
.SILENT: $(OUTPUT) .SILENT: $(OUTPUT)
all: $(OUTPUT) all: $(OUTPUT)
@$(ECHO) Build complete.
$(OUTPUT): $(OBJS) $(OUTPUT): $(OBJS)
@mkdir -p $(@D) @mkdir -p $(@D)
@$(ECHO) Linking $@
$(LINK.o) $(LINK.o)
$(SYMBOLS.out) $(SYMBOLS.out)
$(BUILD)/%.o: %.c $(BUILD)/%.o: %.c
@mkdir -p $(@D) @mkdir -p $(@D)
@$(ECHO) Compiling $@ $(COMPILE.c)
@$(COMPILE.c)
$(BUILD)/%.o: %.cpp $(BUILD)/%.o: %.cpp
@mkdir -p $(@D) @mkdir -p $(@D)
@$(ECHO) Compiling $@ $(COMPILE.cpp)
@$(COMPILE.cpp)
$(BUILD)/%.o: %.s $(BUILD)/%.o: %.s
@mkdir -p $(@D) @mkdir -p $(@D)
@$(COMPILE.s) $(COMPILE.s)
cppcheck: $(SOURCES) cppcheck: $(SOURCES)
$(CPPCHECK) $(CHECKFLAGS) $^ $(CPPCHECK) $(CHECKFLAGS) $^
@@ -177,7 +150,6 @@ run: $(OUTPUT)
@$< @$<
.PHONY: clean .PHONY: clean
clean: clean:
@$(RM) -rf $(BUILD) $(OUTPUT) @$(RM) -rf $(BUILD) $(OUTPUT)
@@ -186,5 +158,3 @@ help:
@echo $(OUTPUT) @echo $(OUTPUT)
-include $(DEPS) -include $(DEPS)
endif

View File

@@ -1,30 +1,4 @@
<div align="center"> # ![logo](media/EMS-ESP_logo_dark.png)
<p align="center">
<a href="#">
<img src="https://raw.githubusercontent.com/emsesp/EMS-ESP32/dev/media/favicon/android-chrome-512x512.png" height="100px" />
</a>
</p>
</div>
<h1 align="center">EMS-ESP</h1>
<p align="center">
<a href="https://emsesp.org">
<img src="https://img.shields.io/badge/Website-0077b5?style=for-the-badge&logo=googlehome&logoColor=white" alt="Website" />
</a>
<a href="https://github.com/emsesp/EMS-ESP32/blob/dev/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/Contribute-ff4785?style=for-the-badge&logo=git&logoColor=white" alt="Contribute" />
</a>
<a href="https://docs.emsesp.org">
<img src="https://img.shields.io/badge/Documentation-0077b5?style=for-the-badge&logo=googledocs&logoColor=white" alt="Guides" />
</a>
<a href="https://discord.gg/3J3GgnzpyT">
<img src="https://img.shields.io/badge/Discord-7289da?style=for-the-badge&logo=discord&logoColor=white" alt="Discord" />
</a>
<a href="https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md">
<img src="https://img.shields.io/badge/Changelog-6c5ce7?style=for-the-badge&logo=git&logoColor=white" alt="Changelog" />
</a>
</p>
[![version](https://img.shields.io/github/release/emsesp/EMS-ESP32.svg?label=Latest%20Release)](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md) [![version](https://img.shields.io/github/release/emsesp/EMS-ESP32.svg?label=Latest%20Release)](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
[![release-date](https://img.shields.io/github/release-date/emsesp/EMS-ESP32.svg?label=Released)](https://github.com/emsesp/EMS-ESP32/commits/main) [![release-date](https://img.shields.io/github/release-date/emsesp/EMS-ESP32.svg?label=Released)](https://github.com/emsesp/EMS-ESP32/commits/main)
@@ -35,14 +9,14 @@
[![chat](https://img.shields.io/discord/816637840644505620.svg?style=flat-square&color=blueviolet)](https://discord.gg/3J3GgnzpyT) [![chat](https://img.shields.io/discord/816637840644505620.svg?style=flat-square&color=blueviolet)](https://discord.gg/3J3GgnzpyT)
[![GitHub stars](https://img.shields.io/github/stars/emsesp/EMS-ESP32.svg?style=social&label=Star)](https://github.com/emsesp/EMS-ESP32/stargazers) [![GitHub stars](https://img.shields.io/github/stars/emsesp/EMS-ESP32.svg?style=social&label=Star)](https://github.com/emsesp/EMS-ESP32/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/emsesp/EMS-ESP32.svg?style=social&label=Fork)](https://github.com/emsesp/EMS-ESP32/network) [![GitHub forks](https://img.shields.io/github/forks/emsesp/EMS-ESP32.svg?style=social&label=Fork)](https://github.com/emsesp/EMS-ES32P/network)
[![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/paypalme/prderbyshire/2) [![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/paypalme/prderbyshire/2)
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller to communicate with **EMS** (Energy Management System) compatible equipment from manufacturers such as Bosch, Buderus, Nefit, Junkers, Worcester, Sieger, elm.leblanc and iVT. **EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller to communicate with **EMS** (Energy Management System) compatible equipment from manufacturers such as Bosch, Buderus, Nefit, Junkers, Worcester, Sieger, elm.leblanc and iVT.
It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built. It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
## 📦&nbsp; **Key Features** ## **Key Features**
- Compatible with EMS, EMS+, EMS2, EMS Plus, Logamatic EMS, Junkers 2-wire, Heatronic 3 and 4 - Compatible with EMS, EMS+, EMS2, EMS Plus, Logamatic EMS, Junkers 2-wire, Heatronic 3 and 4
- Supporting over 120 different EMS compatible devices such as thermostats, boilers, heat pumps, mixing units, solar modules, connect modules, ventilation units, switches and more - Supporting over 120 different EMS compatible devices such as thermostats, boilers, heat pumps, mixing units, solar modules, connect modules, ventilation units, switches and more
@@ -58,39 +32,43 @@ It requires a small circuit to interface with the EMS bus which can be purchased
- A powerful Scheduler to automate tasks and trigger events based data changes - A powerful Scheduler to automate tasks and trigger events based data changes
- A Notification service to alert you of important events - A Notification service to alert you of important events
## 🚀&nbsp; **Installing** ## **Installing**
Head over to [download.emsesp.org](https://download.emsesp.org) for instructions on how to install EMS-ESP. There is also further details on which boards are supported in [this section](https://docs.emsesp.org/Installing/) of the documentation. For a quick install of the latest stable release go to [https://install.emsesp.org](https://install.emsesp.org). For other methods of installing and upgrading, and switching over to the development version go to [this section](https://emsesp.org/Getting-Started/#first-time-install) in the documentation.
## 📋&nbsp; **Documentation** If you're upgrading a BBQKees Electronics EMS Gateway and unsure which firmware to use, please refer to the [this overview](https://emsesp.org/Getting-Started/#bbqkees-electronics-ems-gateway).
Visit [emsesp.org](https://docs.emsesp.org) for more details on how to install and configure EMS-ESP. There is also a collection of Frequently Asked Questions and Troubleshooting tips with example customizations from the community. ## **Documentation**
## 💬&nbsp; **Getting Support** Visit [emsesp.org](https://emsesp.org) for more details on how to install and configure EMS-ESP. There is also a collection of Frequently Asked Questions and Troubleshooting tips with example customizations from the community.
## **Getting Support**
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT). To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
If you find an issue or have a request, see [here](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request. If you find an issue or have a request, see [here](https://emsesp.org/Support/) on how to submit a bug report or feature request.
## 🎥&nbsp; **Live Demo** ## **Live Demo**
For a live demo go to [demo.emsesp.org](https://demo.emsesp.org). Pick a language from the sign on page and log in with any username or password. Note not all features are operational as it's based on static data. For a live demo go to [demo.emsesp.org](https://demo.emsesp.org). Pick a language from the sign on page and log in with any username or password. Note not all features are operational as it's based on static data.
## 💖&nbsp; **Contributors** ## **Contributors**
EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned and maintained by both [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP) with support from [BBQKees Electronics](https://bbqkees-electronics.nl). EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned and maintained by both [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP) with support from [BBQKees Electronics](https://bbqkees-electronics.nl).
You can contact us using [this form](https://emsesp.org/Contact/).
If you like **EMS-ESP**, please give it a ✨ on GitHub, or even better fork it and contribute. You can also offer a small donation. This is an open-source project maintained by volunteers, and your support is greatly appreciated. If you like **EMS-ESP**, please give it a ✨ on GitHub, or even better fork it and contribute. You can also offer a small donation. This is an open-source project maintained by volunteers, and your support is greatly appreciated.
## 📢&nbsp; **Libraries used** ## **Libraries used**
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the core framework that provides the Web UI, which has been heavily modified - [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the core framework that provides the Web UI, which has been heavily modified
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these awesome open source libraries - [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these awesome open source libraries
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing - [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client - [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server and TCP backends - ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
## 📜&nbsp; **License** ## **License**
This program is licensed under GPL-3.0 This program is licensed under GPL-3.0

View File

@@ -1,45 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32c3_out.ld"
},
"core": "esp32",
"extra_flags": [
"-DTASMOTA_SDK",
"-DARDUINO_LOLIN_C3_MINI",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1"
],
"f_cpu": "160000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [
[
"0X303A",
"0x1001"
]
],
"mcu": "esp32c3",
"variant": "lolin_c3_mini"
},
"connectivity": [
"wifi"
],
"debug": {
"openocd_target": "esp32c3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "WEMOS LOLIN C3 Mini",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.wemos.cc/en/latest/c3/c3_mini.html",
"vendor": "WEMOS"
}

View File

@@ -1,34 +0,0 @@
{
"build": {
"core": "esp32",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "esp32"
},
"connectivity": [
"wifi",
"ethernet"
],
"debug": {
"openocd_board": "esp32.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Espressif ESP32 Dev Module",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 460800
},
"download": {
"speed": 230400
},
"url": "https://en.wikipedia.org/wiki/ESP32",
"vendor": "Espressif"
}

View File

@@ -1,47 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s2_out.ld"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DTASMOTA_SDK",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "dio",
"hwids": [
[
"0X303A",
"0x80C2"
]
],
"mcu": "esp32s2",
"variant": "lolin_s2_mini"
},
"connectivity": [
"wifi"
],
"debug": {
"openocd_target": "esp32s2.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "WEMOS LOLIN S2 Mini",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.wemos.cc/en/latest/s2/s2_mini.html",
"vendor": "WEMOS"
}

View File

@@ -1,44 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": [
"wifi"
],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Espressif ESP32-S3 16M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 460800
},
"download": {
"speed": 230400
},
"url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/",
"vendor": "Espressif"
}

View File

@@ -1,37 +0,0 @@
{
"build": {
"arduino":{
"memory_type": "opi_opi"
},
"core": "esp32",
"extra_flags": "-DBOARD_HAS_PSRAM",
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "opi",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": [
"wifi"
],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Espressif ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "32MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 460800
},
"download": {
"speed": 230400
},
"url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/",
"vendor": "Espressif"
}

View File

@@ -1,35 +0,0 @@
{
"build": {
"core": "esp32",
"extra_flags": "-DTASMOTA_SDK",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "esp32"
},
"connectivity": [
"wifi",
"ethernet"
],
"debug": {
"openocd_target": "esp32.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Espressif ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 460800
},
"download": {
"speed": 230400
},
"url": "https://en.wikipedia.org/wiki/ESP32",
"vendor": "Espressif"
}

View File

@@ -1,35 +0,0 @@
{
"build": {
"core": "esp32",
"extra_flags": "-DBOARD_HAS_PSRAM",
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "esp32"
},
"connectivity": [
"wifi",
"ethernet"
],
"debug": {
"openocd_target": "esp32.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Espressif ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 460800
},
"download": {
"speed": 230400
},
"url": "https://en.wikipedia.org/wiki/ESP32",
"vendor": "Espressif"
}

View File

@@ -1,34 +0,0 @@
{
"build": {
"core": "esp32",
"extra_flags": "-DTASMOTA_SDK",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "esp32"
},
"connectivity": [
"wifi"
],
"debug": {
"openocd_target": "esp32.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Tasmota ESP32 4M Flash, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"download": {
"speed": 230400
},
"url": "https://en.wikipedia.org/wiki/ESP32",
"vendor": "Espressif"
}

View File

@@ -1,48 +0,0 @@
{
"build": {
"core": "esp32",
"extra_flags": [
"-DARDUINO_XIAO_ESP32C6",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1"
],
"f_cpu": "160000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [
[
"0x2886",
"0x0046"
],
[
"0x303a",
"0x1001"
]
],
"mcu": "esp32c6",
"variant": "XIAO_ESP32C6"
},
"connectivity": [
"wifi",
"bluetooth",
"zigbee",
"thread"
],
"debug": {
"openocd_target": "esp32c6.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Seeed Studio XIAO ESP32C6",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "https://wiki.seeedstudio.com/XIAO_ESP32C6_Getting_Started/",
"vendor": "Seeed Studio"
}

View File

@@ -9,31 +9,5 @@
} }
], ],
"dictionaries": ["project-words"], "dictionaries": ["project-words"],
"ignorePaths": [ "ignorePaths": ["node_modules", "compile_commands.json", "WWWData.h", "**/venv/**", "lib/eModbus", "lib/ESPAsyncWebServer", "lib/espMqttClient", "analyse.html", "dist", "**/*.csv", "locale_translations.h", "TZ.tsx", "**/*.txt","build/**", "**/i18n/**", "/project-words.txt"]
"node_modules",
"compile_commands.json",
"WWWData.h", "**/venv/**",
"lib/eModbus",
"lib/ESPAsyncWebServer",
"lib/espMqttClient",
"analyse.html",
"dist",
"**/*.csv",
"**/*.md",
"**/*.py",
"locale_translations.h",
"TZ.tsx",
"**/*.txt",
"build/**",
"**/i18n/**",
"/project-words.txt",
"Makefile",
"**/*.ini",
"**/*.json",
"src/core/modbus_entity_parameters.hpp",
"sdkconfig.*",
"managed_components/**",
"pnpm-*.yaml",
"vite.config.ts"
]
} }

View File

@@ -1,85 +0,0 @@
{
"type": "settings",
"Network": {
"ssid": "my_wifi_ssid",
"bssid": "",
"password": "my_wifi_password",
"hostname": "ems-esp"
},
"AP": {
"provision_mode": 2,
"ssid": "ems-esp",
"password": "ems-esp-neo",
"channel": 1,
"ssid_hidden": false,
"max_clients": 4,
"local_ip": "192.168.4.1",
"gateway_ip": "192.168.4.1",
"subnet_mask": "255.255.255.0"
},
"MQTT": {
"enableTLS": false,
"rootCA": "",
"enabled": false,
"host": "127.0.0.1",
"port": 1883,
"base": "ems-esp",
"username": "username",
"password": "password",
"client_id": "ems-esp",
"entity_format": 1,
"publish_time_boiler": 10,
"publish_time_thermostat": 10,
"publish_time_solar": 10,
"publish_time_mixer": 10,
"publish_time_water": 10,
"publish_time_other": 60,
"publish_time_sensor": 10,
"publish_time_heartbeat": 60,
"mqtt_qos": 0,
"mqtt_retain": false,
"ha_enabled": false,
"nested_format": 1,
"discovery_prefix": "homeassistant",
"discovery_type": 0,
"publish_single": false,
"publish_single2cmd": false,
"send_response": false
},
"NTP": {
"enabled": true,
"server": "time.google.com",
"tz_label": "Europe/Amsterdam",
"tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
},
"Security": {
"jwt_secret": "ems-esp-neo",
"users": [
{
"username": "admin",
"password": "admin",
"admin": true
},
{
"username": "guest",
"password": "guest",
"admin": false
}
]
},
"Settings": {
"board_profile": "S3",
"locale": "en",
"tx_mode": 1,
"ems_bus_id": 11,
"boiler_heatingoff": false,
"hide_led": true,
"telnet_enabled": true,
"notoken_api": false,
"analog_enabled": true,
"fahrenheit": false,
"bool_format": 1,
"bool_dashboard": 1,
"enum_format": 1
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

212
dump_telegrams.csv Normal file
View File

@@ -0,0 +1,212 @@
telegram_type_id,name,is_fetched
0x04,UBAFactory,fetched
0x06,RCTime,
0x0A,EasyMonitor,fetched
0x10,UBAErrorMessage1,
0x11,UBAErrorMessage2,
0x12,RCErrorMessage,
0x13,RCErrorMessage2,
0x14,UBATotalUptime,fetched
0x15,UBAMaintenanceData,
0x16,UBAParameters,fetched
0x18,UBAMonitorFast,
0x19,UBAMonitorSlow,
0x1A,UBASetPoints,
0x1C,UBAMaintenanceStatus,
0x1E,WM10TempMessage,
0x23,JunkersSetMixer,fetched
0x26,UBASettingsWW,fetched
0x28,WeatherComp,fetched
0x2A,MC110Status,
0x2E,Meters,
0x33,UBAParameterWW,fetched
0x34,UBAMonitorWW,
0x35,UBAFlags,
0x37,WWSettings,fetched
0x38,WWTimer,fetched
0x39,WWCircTimer,fetched
0x3A,RC30WWSettings,fetched
0x3B,Energy,
0x3D,RC35Set,
0x3E,RC35Monitor,
0x3F,RC35Timer,
0x40,RC30Temp,
0x41,RC30Monitor,
0x42,RC35Timer2,
0x47,RC35Set,
0x48,RC35Monitor,
0x49,RC35Timer,
0x4C,RC35Timer2,
0x51,RC35Set,
0x52,RC35Monitor,
0x53,RC35Timer,
0x56,RC35Timer2,
0x5B,RC35Set,
0x5C,RC35Monitor,
0x5D,RC35Timer,
0x60,RC35Timer2,
0x96,SM10Config,fetched
0x97,SM10Monitor,
0x9C,WM10MonitorMessage,
0x9D,WM10SetMessage,
0xA2,RCError,
0xA3,RCOutdoorTemp,
0xA5,IBASettings,fetched
0xA7,RC30Set,
0xA9,RC30Vacation,fetched
0xAA,MMConfigMessage,fetched
0xAB,MMStatusMessage,
0xAC,MMSetMessage,
0xAF,RC20Remote,
0xB0,RC10Set,
0xB1,RC10Monitor,
0xBB,HybridSettings,fetched
0xBF,ErrorMessage,
0xC2,UBAErrorMessage3,
0xD1,UBAOutdoorTemp,
0xE3,UBAMonitorSlowPlus2,
0xE4,UBAMonitorFastPlus,
0xE5,UBAMonitorSlowPlus,
0xE6,UBAParametersPlus,fetched
0xE9,UBAMonitorWWPlus,
0xEA,UBAParameterWWPlus,fetched
0x0101,ISM1Set,fetched
0x0103,ISM1StatusMessage,fetched
0x0104,ISM2StatusMessage,
0x010C,IPMStatusMessage,
0x011E,IPMTempMessage,
0x0165,JunkersSet,
0x0166,JunkersSet,
0x0167,JunkersSet,
0x0168,JunkersSet,
0x016F,JunkersMonitor,
0x0170,JunkersMonitor,
0x0171,JunkersMonitor,
0x0172,JunkersMonitor,
0x0179,JunkersSet,
0x017A,JunkersSet,
0x017B,JunkersSet,
0x017C,JunkersSet,
0x01D3,JunkersDhw,fetched
0x023A,RC300OutdoorTemp,fetched
0x023E,PVSettings,fetched
0x0240,RC300Settings,fetched
0x0267,RC300Floordry,
0x0269,RC300Holiday1,fetched
0x0291,HPMode,fetched
0x0292,HPMode,fetched
0x0293,HPMode,fetched
0x0294,HPMode,fetched
0x029B,RC300Curves,
0x029C,RC300Curves,
0x029D,RC300Curves,
0x029E,RC300Curves,
0x029F,RC300Curves,
0x02A0,RC300Curves,
0x02A1,RC300Curves,
0x02A2,RC300Curves,
0x02A5,RC300Monitor,
0x02A6,RC300Monitor,
0x02A7,RC300Monitor,
0x02A8,RC300Monitor,
0x02A9,RC300Monitor,
0x02AA,RC300Monitor,
0x02AB,RC300Monitor,
0x02AC,RC300Monitor,
0x02AF,RC300Summer,
0x02B0,RC300Summer,
0x02B1,RC300Summer,
0x02B2,RC300Summer,
0x02B3,RC300Summer,
0x02B4,RC300Summer,
0x02B5,RC300Summer,
0x02B6,RC300Summer,
0x02B9,RC300Set,
0x02BA,RC300Set,
0x02BB,RC300Set,
0x02BC,RC300Set,
0x02BD,RC300Set,
0x02BE,RC300Set,
0x02BF,RC300Set,
0x02C0,RC300Set,
0x02CC,RC300Set2,
0x02CD,MMPLUSConfigMessage,fetched
0x02CE,RC300Set2,
0x02D0,RC300Set2,
0x02D2,RC300Set2,
0x02D5,MMPLUSConfigMessage,fetched
0x02D6,HPPump2,fetched
0x02D7,MMPLUSStatusMessage,
0x02DF,MMPLUSStatusMessage,
0x02F5,RC300WWmode,fetched
0x02F6,RC300WW2mode,fetched
0x031B,RC300WWtemp,fetched
0x031D,RC300WWmode2,
0x031E,RC300WWmode2,
0x0358,SM100SystemConfig,fetched
0x035A,SM100CircuitConfig,fetched
0x035C,SM100HeatAssist,fetched
0x035D,SM100Circuit2Config,fetched
0x035F,SM100Config1,fetched
0x0361,SM100Differential,fetched
0x0362,SM100Monitor,
0x0363,SM100Monitor2,
0x0364,SM100Status,
0x0366,SM100Config,
0x036A,SM100Status2,
0x0380,SM100CollectorConfig,fetched
0x038E,SM100Energy,fetched
0x0391,SM100Time,fetched
0x0467,HPSet,
0x0468,HPSet,
0x0469,HPSet,
0x046A,HPSet,
0x0471,RC300Summer2,
0x0472,RC300Summer2,
0x0473,RC300Summer2,
0x0474,RC300Summer2,
0x0475,RC300Summer2,
0x0476,RC300Summer2,
0x0477,RC300Summer2,
0x0478,RC300Summer2,
0x047B,HP2,
0x0484,HPSilentMode,fetched
0x0485,HpCooling,fetched
0x0486,HpInConfig,fetched
0x0488,HPValve,fetched
0x048A,HpPool,fetched
0x048B,HPPumps,fetched
0x048D,HpPower,fetched
0x048F,HpTemperatures,
0x0491,HPAdditionalHeater,fetched
0x0492,HpHeaterConfig,fetched
0x0494,UBAEnergySupplied,
0x0495,UBAInformation,
0x0499,HPDhwSettings,fetched
0x049C,HPSettings2,fetched
0x049D,HPSettings3,fetched
0x04A2,HpInput,fetched
0x04A5,HPFan,fetched
0x04A7,HPPowerLimit,fetched
0x04AA,HPPower2,fetched
0x04AE,HPEnergy,fetched
0x04AF,HPMeters,fetched
0x056B,VentilationMode,fetched
0x0583,VentilationMonitor,
0x0585,Blowerspeed,
0x0587,Bypass,
0x05BA,HpPoolStatus,fetched
0x05D9,Airquality,
0x0772,HIUSettings,
0x0779,HIUMonitor,
0x0935,EM100SetMessage,fetched
0x0936,EM100OutMessage,
0x0937,EM100TempMessage,
0x0938,EM100InputMessage,
0x0939,EM100MonitorMessage,
0x093A,EM100ConfigMessage,
0x0998,HPSettings,fetched
0x0999,HPFunctionTest,fetched
0x099B,HPFlowTemp,
0x099C,HPComp,
0x09A0,HPTemperature,
1 telegram_type_id name is_fetched
2 0x04 UBAFactory fetched
3 0x06 RCTime
4 0x0A EasyMonitor fetched
5 0x10 UBAErrorMessage1
6 0x11 UBAErrorMessage2
7 0x12 RCErrorMessage
8 0x13 RCErrorMessage2
9 0x14 UBATotalUptime fetched
10 0x15 UBAMaintenanceData
11 0x16 UBAParameters fetched
12 0x18 UBAMonitorFast
13 0x19 UBAMonitorSlow
14 0x1A UBASetPoints
15 0x1C UBAMaintenanceStatus
16 0x1E WM10TempMessage
17 0x23 JunkersSetMixer fetched
18 0x26 UBASettingsWW fetched
19 0x28 WeatherComp fetched
20 0x2A MC110Status
21 0x2E Meters
22 0x33 UBAParameterWW fetched
23 0x34 UBAMonitorWW
24 0x35 UBAFlags
25 0x37 WWSettings fetched
26 0x38 WWTimer fetched
27 0x39 WWCircTimer fetched
28 0x3A RC30WWSettings fetched
29 0x3B Energy
30 0x3D RC35Set
31 0x3E RC35Monitor
32 0x3F RC35Timer
33 0x40 RC30Temp
34 0x41 RC30Monitor
35 0x42 RC35Timer2
36 0x47 RC35Set
37 0x48 RC35Monitor
38 0x49 RC35Timer
39 0x4C RC35Timer2
40 0x51 RC35Set
41 0x52 RC35Monitor
42 0x53 RC35Timer
43 0x56 RC35Timer2
44 0x5B RC35Set
45 0x5C RC35Monitor
46 0x5D RC35Timer
47 0x60 RC35Timer2
48 0x96 SM10Config fetched
49 0x97 SM10Monitor
50 0x9C WM10MonitorMessage
51 0x9D WM10SetMessage
52 0xA2 RCError
53 0xA3 RCOutdoorTemp
54 0xA5 IBASettings fetched
55 0xA7 RC30Set
56 0xA9 RC30Vacation fetched
57 0xAA MMConfigMessage fetched
58 0xAB MMStatusMessage
59 0xAC MMSetMessage
60 0xAF RC20Remote
61 0xB0 RC10Set
62 0xB1 RC10Monitor
63 0xBB HybridSettings fetched
64 0xBF ErrorMessage
65 0xC2 UBAErrorMessage3
66 0xD1 UBAOutdoorTemp
67 0xE3 UBAMonitorSlowPlus2
68 0xE4 UBAMonitorFastPlus
69 0xE5 UBAMonitorSlowPlus
70 0xE6 UBAParametersPlus fetched
71 0xE9 UBAMonitorWWPlus
72 0xEA UBAParameterWWPlus fetched
73 0x0101 ISM1Set fetched
74 0x0103 ISM1StatusMessage fetched
75 0x0104 ISM2StatusMessage
76 0x010C IPMStatusMessage
77 0x011E IPMTempMessage
78 0x0165 JunkersSet
79 0x0166 JunkersSet
80 0x0167 JunkersSet
81 0x0168 JunkersSet
82 0x016F JunkersMonitor
83 0x0170 JunkersMonitor
84 0x0171 JunkersMonitor
85 0x0172 JunkersMonitor
86 0x0179 JunkersSet
87 0x017A JunkersSet
88 0x017B JunkersSet
89 0x017C JunkersSet
90 0x01D3 JunkersDhw fetched
91 0x023A RC300OutdoorTemp fetched
92 0x023E PVSettings fetched
93 0x0240 RC300Settings fetched
94 0x0267 RC300Floordry
95 0x0269 RC300Holiday1 fetched
96 0x0291 HPMode fetched
97 0x0292 HPMode fetched
98 0x0293 HPMode fetched
99 0x0294 HPMode fetched
100 0x029B RC300Curves
101 0x029C RC300Curves
102 0x029D RC300Curves
103 0x029E RC300Curves
104 0x029F RC300Curves
105 0x02A0 RC300Curves
106 0x02A1 RC300Curves
107 0x02A2 RC300Curves
108 0x02A5 RC300Monitor
109 0x02A6 RC300Monitor
110 0x02A7 RC300Monitor
111 0x02A8 RC300Monitor
112 0x02A9 RC300Monitor
113 0x02AA RC300Monitor
114 0x02AB RC300Monitor
115 0x02AC RC300Monitor
116 0x02AF RC300Summer
117 0x02B0 RC300Summer
118 0x02B1 RC300Summer
119 0x02B2 RC300Summer
120 0x02B3 RC300Summer
121 0x02B4 RC300Summer
122 0x02B5 RC300Summer
123 0x02B6 RC300Summer
124 0x02B9 RC300Set
125 0x02BA RC300Set
126 0x02BB RC300Set
127 0x02BC RC300Set
128 0x02BD RC300Set
129 0x02BE RC300Set
130 0x02BF RC300Set
131 0x02C0 RC300Set
132 0x02CC RC300Set2
133 0x02CD MMPLUSConfigMessage fetched
134 0x02CE RC300Set2
135 0x02D0 RC300Set2
136 0x02D2 RC300Set2
137 0x02D5 MMPLUSConfigMessage fetched
138 0x02D6 HPPump2 fetched
139 0x02D7 MMPLUSStatusMessage
140 0x02DF MMPLUSStatusMessage
141 0x02F5 RC300WWmode fetched
142 0x02F6 RC300WW2mode fetched
143 0x031B RC300WWtemp fetched
144 0x031D RC300WWmode2
145 0x031E RC300WWmode2
146 0x0358 SM100SystemConfig fetched
147 0x035A SM100CircuitConfig fetched
148 0x035C SM100HeatAssist fetched
149 0x035D SM100Circuit2Config fetched
150 0x035F SM100Config1 fetched
151 0x0361 SM100Differential fetched
152 0x0362 SM100Monitor
153 0x0363 SM100Monitor2
154 0x0364 SM100Status
155 0x0366 SM100Config
156 0x036A SM100Status2
157 0x0380 SM100CollectorConfig fetched
158 0x038E SM100Energy fetched
159 0x0391 SM100Time fetched
160 0x0467 HPSet
161 0x0468 HPSet
162 0x0469 HPSet
163 0x046A HPSet
164 0x0471 RC300Summer2
165 0x0472 RC300Summer2
166 0x0473 RC300Summer2
167 0x0474 RC300Summer2
168 0x0475 RC300Summer2
169 0x0476 RC300Summer2
170 0x0477 RC300Summer2
171 0x0478 RC300Summer2
172 0x047B HP2
173 0x0484 HPSilentMode fetched
174 0x0485 HpCooling fetched
175 0x0486 HpInConfig fetched
176 0x0488 HPValve fetched
177 0x048A HpPool fetched
178 0x048B HPPumps fetched
179 0x048D HpPower fetched
180 0x048F HpTemperatures
181 0x0491 HPAdditionalHeater fetched
182 0x0492 HpHeaterConfig fetched
183 0x0494 UBAEnergySupplied
184 0x0495 UBAInformation
185 0x0499 HPDhwSettings fetched
186 0x049C HPSettings2 fetched
187 0x049D HPSettings3 fetched
188 0x04A2 HpInput fetched
189 0x04A5 HPFan fetched
190 0x04A7 HPPowerLimit fetched
191 0x04AA HPPower2 fetched
192 0x04AE HPEnergy fetched
193 0x04AF HPMeters fetched
194 0x056B VentilationMode fetched
195 0x0583 VentilationMonitor
196 0x0585 Blowerspeed
197 0x0587 Bypass
198 0x05BA HpPoolStatus fetched
199 0x05D9 Airquality
200 0x0772 HIUSettings
201 0x0779 HIUMonitor
202 0x0935 EM100SetMessage fetched
203 0x0936 EM100OutMessage
204 0x0937 EM100TempMessage
205 0x0938 EM100InputMessage
206 0x0939 EM100MonitorMessage
207 0x093A EM100ConfigMessage
208 0x0998 HPSettings fetched
209 0x0999 HPFunctionTest fetched
210 0x099B HPFlowTemp
211 0x099C HPComp
212 0x09A0 HPTemperature

4
interface/.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

View File

@@ -4,4 +4,5 @@ dist/
src/i18n/* src/i18n/*
.prettierrc .prettierrc
.yarn/
.typesafe-i18n.json .typesafe-i18n.json

1
interface/.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -10,7 +10,8 @@ export default tseslint.config(
{ {
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: true project: true,
tsconfigRootDir: import.meta.dirname
} }
} }
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "EMS-ESP", "name": "EMS-ESP",
"version": "3.7.3", "version": "3.7.0",
"description": "EMS-ESP WebUI", "description": "EMS-ESP WebUI",
"homepage": "https://emsesp.org", "homepage": "https://emsesp.org",
"author": "proddy, emsesp.org", "author": "proddy, emsesp.org",
@@ -8,60 +8,60 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"build-hosted": "typesafe-i18n && vite build --mode hosted", "build-hosted": "typesafe-i18n --no-watch && vite build --mode hosted",
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"", "preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"npm:mock-rest\" \"vite preview\"",
"mock-rest": "bun --watch ../mock-api/restServer.ts", "mock-rest": "bun --watch ../mock-api/rest_server.ts",
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite\"", "standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-rest\" \"vite\"",
"typesafe-i18n": "typesafe-i18n --no-watch", "typesafe-i18n": "typesafe-i18n --no-watch",
"webUI": "node progmem-generator.js", "webUI": "node progmem-generator.js",
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'", "format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
"lint": "eslint . --fix" "lint": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@alova/adapter-xhr": "2.2.1", "@alova/adapter-xhr": "2.0.9",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.13.0",
"@mui/icons-material": "^7.3.4", "@mui/icons-material": "^6.1.5",
"@mui/material": "^7.3.4", "@mui/material": "^6.1.5",
"@table-library/react-table-library": "4.1.15", "@table-library/react-table-library": "4.1.7",
"alova": "3.3.4", "alova": "3.1.1",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"formidable": "^3.5.4",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"magic-string": "^0.30.19", "mime-types": "^2.1.35",
"mime-types": "^3.0.1", "preact": "^10.24.3",
"preact": "^10.27.2", "react": "^18.3.1",
"react": "^19.2.0", "react-dom": "^18.3.1",
"react-dom": "^19.2.0", "react-icons": "^5.3.0",
"react-icons": "^5.5.0", "react-router-dom": "^6.27.0",
"react-router": "^7.9.4", "react-toastify": "^10.0.6",
"react-toastify": "^11.0.5",
"typesafe-i18n": "^5.26.2", "typesafe-i18n": "^5.26.2",
"typescript": "^5.9.3" "typescript": "^5.6.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.28.4", "@babel/core": "^7.26.0",
"@eslint/js": "^9.38.0", "@eslint/js": "^9.13.0",
"@preact/compat": "^18.3.1", "@preact/compat": "^18.3.1",
"@preact/preset-vite": "^2.10.2", "@preact/preset-vite": "^2.9.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^24.9.1", "@types/formidable": "^3",
"@types/react": "^19.2.2", "@types/node": "^22.8.1",
"@types/react-dom": "^19.2.2", "@types/react": "^18.3.12",
"concurrently": "^9.2.1", "@types/react-dom": "^18.3.1",
"eslint": "^9.38.0", "@types/react-router-dom": "^5.3.3",
"eslint-config-prettier": "^10.1.8", "concurrently": "^9.0.1",
"prettier": "^3.6.2", "eslint": "^9.13.0",
"rollup-plugin-visualizer": "^6.0.5", "eslint-config-prettier": "^9.1.0",
"terser": "^5.44.0", "formidable": "^3.5.2",
"typescript-eslint": "^8.46.2", "prettier": "^3.3.3",
"vite": "^7.1.11", "rollup-plugin-visualizer": "^5.12.0",
"terser": "^5.36.0",
"typescript-eslint": "8.11.0",
"vite": "^5.4.10",
"vite-plugin-imagemin": "^0.6.1", "vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.0.1"
}, },
"packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8" "packageManager": "yarn@4.5.1"
} }

6056
interface/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
onlyBuiltDependencies:
- cwebp-bin
- esbuild
- gifsicle
- jpegtran-bin
- mozjpeg
- optipng-bin
- pngquant-bin

View File

@@ -4,7 +4,6 @@ import {
existsSync, existsSync,
readFileSync, readFileSync,
readdirSync, readdirSync,
statSync,
unlinkSync unlinkSync
} from 'fs'; } from 'fs';
import mime from 'mime-types'; import mime from 'mime-types';
@@ -13,81 +12,69 @@ import zlib from 'zlib';
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n'; const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
const INDENT = ' '; const INDENT = ' ';
const outputPath = '../src/ESP32React/WWWData.h'; const outputPath = '../lib/framework/WWWData.h';
const sourcePath = './dist'; const sourcePath = './dist';
const bytesPerLine = 20; const bytesPerLine = 20;
let totalSize = 0; var totalSize = 0;
let bundleStats = {
js: { count: 0, uncompressed: 0, compressed: 0 },
css: { count: 0, uncompressed: 0, compressed: 0 },
html: { count: 0, uncompressed: 0, compressed: 0 },
svg: { count: 0, uncompressed: 0, compressed: 0 },
other: { count: 0, uncompressed: 0, compressed: 0 }
};
const generateWWWClass = const generateWWWClass = () =>
() => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler; `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
// Bundle Statistics: // Total size is ${totalSize} bytes
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB
// - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB
// - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}%
// - Generated on: ${new Date().toISOString()}
class WWWData { class WWWData {
${INDENT}public: ${indent}public:
${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) { ${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "${f.hash}");`).join('\n')} ${fileInfo.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`).join('\n')}
${INDENT.repeat(2)}} ${indent.repeat(2)}}
}; };
`; `;
const getFilesSync = (dir, files = []) => { function getFilesSync(dir, files = []) {
readdirSync(dir, { withFileTypes: true }).forEach((entry) => { readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
const entryPath = resolve(dir, entry.name); const entryPath = resolve(dir, entry.name);
entry.isDirectory() ? getFilesSync(entryPath, files) : files.push(entryPath); if (entry.isDirectory()) {
getFilesSync(entryPath, files);
} else {
files.push(entryPath);
}
}); });
return files; return files;
}; }
const cleanAndOpen = (path) => { function cleanAndOpen(path) {
existsSync(path) && unlinkSync(path); if (existsSync(path)) {
unlinkSync(path);
}
return createWriteStream(path, { flags: 'w+' }); return createWriteStream(path, { flags: 'w+' });
}; }
const getFileType = (filePath) => {
const ext = filePath.split('.').pop().toLowerCase();
if (ext === 'js') return 'js';
if (ext === 'css') return 'css';
if (ext === 'html') return 'html';
if (ext === 'svg') return 'svg';
return 'other';
};
const writeFile = (relativeFilePath, buffer) => { const writeFile = (relativeFilePath, buffer) => {
const variable = `ESP_REACT_DATA_${fileInfo.length}`; const variable = 'ESP_REACT_DATA_' + fileInfo.length;
const mimeType = mime.lookup(relativeFilePath); const mimeType = mime.lookup(relativeFilePath);
const fileType = getFileType(relativeFilePath); var size = 0;
let size = 0; writeStream.write('const uint8_t ' + variable + '[] = {');
writeStream.write(`const uint8_t ${variable}[] = {`); // const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
const zipBuffer = zlib.gzipSync(buffer, { level: 9 }); const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
// create sha
const hashSum = crypto.createHash('sha256');
hashSum.update(zipBuffer);
const hash = hashSum.digest('hex');
zipBuffer.forEach((b) => { zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) { if (!(size % bytesPerLine)) {
writeStream.write('\n' + INDENT); writeStream.write('\n');
writeStream.write(indent);
} }
writeStream.write('0x' + b.toString(16).toUpperCase().padStart(2, '0') + ','); writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ',');
size++; size++;
}); });
size % bytesPerLine && writeStream.write('\n'); if (size % bytesPerLine) {
writeStream.write('};\n\n'); writeStream.write('\n');
}
// Update bundle statistics writeStream.write('};\n\n');
bundleStats[fileType].count++;
bundleStats[fileType].uncompressed += buffer.length;
bundleStats[fileType].compressed += zipBuffer.length;
fileInfo.push({ fileInfo.push({
uri: '/' + relativeFilePath.replace(sep, '/'), uri: '/' + relativeFilePath.replace(sep, '/'),
@@ -97,52 +84,32 @@ const writeFile = (relativeFilePath, buffer) => {
hash hash
}); });
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
totalSize += size; totalSize += size;
}; };
console.log(`Generating ${outputPath} from ${sourcePath}`); // start
console.log('Generating ' + outputPath + ' from ' + sourcePath);
const includes = ARDUINO_INCLUDES;
const indent = INDENT;
const fileInfo = []; const fileInfo = [];
const writeStream = cleanAndOpen(resolve(outputPath)); const writeStream = cleanAndOpen(resolve(outputPath));
writeStream.write(ARDUINO_INCLUDES); // includes
writeStream.write(includes);
// process static files
const buildPath = resolve(sourcePath); const buildPath = resolve(sourcePath);
for (const filePath of getFilesSync(buildPath)) { for (const filePath of getFilesSync(buildPath)) {
writeFile(relative(buildPath, filePath), readFileSync(filePath)); const readStream = readFileSync(filePath);
const relativeFilePath = relative(buildPath, filePath);
writeFile(relativeFilePath, readStream);
} }
// add class
writeStream.write(generateWWWClass()); writeStream.write(generateWWWClass());
// end
writeStream.end(); writeStream.end();
// Calculate and display bundle statistics console.log('Total size: ' + totalSize / 1000 + ' KB');
const totalUncompressed = Object.values(bundleStats).reduce(
(sum, stat) => sum + stat.uncompressed,
0
);
const totalCompressed = Object.values(bundleStats).reduce(
(sum, stat) => sum + stat.compressed,
0
);
const compressionRatio = (
((totalUncompressed - totalCompressed) / totalUncompressed) *
100
).toFixed(1);
console.log('\n📊 Bundle Size Analysis:');
console.log('='.repeat(50));
console.log(`Total compressed size: ${(totalSize / 1000).toFixed(1)} KB`);
console.log(`Total uncompressed size: ${(totalUncompressed / 1000).toFixed(1)} KB`);
console.log(`Compression ratio: ${compressionRatio}%`);
console.log('\n📁 File Type Breakdown:');
Object.entries(bundleStats).forEach(([type, stats]) => {
if (stats.count > 0) {
const ratio = (
((stats.uncompressed - stats.compressed) / stats.uncompressed) *
100
).toFixed(1);
console.log(
`${type.toUpperCase().padEnd(4)}: ${stats.count} files, ${(stats.uncompressed / 1000).toFixed(1)} KB → ${(stats.compressed / 1000).toFixed(1)} KB (${ratio}% compression)`
);
}
});
console.log('='.repeat(50));

View File

@@ -13,9 +13,8 @@
local('Roboto'), local('Roboto'),
local('Roboto-Regular'), local('Roboto-Regular'),
url(../fonts/re.woff2) format('woff2'); url(../fonts/re.woff2) format('woff2');
unicode-range: unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131,
U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0141-0144, U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC,
U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+2212, U+2215, U+FEFF, U+FFFD;
U+FEFF, U+FFFD;
} }

View File

@@ -1,44 +1,27 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ToastContainer, Zoom } from 'react-toastify'; import { Slide, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';
import AppRouting from 'AppRouting'; import AppRouting from 'AppRouting';
import CustomTheme from 'CustomTheme'; import CustomTheme from 'CustomTheme';
import TypesafeI18n from 'i18n/i18n-react'; import TypesafeI18n from 'i18n/i18n-react';
import type { Locales } from 'i18n/i18n-types'; import { detectLocale } from 'i18n/i18n-util';
import { loadLocaleAsync } from 'i18n/i18n-util.async'; import { loadLocaleAsync } from 'i18n/i18n-util.async';
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors'; import { localStorageDetector } from 'typesafe-i18n/detectors';
const availableLocales = [ const detectedLocale = detectLocale(localStorageDetector);
'de',
'en',
'it',
'fr',
'nl',
'no',
'pl',
'sk',
'sv',
'tr',
'cz'
];
const App = () => { const App = () => {
const [wasLoaded, setWasLoaded] = useState(false); const [wasLoaded, setWasLoaded] = useState(false);
const [locale, setLocale] = useState<Locales>('en');
useEffect(() => { useEffect(() => {
// determine locale, take from session if set other default to browser language void loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
const browserLocale = detectLocale('en', availableLocales, navigatorDetector);
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
localStorage.setItem('lang', newLocale);
setLocale(newLocale);
void loadLocaleAsync(newLocale).then(() => setWasLoaded(true));
}, []); }, []);
if (!wasLoaded) return null; if (!wasLoaded) return null;
return ( return (
<TypesafeI18n locale={locale}> <TypesafeI18n locale={detectedLocale}>
<CustomTheme> <CustomTheme>
<AppRouting /> <AppRouting />
<ToastContainer <ToastContainer
@@ -46,17 +29,14 @@ const App = () => {
autoClose={3000} autoClose={3000}
hideProgressBar={false} hideProgressBar={false}
newestOnTop={false} newestOnTop={false}
closeOnClick closeOnClick={true}
rtl={false} rtl={false}
pauseOnFocusLoss pauseOnFocusLoss={false}
draggable={false} draggable={false}
pauseOnHover={false} pauseOnHover={false}
transition={Zoom} transition={Slide}
closeButton={false} closeButton={false}
theme="dark" theme="light"
toastStyle={{
border: '1px solid #177ac9'
}}
/> />
</CustomTheme> </CustomTheme>
</TypesafeI18n> </TypesafeI18n>

View File

@@ -1,5 +1,5 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { Navigate, Route, Routes } from 'react-router'; import { Navigate, Route, Routes } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AuthenticatedRouting from 'AuthenticatedRouting'; import AuthenticatedRouting from 'AuthenticatedRouting';

View File

@@ -1,5 +1,5 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router'; import { Navigate, Route, Routes } from 'react-router-dom';
import CustomEntities from 'app/main/CustomEntities'; import CustomEntities from 'app/main/CustomEntities';
import Customizations from 'app/main/Customizations'; import Customizations from 'app/main/Customizations';
@@ -15,6 +15,7 @@ import DownloadUpload from 'app/settings/DownloadUpload';
import MqttSettings from 'app/settings/MqttSettings'; import MqttSettings from 'app/settings/MqttSettings';
import NTPSettings from 'app/settings/NTPSettings'; import NTPSettings from 'app/settings/NTPSettings';
import Settings from 'app/settings/Settings'; import Settings from 'app/settings/Settings';
import Version from 'app/settings/Version';
import Network from 'app/settings/network/Network'; import Network from 'app/settings/network/Network';
import Security from 'app/settings/security/Security'; import Security from 'app/settings/security/Security';
import APStatus from 'app/status/APStatus'; import APStatus from 'app/status/APStatus';
@@ -25,7 +26,6 @@ import NTPStatus from 'app/status/NTPStatus';
import NetworkStatus from 'app/status/NetworkStatus'; import NetworkStatus from 'app/status/NetworkStatus';
import Status from 'app/status/Status'; import Status from 'app/status/Status';
import SystemLog from 'app/status/SystemLog'; import SystemLog from 'app/status/SystemLog';
import Version from 'app/status/Version';
import { Layout } from 'components'; import { Layout } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
@@ -37,9 +37,10 @@ const AuthenticatedRouting = () => {
<Route path="/dashboard/*" element={<Dashboard />} /> <Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/devices/*" element={<Devices />} /> <Route path="/devices/*" element={<Devices />} />
<Route path="/sensors/*" element={<Sensors />} /> <Route path="/sensors/*" element={<Sensors />} />
<Route path="/help/*" element={<Help />} />
<Route path="/status/*" element={<Status />} /> <Route path="/status/*" element={<Status />} />
<Route path="/help/*" element={<Help />} />
<Route path="/*" element={<Navigate to="/" />} />
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} /> <Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
<Route path="/status/activity" element={<Activity />} /> <Route path="/status/activity" element={<Activity />} />
<Route path="/status/log" element={<SystemLog />} /> <Route path="/status/log" element={<SystemLog />} />
@@ -47,17 +48,17 @@ const AuthenticatedRouting = () => {
<Route path="/status/ntp" element={<NTPStatus />} /> <Route path="/status/ntp" element={<NTPStatus />} />
<Route path="/status/ap" element={<APStatus />} /> <Route path="/status/ap" element={<APStatus />} />
<Route path="/status/network" element={<NetworkStatus />} /> <Route path="/status/network" element={<NetworkStatus />} />
<Route path="/status/version" element={<Version />} />
{me.admin && ( {me.admin && (
<> <>
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/settings/version" element={<Version />} />
<Route path="/settings/application" element={<ApplicationSettings />} /> <Route path="/settings/application" element={<ApplicationSettings />} />
<Route path="/settings/mqtt" element={<MqttSettings />} /> <Route path="/settings/mqtt" element={<MqttSettings />} />
<Route path="/settings/ntp" element={<NTPSettings />} /> <Route path="/settings/ntp" element={<NTPSettings />} />
<Route path="/settings/ap" element={<APSettings />} /> <Route path="/settings/ap" element={<APSettings />} />
<Route path="/settings/modules" element={<Modules />} /> <Route path="/settings/modules" element={<Modules />} />
<Route path="/settings/downloadUpload" element={<DownloadUpload />} /> <Route path="/settings/upload" element={<DownloadUpload />} />
<Route path="/settings/network/*" element={<Network />} /> <Route path="/settings/network/*" element={<Network />} />
<Route path="/settings/security/*" element={<Security />} /> <Route path="/settings/security/*" element={<Security />} />
@@ -67,8 +68,6 @@ const AuthenticatedRouting = () => {
<Route path="/customentities" element={<CustomEntities />} /> <Route path="/customentities" element={<CustomEntities />} />
</> </>
)} )}
<Route path="/*" element={<Navigate to="/" />} />
</Routes> </Routes>
</Layout> </Layout>
); );

View File

@@ -1,7 +1,11 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { CssBaseline, ThemeProvider, responsiveFontSizes } from '@mui/material'; import { CssBaseline } from '@mui/material';
import { createTheme } from '@mui/material/styles'; import {
ThemeProvider,
createTheme,
responsiveFontSizes
} from '@mui/material/styles';
import type { RequiredChildrenProps } from 'utils'; import type { RequiredChildrenProps } from 'utils';

View File

@@ -98,7 +98,7 @@ const SignIn = () => {
<Box display="flex" flexDirection="column" alignItems="center"> <Box display="flex" flexDirection="column" alignItems="center">
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
disabled={processing} disabled={processing}
sx={{ sx={{
width: 240 width: 240
@@ -117,7 +117,7 @@ const SignIn = () => {
}} }}
/> />
<ValidatedPasswordField <ValidatedPasswordField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
disabled={processing} disabled={processing}
sx={{ sx={{
width: 240 width: 240

View File

@@ -5,7 +5,7 @@ import type {
Action, Action,
Activity, Activity,
CoreData, CoreData,
DashboardData, DashboardItem,
DeviceData, DeviceData,
DeviceEntity, DeviceEntity,
Entities, Entities,
@@ -22,7 +22,7 @@ import type {
// Dashboard // Dashboard
export const readDashboard = () => export const readDashboard = () =>
alovaInstance.Get<DashboardData>('/rest/dashboardData', { alovaInstance.Get<DashboardItem[]>('/rest/dashboardData', {
responseType: 'arraybuffer' // uses msgpack responseType: 'arraybuffer' // uses msgpack
}); });
@@ -70,7 +70,6 @@ export const readDeviceEntities = (id: number) =>
alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities`, { alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities`, {
params: { id }, params: { id },
responseType: 'arraybuffer', responseType: 'arraybuffer',
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
transform(data) { transform(data) {
return (data as DeviceEntity[]).map((de: DeviceEntity) => ({ return (data as DeviceEntity[]).map((de: DeviceEntity) => ({
...de, ...de,
@@ -93,7 +92,6 @@ export const writeDeviceName = (data: { id: number; name: string }) =>
// SettingsScheduler // SettingsScheduler
export const readSchedule = () => export const readSchedule = () =>
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', { alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
transform(data) { transform(data) {
return (data as Schedule).schedule.map((si: ScheduleItem) => ({ return (data as Schedule).schedule.map((si: ScheduleItem) => ({
...si, ...si,
@@ -131,7 +129,6 @@ export const writeModules = (data: {
// CustomEntities // CustomEntities
export const readCustomEntities = () => export const readCustomEntities = () =>
alovaInstance.Get<EntityItem[]>('/rest/customEntities', { alovaInstance.Get<EntityItem[]>('/rest/customEntities', {
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
transform(data) { transform(data) {
return (data as Entities).entities.map((ei: EntityItem) => ({ return (data as Entities).entities.map((ei: EntityItem) => ({
...ei, ...ei,
@@ -146,8 +143,7 @@ export const readCustomEntities = () =>
o_name: ei.name, o_name: ei.name,
o_writeable: ei.writeable, o_writeable: ei.writeable,
o_value: ei.value, o_value: ei.value,
o_deleted: ei.deleted, o_deleted: ei.deleted
o_hide: ei.hide
})); }));
} }
}); });

View File

@@ -22,7 +22,7 @@ export const alovaInstance = createAlova({
method.config.headers.Authorization = method.config.headers.Authorization =
'Bearer ' + localStorage.getItem(ACCESS_TOKEN); 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
} }
// for simulating very slow networks // for simulating vrey slow networks
// return new Promise((resolve) => { // return new Promise((resolve) => {
// const random = 3000 + Math.random() * 2000; // const random = 3000 + Math.random() * 2000;
// setTimeout(resolve, Math.floor(random)); // setTimeout(resolve, Math.floor(random));
@@ -57,10 +57,7 @@ export const alovaInstance = createAlova({
}); });
export const alovaInstanceGH = createAlova({ export const alovaInstanceGH = createAlova({
baseURL: baseURL: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
process.env.NODE_ENV === 'development'
? '/gh'
: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
statesHook: ReactHook, statesHook: ReactHook,
requestAdapter: xhrRequestAdapter() requestAdapter: xhrRequestAdapter()
}); });

View File

@@ -2,7 +2,7 @@ import type { LogSettings, SystemStatus } from 'types';
import { alovaInstance, alovaInstanceGH } from './endpoints'; import { alovaInstance, alovaInstanceGH } from './endpoints';
// systemStatus - also used to ping in System Monitor for pinging // systemStatus - also used to ping in Restart monitor for pinging
export const readSystemStatus = () => export const readSystemStatus = () =>
alovaInstance.Get<SystemStatus>('/rest/systemStatus'); alovaInstance.Get<SystemStatus>('/rest/systemStatus');
@@ -14,25 +14,16 @@ export const updateLogSettings = (data: LogSettings) =>
export const fetchLogES = () => alovaInstance.Get('/es/log'); export const fetchLogES = () => alovaInstance.Get('/es/log');
// Get versions from GitHub // Get versions from GitHub
// cache for 10 minutes to stop getting the IP blocked by GitHub
export const getStableVersion = () => export const getStableVersion = () =>
alovaInstanceGH.Get('latest', { alovaInstanceGH.Get('latest', {
cacheFor: 60 * 10 * 1000, transform(response: { data: { name: string } }) {
transform(response: { data: { name: string; published_at: string } }) { return response.data.name.substring(1);
return {
name: response.data.name.substring(1),
published_at: response.data.published_at
};
} }
}); });
export const getDevVersion = () => export const getDevVersion = () =>
alovaInstanceGH.Get('tags/latest', { alovaInstanceGH.Get('tags/latest', {
cacheFor: 60 * 10 * 1000, transform(response: { data: { name: string } }) {
transform(response: { data: { name: string; published_at: string } }) { return response.data.name.split(/\s+/).splice(-1)[0].substring(1);
return {
name: response.data.name.split(/\s+/).splice(-1)[0]?.substring(1) || '',
published_at: response.data.published_at
};
} }
}); });

View File

@@ -1,37 +1,40 @@
// @ts-nocheck - Optimized MessagePack unpacking library for EMS-ESP32 let decoder;
let decoder,
src,
srcEnd,
position = 0,
strings = [],
stringPosition = 0,
currentUnpackr = {},
currentStructures,
srcString,
srcStringStart = 0,
srcStringEnd = 0,
bundledStrings,
referenceMap,
dataView;
const EMPTY_ARRAY = [],
currentExtensions = [];
const defaultOptions = { useRecords: false, mapsAsObjects: true };
try { try {
decoder = new TextDecoder(); decoder = new TextDecoder();
} catch (error) {} } catch (error) {}
class C1Type {} let src;
const C1 = new C1Type(); let srcEnd;
let position = 0;
const EMPTY_ARRAY = [];
let strings = EMPTY_ARRAY;
let stringPosition = 0;
let currentUnpackr = {};
let currentStructures;
let srcString;
let srcStringStart = 0;
let srcStringEnd = 0;
let bundledStrings;
let referenceMap;
const currentExtensions = [];
let dataView;
const defaultOptions = {
useRecords: false,
mapsAsObjects: true
};
export class C1Type {}
export const C1 = new C1Type();
C1.name = 'MessagePack 0xC1'; C1.name = 'MessagePack 0xC1';
let sequentialMode = false, let sequentialMode = false;
inlineObjectReadThreshold = 2, let inlineObjectReadThreshold = 2;
readStruct, let readStruct, onLoadedStructures, onSaveState;
onLoadedStructures, // no-eval build
onSaveState;
try { try {
new Function(''); new Function('');
} catch (error) { } catch (error) {
// if eval variants are not supported, do not create inline object readers ever
inlineObjectReadThreshold = Infinity; inlineObjectReadThreshold = Infinity;
} }
export class Unpackr { export class Unpackr {
constructor(options) { constructor(options) {
if (options) { if (options) {
@@ -47,15 +50,19 @@ export class Unpackr {
if (options.structures) if (options.structures)
options.structures.sharedLength = options.structures.length; options.structures.sharedLength = options.structures.length;
else if (options.getStructures) { else if (options.getStructures) {
(options.structures = []).uninitialized = true; (options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
options.structures.sharedLength = 0; options.structures.sharedLength = 0;
} }
if (options.int64AsNumber) options.int64AsType = 'number'; if (options.int64AsNumber) {
options.int64AsType = 'number';
}
} }
Object.assign(this, options); Object.assign(this, options);
} }
unpack(source, options?: any) { unpack(source, options?: any) {
if (src) { if (src) {
// re-entrant execution, save the state and restore it after we do this unpack
return saveState(() => { return saveState(() => {
clearSource(); clearSource();
return this return this
@@ -79,6 +86,9 @@ export class Unpackr {
strings = EMPTY_ARRAY; strings = EMPTY_ARRAY;
bundledStrings = null; bundledStrings = null;
src = source; src = source;
// this provides cached access to the data view for a buffer if it is getting reused, which is a recommend
// technique for getting data from a database where it can be copied into an existing buffer instead of creating
// new ones
try { try {
dataView = dataView =
source.dataView || source.dataView ||
@@ -181,10 +191,10 @@ export class Unpackr {
return this.unpack(source, end); return this.unpack(source, end);
} }
} }
function getPosition() { export function getPosition() {
return position; return position;
} }
function checkedRead(options: any) { export function checkedRead(options: any) {
try { try {
if (!currentUnpackr.trusted && !sequentialMode) { if (!currentUnpackr.trusted && !sequentialMode) {
const sharedLength = currentStructures.sharedLength || 0; const sharedLength = currentStructures.sharedLength || 0;
@@ -254,7 +264,7 @@ function restoreStructures() {
currentStructures.restoreStructures = null; currentStructures.restoreStructures = null;
} }
function read() { export function read() {
let token = src[position++]; let token = src[position++];
if (token < 0xa0) { if (token < 0xa0) {
if (token < 0x80) { if (token < 0x80) {
@@ -579,7 +589,7 @@ const createSecondByteReader = (firstId, read0) =>
return structure.read(); return structure.read();
}; };
function loadStructures() { export function loadStructures() {
const loadedStructures = saveState(() => { const loadedStructures = saveState(() => {
// save the state in case getStructures modifies our buffer // save the state in case getStructures modifies our buffer
src = null; src = null;
@@ -595,8 +605,9 @@ var readFixedString = readStringJS;
var readString8 = readStringJS; var readString8 = readStringJS;
var readString16 = readStringJS; var readString16 = readStringJS;
var readString32 = readStringJS; var readString32 = readStringJS;
let isNativeAccelerationEnabled = false; export let isNativeAccelerationEnabled = false;
function setExtractor(extractStrings) {
export function setExtractor(extractStrings) {
isNativeAccelerationEnabled = true; isNativeAccelerationEnabled = true;
readFixedString = readString(1); readFixedString = readString(1);
readString8 = readString(2); readString8 = readString(2);
@@ -690,7 +701,7 @@ function readStringJS(length) {
return result; return result;
} }
function readString(source, start, length) { export function readString(source, start, length) {
const existingSrc = src; const existingSrc = src;
src = source; src = source;
position = start; position = start;
@@ -1054,7 +1065,7 @@ currentExtensions[0x70] = (data) => {
currentExtensions[0x73] = () => new Set(read()); currentExtensions[0x73] = () => new Set(read());
const typedArrays = [ export const typedArrays = [
'Int8', 'Int8',
'Uint8', 'Uint8',
'Uint8Clamped', 'Uint8Clamped',
@@ -1166,20 +1177,44 @@ function saveState(callback) {
dataView = new DataView(src.buffer, src.byteOffset, src.byteLength); dataView = new DataView(src.buffer, src.byteOffset, src.byteLength);
return value; return value;
} }
function clearSource() { export function clearSource() {
src = null; src = null;
referenceMap = null; referenceMap = null;
currentStructures = null; currentStructures = null;
} }
function addExtension(extension) { export function addExtension(extension) {
if (extension.unpack) currentExtensions[extension.type] = extension.unpack; if (extension.unpack) currentExtensions[extension.type] = extension.unpack;
else currentExtensions[extension.type] = extension; else currentExtensions[extension.type] = extension;
} }
const mult10 = new Array(147); export const mult10 = new Array(147); // this is a table matching binary exponents to the multiplier to determine significant digit rounding
for (let i = 0; i < 256; i++) { for (let i = 0; i < 256; i++) {
mult10[i] = +('1e' + Math.floor(45.15 - i * 0.30103)); mult10[i] = +('1e' + Math.floor(45.15 - i * 0.30103));
} }
const defaultUnpackr = new Unpackr({ useRecords: false }); export const Decoder = Unpackr;
var defaultUnpackr = new Unpackr({ useRecords: false });
export const unpack = defaultUnpackr.unpack; export const unpack = defaultUnpackr.unpack;
export const unpackMultiple = defaultUnpackr.unpackMultiple;
export const decode = defaultUnpackr.unpack;
export const FLOAT32_OPTIONS = {
NEVER: 0,
ALWAYS: 1,
DECIMAL_ROUND: 3,
DECIMAL_FIT: 4
};
const f32Array = new Float32Array(1);
const u8Array = new Uint8Array(f32Array.buffer, 0, 4);
export function roundFloat32(float32Number) {
f32Array[0] = float32Number;
const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)];
return (
((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) /
multiplier
);
}
export function setReadStruct(updatedReadStruct, loadedStructs, saveState) {
readStruct = updatedReadStruct;
onLoadedStructures = loadedStructs;
onSaveState = saveState;
}

View File

@@ -1,5 +1,5 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useBlocker } from 'react-router'; import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
@@ -57,7 +57,7 @@ const CustomEntities = () => {
if (!dialogOpen && !numChanges) { if (!dialogOpen && !numChanges) {
void fetchEntities(); void fetchEntities();
} }
}); }, 3000);
const { send: writeEntities } = useRequest( const { send: writeEntities } = useRequest(
(data: Entities) => writeCustomEntities(data), (data: Entities) => writeCustomEntities(data),
@@ -76,7 +76,6 @@ const CustomEntities = () => {
ei.factor !== ei.o_factor || ei.factor !== ei.o_factor ||
ei.value_type !== ei.o_value_type || ei.value_type !== ei.o_value_type ||
ei.writeable !== ei.o_writeable || ei.writeable !== ei.o_writeable ||
ei.hide !== ei.o_hide ||
ei.deleted !== ei.o_deleted || ei.deleted !== ei.o_deleted ||
(ei.value || '') !== (ei.o_value || '') (ei.value || '') !== (ei.o_value || '')
); );
@@ -84,7 +83,7 @@ const CustomEntities = () => {
const entity_theme = useTheme({ const entity_theme = useTheme({
Table: ` Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px; --data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 90px;
`, `,
BaseRow: ` BaseRow: `
font-size: 14px; font-size: 14px;
@@ -137,8 +136,8 @@ const CustomEntities = () => {
const saveEntities = async () => { const saveEntities = async () => {
await writeEntities({ await writeEntities({
entities: entities entities: entities
.filter((ei: EntityItem) => !ei.deleted) .filter((ei) => !ei.deleted)
.map((condensed_ei: EntityItem) => ({ .map((condensed_ei) => ({
id: condensed_ei.id, id: condensed_ei.id,
ram: condensed_ei.ram, ram: condensed_ei.ram,
name: condensed_ei.name, name: condensed_ei.name,
@@ -148,7 +147,6 @@ const CustomEntities = () => {
factor: condensed_ei.factor, factor: condensed_ei.factor,
uom: condensed_ei.uom, uom: condensed_ei.uom,
writeable: condensed_ei.writeable, writeable: condensed_ei.writeable,
hide: condensed_ei.hide,
value_type: condensed_ei.value_type, value_type: condensed_ei.value_type,
value: condensed_ei.value value: condensed_ei.value
})) }))
@@ -197,26 +195,6 @@ const CustomEntities = () => {
}); });
}; };
const onDialogDup = (item: EntityItem) => {
setCreating(true);
setSelectedEntityItem({
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
name: item.name + '_',
ram: item.ram,
device_id: item.device_id,
type_id: item.type_id,
offset: item.offset,
factor: item.factor,
uom: item.uom,
value_type: item.value_type,
writeable: item.writeable,
deleted: false,
hide: item.hide,
value: item.value
});
setDialogOpen(true);
};
const addEntityItem = () => { const addEntityItem = () => {
setCreating(true); setCreating(true);
setSelectedEntityItem({ setSelectedEntityItem({
@@ -231,7 +209,6 @@ const CustomEntities = () => {
value_type: 0, value_type: 0,
writeable: false, writeable: false,
deleted: false, deleted: false,
hide: false,
value: '' value: ''
}); });
setDialogOpen(true); setDialogOpen(true);
@@ -243,7 +220,7 @@ const CustomEntities = () => {
: typeof value === 'number' : typeof value === 'number'
? new Intl.NumberFormat().format(value) + ? new Intl.NumberFormat().format(value) +
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]) (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
: (value as string) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]); : (value as string);
} }
function showHex(value: number, digit: number) { function showHex(value: number, digit: number) {
@@ -252,17 +229,15 @@ const CustomEntities = () => {
const renderEntity = () => { const renderEntity = () => {
if (!entities) { if (!entities) {
return ( return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />;
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
);
} }
return ( return (
<Table <Table
data={{ data={{
nodes: entities nodes: entities
.filter((ei: EntityItem) => !ei.deleted) .filter((ei) => !ei.deleted)
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
}} }}
theme={entity_theme} theme={entity_theme}
layout={{ custom: true }} layout={{ custom: true }}
@@ -321,7 +296,6 @@ const CustomEntities = () => {
creating={creating} creating={creating}
onClose={onDialogClose} onClose={onDialogClose}
onSave={onDialogSave} onSave={onDialogSave}
onDup={onDialogDup}
selectedItem={selectedEntityItem} selectedItem={selectedEntityItem}
validator={entityItemValidation(entities, selectedEntityItem)} validator={entityItemValidation(entities, selectedEntityItem)}
/> />

View File

@@ -2,11 +2,7 @@ import { useEffect, useState } from 'react';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import { import {
Box, Box,
@@ -16,11 +12,11 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
TextField TextField
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator'; import type Schema from 'async-validator';
@@ -38,7 +34,6 @@ interface CustomEntitiesDialogProps {
creating: boolean; creating: boolean;
onClose: () => void; onClose: () => void;
onSave: (ei: EntityItem) => void; onSave: (ei: EntityItem) => void;
onDup: (ei: EntityItem) => void;
selectedItem: EntityItem; selectedItem: EntityItem;
validator: Schema; validator: Schema;
} }
@@ -48,7 +43,6 @@ const CustomEntitiesDialog = ({
creating, creating,
onClose, onClose,
onSave, onSave,
onDup,
selectedItem, selectedItem,
validator validator
}: CustomEntitiesDialogProps) => { }: CustomEntitiesDialogProps) => {
@@ -65,19 +59,12 @@ const CustomEntitiesDialog = ({
setEditItem({ setEditItem({
...selectedItem, ...selectedItem,
device_id: selectedItem.device_id.toString(16).toUpperCase(), device_id: selectedItem.device_id.toString(16).toUpperCase(),
type_id: selectedItem.type_id.toString(16).toUpperCase(), type_id: selectedItem.type_id.toString(16).toUpperCase()
factor:
selectedItem.value_type === DeviceValueType.BOOL
? selectedItem.factor.toString(16).toUpperCase()
: selectedItem.factor
}); });
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = ( const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -93,12 +80,6 @@ const CustomEntitiesDialog = ({
if (typeof editItem.type_id === 'string') { if (typeof editItem.type_id === 'string') {
editItem.type_id = parseInt(editItem.type_id, 16); editItem.type_id = parseInt(editItem.type_id, 16);
} }
if (
editItem.value_type === DeviceValueType.BOOL &&
typeof editItem.factor === 'string'
) {
editItem.factor = parseInt(editItem.factor, 16);
}
onSave(editItem); onSave(editItem);
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors(error as ValidateFieldsError);
@@ -110,10 +91,6 @@ const CustomEntitiesDialog = ({
onSave(editItem); onSave(editItem);
}; };
const dup = () => {
onDup(editItem);
};
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <Dialog sx={dialogStyle} open={open} onClose={handleClose}>
<DialogTitle> <DialogTitle>
@@ -126,7 +103,7 @@ const CustomEntitiesDialog = ({
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid size={12}> <Grid size={12}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="name" name="name"
label={LL.NAME(0)} label={LL.NAME(0)}
value={editItem.name} value={editItem.name}
@@ -135,20 +112,6 @@ const CustomEntitiesDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
/> />
</Grid> </Grid>
<Grid mt={3}>
<BlockFormControlLabel
control={
<Checkbox
icon={<InsertCommentOutlinedIcon htmlColor="white" />}
checkedIcon={<CommentsDisabledOutlinedIcon color="primary" />}
checked={editItem.hide}
onChange={updateFormValue}
name="hide"
/>
}
label="API/MQTT"
/>
</Grid>
<Grid> <Grid>
<TextField <TextField
name="ram" name="ram"
@@ -165,7 +128,6 @@ const CustomEntitiesDialog = ({
</TextField> </TextField>
</Grid> </Grid>
{editItem.ram === 1 && ( {editItem.ram === 1 && (
<>
<Grid> <Grid>
<TextField <TextField
name="value" name="value"
@@ -178,32 +140,13 @@ const CustomEntitiesDialog = ({
margin="normal" margin="normal"
/> />
</Grid> </Grid>
<Grid>
<TextField
name="uom"
label={LL.UNIT()}
value={editItem.uom}
margin="normal"
onChange={updateFormValue}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={val} value={i}>
{val}
</MenuItem>
))}
</TextField>
</Grid>
</>
)} )}
{editItem.ram === 0 && ( {editItem.ram === 0 && (
<> <>
<Grid mt={3}> <Grid mt={3} size={9}>
<BlockFormControlLabel <BlockFormControlLabel
control={ control={
<Checkbox <Checkbox
icon={<EditOffOutlinedIcon color="primary" />}
checkedIcon={<EditOutlinedIcon htmlColor="white" />}
checked={editItem.writeable} checked={editItem.writeable}
onChange={updateFormValue} onChange={updateFormValue}
name="writeable" name="writeable"
@@ -214,7 +157,7 @@ const CustomEntitiesDialog = ({
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="device_id" name="device_id"
label={LL.ID_OF(LL.DEVICE())} label={LL.ID_OF(LL.DEVICE())}
margin="normal" margin="normal"
@@ -234,7 +177,7 @@ const CustomEntitiesDialog = ({
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="type_id" name="type_id"
label={LL.ID_OF(LL.TYPE(1))} label={LL.ID_OF(LL.TYPE(1))}
margin="normal" margin="normal"
@@ -254,7 +197,7 @@ const CustomEntitiesDialog = ({
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="offset" name="offset"
label={LL.OFFSET()} label={LL.OFFSET()}
margin="normal" margin="normal"
@@ -312,7 +255,7 @@ const CustomEntitiesDialog = ({
<TextField <TextField
name="factor" name="factor"
label={LL.FACTOR()} label={LL.FACTOR()}
value={numberValue(editItem.factor as number)} value={numberValue(editItem.factor)}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
@@ -346,41 +289,15 @@ const CustomEntitiesDialog = ({
editItem.device_id !== '0' && ( editItem.device_id !== '0' && (
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="factor" name="factor"
label={LL.BYTES()} label="Bytes"
value={numberValue(editItem.factor as number)} value={numberValue(editItem.factor)}
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
margin="normal" margin="normal"
type="number" type="number"
slotProps={{
htmlInput: { step: '1', min: '1', max: '255' }
}}
/>
</Grid>
)}
{editItem.value_type === DeviceValueType.BOOL && (
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="factor"
label={LL.BITMASK()}
value={editItem.factor as string}
sx={{ width: '11ch' }}
variant="outlined"
onChange={updateFormValue}
margin="normal"
type="string"
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">0x</InputAdornment>
)
},
htmlInput: { style: { textTransform: 'uppercase' } }
}}
/> />
</Grid> </Grid>
)} )}
@@ -399,15 +316,6 @@ const CustomEntitiesDialog = ({
> >
{LL.REMOVE()} {LL.REMOVE()}
</Button> </Button>
<Button
sx={{ ml: 1 }}
startIcon={<AddIcon />}
variant="outlined"
color="primary"
onClick={dup}
>
{LL.DUPLICATE()}
</Button>
</Box> </Box>
)} )}
<Button <Button

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useBlocker, useLocation } from 'react-router'; import { useBlocker, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -16,7 +16,6 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
InputAdornment, InputAdornment,
Link, Link,
MenuItem, MenuItem,
@@ -25,6 +24,7 @@ import {
ToggleButtonGroup, ToggleButtonGroup,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { import {
Body, Body,
@@ -38,7 +38,7 @@ import {
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import SystemMonitor from 'app/status/SystemMonitor'; import RestartMonitor from 'app/status/RestartMonitor';
import { import {
BlockNavigation, BlockNavigation,
ButtonRow, ButtonRow,
@@ -125,22 +125,13 @@ const Customizations = () => {
const setOriginalSettings = (data: DeviceEntity[]) => { const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities( setDeviceEntities(
data.map((de) => { data.map((de) => ({
const result: DeviceEntity = {
...de, ...de,
o_m: de.m o_m: de.m,
}; o_cn: de.cn,
if (de.cn !== undefined) { o_mi: de.mi,
result.o_cn = de.cn; o_ma: de.ma
} }))
if (de.mi !== undefined) {
result.o_mi = de.mi;
}
if (de.ma !== undefined) {
result.o_ma = de.ma;
}
return result;
})
); );
}; };
@@ -253,11 +244,8 @@ const Customizations = () => {
setSelectedDevice(-1); setSelectedDevice(-1);
setSelectedDeviceTypeNameURL(''); setSelectedDeviceTypeNameURL('');
} else { } else {
const device = devices.devices[index]; setSelectedDeviceTypeNameURL(devices.devices[index].url || '');
if (device) { setSelectedDeviceName(devices.devices[index].n);
setSelectedDeviceTypeNameURL(device.url || '');
setSelectedDeviceName(device.n);
}
setNumChanges(0); setNumChanges(0);
setRestartNeeded(false); setRestartNeeded(false);
} }
@@ -318,7 +306,7 @@ const Customizations = () => {
const filter_entity = (de: DeviceEntity) => const filter_entity = (de: DeviceEntity) =>
(de.m & selectedFilters || !selectedFilters) && (de.m & selectedFilters || !selectedFilters) &&
formatName(de, true).toLowerCase().includes(search.toLowerCase()); formatName(de, true).includes(search);
const maskDisabled = (set: boolean) => { const maskDisabled = (set: boolean) => {
setDeviceEntities( setDeviceEntities(
@@ -408,20 +396,14 @@ const Customizations = () => {
await sendCustomizationEntities({ await sendCustomizationEntities({
id: selectedDevice, id: selectedDevice,
entity_ids: masked_entities entity_ids: masked_entities
}) }).catch((error: Error) => {
.then(() => {
toast.success(LL.CUSTOMIZATIONS_SAVED());
})
.catch((error: Error) => {
if (error.message === 'Reboot required') { if (error.message === 'Reboot required') {
setRestartNeeded(true); setRestartNeeded(true);
} else { } else {
toast.error(error.message); toast.error(error.message);
} }
})
.finally(() => {
setOriginalSettings(deviceEntities);
}); });
setOriginalSettings(deviceEntities);
} }
}; };
@@ -563,7 +545,7 @@ const Customizations = () => {
size="small" size="small"
color="secondary" color="secondary"
value={getMaskString(selectedFilters)} value={getMaskString(selectedFilters)}
onChange={(_, mask: string[]) => { onChange={(event, mask: string[]) => {
setSelectedFilters(getMaskNumber(mask)); setSelectedFilters(getMaskNumber(mask));
}} }}
> >
@@ -611,7 +593,7 @@ const Customizations = () => {
</Button> </Button>
</Grid> </Grid>
<Grid> <Grid>
<Typography variant="subtitle2" color="grey"> <Typography variant="subtitle2" color="primary">
{LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length} {LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length}
&nbsp;{LL.ENTITIES(deviceEntities.length)} &nbsp;{LL.ENTITIES(deviceEntities.length)}
</Typography> </Typography>
@@ -755,7 +737,7 @@ const Customizations = () => {
return ( return (
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <SystemMonitor /> : renderContent()} {restarting ? <RestartMonitor /> : renderContent()}
{selectedDeviceEntity && ( {selectedDeviceEntity && (
<SettingsCustomizationsDialog <SettingsCustomizationsDialog
open={dialogOpen} open={dialogOpen}

View File

@@ -10,10 +10,10 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
TextField, TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -54,10 +54,7 @@ const CustomizationsDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = ( const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }

View File

@@ -1,12 +1,10 @@
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { IconContext } from 'react-icons/lib'; import { IconContext } from 'react-icons/lib';
import { Link } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'; import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import { import {
@@ -14,20 +12,16 @@ import {
IconButton, IconButton,
ToggleButton, ToggleButton,
ToggleButtonGroup, ToggleButtonGroup,
Tooltip,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { Body, Cell, Row, Table } from '@table-library/react-table-library/table'; import { Body, Cell, Row, Table } 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 { CellTree, useTree } from '@table-library/react-table-library/tree'; import { CellTree, useTree } from '@table-library/react-table-library/tree';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import { import { FormLoader, SectionContent, useLayoutTitle } from 'components';
ButtonTooltip,
FormLoader,
MessageBox,
SectionContent,
useLayoutTitle
} 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 { useInterval, usePersistState } from 'utils'; import { useInterval, usePersistState } from 'utils';
@@ -60,12 +54,13 @@ const Dashboard = () => {
const { const {
data, data,
send: fetchDashboard, send: fetchDashboard,
error error,
loading
} = useRequest(readDashboard, { } = useRequest(readDashboard, {
initialData: { connected: true, nodes: [] } initialData: []
}).onSuccess((event) => { }).onSuccess((event) => {
if (event.data.nodes.length !== parentNodes) { if (event.data.length !== parentNodes) {
setParentNodes(event.data.nodes.length); // count number of parents/devices setParentNodes(event.data.length); // count number of parents/devices
} }
}); });
@@ -76,12 +71,11 @@ const Dashboard = () => {
} }
); );
const deviceValueDialogSave = useCallback( const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
async (devicevalue: DeviceValue) => {
if (!selectedDashboardItem) { if (!selectedDashboardItem) {
return; return;
} }
const id = selectedDashboardItem.id; // this is the parent ID const id = selectedDashboardItem.parentNode.id; // this is the parent ID
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v }) await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => { .then(() => {
toast.success(LL.WRITE_CMD_SENT()); toast.success(LL.WRITE_CMD_SENT());
@@ -93,13 +87,9 @@ const Dashboard = () => {
setDeviceValueDialogOpen(false); setDeviceValueDialogOpen(false);
setSelectedDashboardItem(undefined); setSelectedDashboardItem(undefined);
}); });
}, };
[selectedDashboardItem, sendDeviceValue, LL]
);
const dashboard_theme = useMemo( const dashboard_theme = useTheme({
() =>
useTheme({
Table: ` Table: `
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px; --data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
`, `,
@@ -117,7 +107,7 @@ const Dashboard = () => {
}, },
&:hover .td { &:hover .td {
background-color: #177ac9; background-color: #177ac9;
}, }
`, `,
BaseCell: ` BaseCell: `
&:nth-of-type(2) { &:nth-of-type(2) {
@@ -127,14 +117,12 @@ const Dashboard = () => {
text-align: right; text-align: right;
} }
` `
}), });
[]
);
const tree = useTree( const tree = useTree(
{ nodes: data.nodes }, { nodes: data },
{ {
onChange: () => {} // not used but needed onChange: undefined // not used but needed
}, },
{ {
treeIcon: { treeIcon: {
@@ -161,16 +149,15 @@ const Dashboard = () => {
if (!deviceValueDialogOpen) { if (!deviceValueDialogOpen) {
void fetchDashboard(); void fetchDashboard();
} }
}); }, 3000);
useEffect(() => { useEffect(() => {
showAll showAll
? tree.fns.onAddAll(data.nodes.map((item: DashboardItem) => item.id)) // expand tree ? tree.fns.onAddAll(data.map((item: DashboardItem) => item.id)) // expand tree
: tree.fns.onRemoveAll(); // collapse tree : tree.fns.onRemoveAll(); // collapse tree
}, [parentNodes]); }, [parentNodes]);
const showType = useCallback( const showType = (n?: string, t?: number) => {
(n?: string, t?: number) => {
// if we have a name show it // if we have a name show it
if (n) { if (n) {
return n; return n;
@@ -191,44 +178,40 @@ const Dashboard = () => {
} }
} }
return ''; return '';
}, };
[LL]
);
const showName = (di: DashboardItem) => { const showName = (di: DashboardItem) => {
if (di.id < 100) { if (di.id < 100) {
// if its a device (parent node) and has entities // if its a device (parent node) and has entities
if (di.nodes?.length) { if (di.nodes?.length) {
return ( return (
<span style={{ fontWeight: 'bold', fontSize: '14px' }}> <>
<span style="font-size: 14px">
<DeviceIcon type_id={di.t ?? 0} /> <DeviceIcon type_id={di.t ?? 0} />
&nbsp;&nbsp;{showType(di.n, di.t)} &nbsp;&nbsp;{showType(di.n, di.t)}
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
</span> </span>
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
</>
); );
} }
} }
if (di.dv) { if (di.dv) {
return <span>{di.dv.id.slice(2)}</span>; return <span style="color:lightgrey">{di.dv.id.slice(2)}</span>;
} }
return null;
}; };
const hasMask = (id: string, mask: number) => const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask; (parseInt(id.slice(0, 2), 16) & mask) === mask;
const editDashboardValue = useCallback( const editDashboardValue = (di: DashboardItem) => {
(di: DashboardItem) => {
if (me.admin && di.dv?.c) { if (me.admin && di.dv?.c) {
setSelectedDashboardItem(di); setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true); setDeviceValueDialogOpen(true);
} }
}, };
[me.admin]
);
const handleShowAll = ( const handleShowAll = (
_event: React.MouseEvent<HTMLElement>, event: React.MouseEvent<HTMLElement>,
toggle: boolean | null toggle: boolean | null
) => { ) => {
if (toggle !== null) { if (toggle !== null) {
@@ -239,40 +222,26 @@ const Dashboard = () => {
const renderContent = () => { const renderContent = () => {
if (!data) { if (!data) {
return ( return <FormLoader onRetry={fetchDashboard} errorMessage={error?.message} />;
<FormLoader onRetry={fetchDashboard} errorMessage={error?.message || ''} />
);
} }
const hasFavEntities = data.nodes.filter(
(item: DashboardItem) => item.id <= 90
).length;
return ( return (
<> <>
{!data.connected && ( <Box
<MessageBox mb={2} level="error" message={LL.EMS_BUS_WARNING()} /> sx={{
)} backgroundColor: 'black',
pt: 1,
{data.connected && data.nodes.length > 0 && !hasFavEntities && ( pl: 2
<MessageBox mb={2} level="warning"> }}
<Typography> >
{LL.NO_DATA_1()}&nbsp; <Grid container spacing={0} justifyContent="flex-start">
<Link to="/customizations" style={{ color: 'white' }}> <Grid size={11}>
{LL.CUSTOMIZATIONS()} <Typography mb={2} variant="body1" color="warning">
</Link> {LL.DASHBOARD_1()}.
&nbsp;{LL.NO_DATA_2()}&nbsp;
{LL.NO_DATA_3()}&nbsp;
<Link to="/devices" style={{ color: 'white' }}>
{LL.DEVICES()}
</Link>
.
</Typography> </Typography>
</MessageBox> </Grid>
)}
{data.nodes.length > 0 && ( <Grid size={1} alignItems="end">
<>
<ToggleButtonGroup <ToggleButtonGroup
color="primary" color="primary"
size="small" size="small"
@@ -280,20 +249,16 @@ const Dashboard = () => {
exclusive exclusive
onChange={handleShowAll} onChange={handleShowAll}
> >
<ButtonTooltip title={LL.ALLVALUES()} arrow>
<ToggleButton value={true}> <ToggleButton value={true}>
<UnfoldMoreIcon sx={{ fontSize: 18 }} /> <UnfoldMoreIcon sx={{ fontSize: 14 }} />
</ToggleButton> </ToggleButton>
</ButtonTooltip>
<ButtonTooltip title={LL.COMPACT()} arrow>
<ToggleButton value={false}> <ToggleButton value={false}>
<UnfoldLessIcon sx={{ fontSize: 18 }} /> <UnfoldLessIcon sx={{ fontSize: 14 }} />
</ToggleButton> </ToggleButton>
</ButtonTooltip>
</ToggleButtonGroup> </ToggleButtonGroup>
<ButtonTooltip title={LL.DASHBOARD_1()} arrow> </Grid>
<HelpOutlineIcon color="primary" sx={{ ml: 1, fontSize: 20 }} /> </Grid>
</ButtonTooltip> </Box>
<Box <Box
padding={1} padding={1}
@@ -307,12 +272,17 @@ const Dashboard = () => {
<IconContext.Provider <IconContext.Provider
value={{ value={{
color: 'lightblue', color: 'lightblue',
size: '18', size: '16',
style: { verticalAlign: 'middle' } style: { verticalAlign: 'middle' }
}} }}
> >
{!loading && data.length === 0 ? (
<Typography variant="subtitle2" color="secondary">
{LL.NO_DATA()}
</Typography>
) : (
<Table <Table
data={{ nodes: data.nodes }} data={{ nodes: data }}
theme={dashboard_theme} theme={dashboard_theme}
layout={{ custom: true }} layout={{ custom: true }}
tree={tree} tree={tree}
@@ -329,20 +299,21 @@ const Dashboard = () => {
<> <>
<Cell>{showName(di)}</Cell> <Cell>{showName(di)}</Cell>
<Cell> <Cell>
<ButtonTooltip <Tooltip
placement="left"
title={formatValue(LL, di.dv?.v, di.dv?.u)} title={formatValue(LL, di.dv?.v, di.dv?.u)}
arrow
> >
<span>{formatValue(LL, di.dv?.v, di.dv?.u)}</span> <span style={{ color: 'lightgrey' }}>
</ButtonTooltip> {formatValue(LL, di.dv?.v, di.dv?.u)}
</span>
</Tooltip>
</Cell> </Cell>
<Cell> <Cell>
{me.admin && {me.admin &&
di.dv?.c && di.dv?.c &&
!hasMask( !hasMask(di.dv.id, DeviceEntityMask.DV_READONLY) && (
di.dv.id,
DeviceEntityMask.DV_READONLY
) && (
<IconButton <IconButton
size="small" size="small"
onClick={() => editDashboardValue(di)} onClick={() => editDashboardValue(di)}
@@ -367,11 +338,10 @@ const Dashboard = () => {
</Body> </Body>
)} )}
</Table> </Table>
)}
</IconContext.Provider> </IconContext.Provider>
</Box> </Box>
</> </>
)}
</>
); );
}; };

View File

@@ -6,20 +6,19 @@ import {
useState useState
} from 'react'; } from 'react';
import { IconContext } from 'react-icons'; import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import ConstructionIcon from '@mui/icons-material/Construction';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import HighlightOffIcon from '@mui/icons-material/HighlightOff';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined'; import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import SearchIcon from '@mui/icons-material/Search';
import StarIcon from '@mui/icons-material/Star'; import StarIcon from '@mui/icons-material/Star';
import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined'; import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined';
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
@@ -31,16 +30,17 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
IconButton, IconButton,
InputAdornment,
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
TextField, Tooltip,
ToggleButton, type TooltipProps,
Typography Typography,
styled,
tooltipClasses
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { useRowSelect } from '@table-library/react-table-library/select'; import { useRowSelect } from '@table-library/react-table-library/select';
import { SortToggleType, useSort } from '@table-library/react-table-library/sort'; import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
@@ -57,12 +57,7 @@ import { useTheme } from '@table-library/react-table-library/theme';
import type { Action, State } from '@table-library/react-table-library/types/common'; import type { Action, State } from '@table-library/react-table-library/types/common';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import { import { MessageBox, SectionContent, useLayoutTitle } from 'components';
ButtonTooltip,
MessageBox,
SectionContent,
useLayoutTitle
} 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 { useInterval } from 'utils'; import { useInterval } from 'utils';
@@ -85,7 +80,6 @@ const Devices = () => {
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false); const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
const [showDeviceInfo, setShowDeviceInfo] = useState(false); const [showDeviceInfo, setShowDeviceInfo] = useState(false);
const [selectedDevice, setSelectedDevice] = useState<number>(); const [selectedDevice, setSelectedDevice] = useState<number>();
const [search, setSearch] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
@@ -161,6 +155,7 @@ const Devices = () => {
} }
&.tr.tr-body.row-select.row-select-single-selected { &.tr.tr-body.row-select.row-select-single-selected {
background-color: #177ac9; background-color: #177ac9;
font-weight: normal;
} }
` `
}); });
@@ -174,9 +169,9 @@ const Devices = () => {
HeaderRow: ` HeaderRow: `
.th { .th {
padding: 8px; padding: 8px;
height: 36px;
`, `,
Row: ` Row: `
font-weight: bold;
&:hover .td { &:hover .td {
background-color: #177ac9; background-color: #177ac9;
` `
@@ -227,6 +222,20 @@ const Devices = () => {
} }
]); ]);
const ButtonTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} arrow classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.arrow}`]: {
color: theme.palette.success.main
},
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.success.main,
color: 'rgba(0, 0, 0, 0.87)',
boxShadow: theme.shadows[1],
fontSize: 10
}
}));
const getSortIcon = (state: State, sortKey: unknown) => { const getSortIcon = (state: State, sortKey: unknown) => {
if (state.sortKey === sortKey && state.reverse) { if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />; return <KeyboardArrowDownOutlinedIcon />;
@@ -276,7 +285,6 @@ const Devices = () => {
const resetDeviceSelect = () => { const resetDeviceSelect = () => {
device_select.fns.onRemoveAll(); device_select.fns.onRemoveAll();
setSearch('');
}; };
const escFunction = useCallback( const escFunction = useCallback(
@@ -299,9 +307,9 @@ const Devices = () => {
const customize = () => { const customize = () => {
if (selectedDevice === 99) { if (selectedDevice === 99) {
void navigate('/customentities'); navigate('/customentities');
} else { } else {
void navigate('/customizations', { state: selectedDevice }); navigate('/customizations', { state: selectedDevice });
} }
}; };
@@ -329,16 +337,13 @@ const Devices = () => {
const handleDownloadCsv = () => { const handleDownloadCsv = () => {
const deviceIndex = coreData.devices.findIndex( const deviceIndex = coreData.devices.findIndex(
(d: Device) => d.id === device_select.state.id (d) => d.id === device_select.state.id
); );
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return;
} }
const selectedDevice = coreData.devices[deviceIndex]; const filename =
if (!selectedDevice) { coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n;
return;
}
const filename = selectedDevice.tn + '_' + selectedDevice.n;
const columns = [ const columns = [
{ {
@@ -353,7 +358,7 @@ const Devices = () => {
{ {
accessor: (dv: DeviceValue) => accessor: (dv: DeviceValue) =>
dv.u !== undefined && DeviceValueUOM_s[dv.u] dv.u !== undefined && DeviceValueUOM_s[dv.u]
? DeviceValueUOM_s[dv.u]?.replace(/[^a-zA-Z0-9]/g, '') ? DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, '')
: '', : '',
name: 'UoM' name: 'UoM'
}, },
@@ -376,9 +381,7 @@ const Devices = () => {
]; ];
const data = onlyFav const data = onlyFav
? deviceData.nodes.filter((dv: DeviceValue) => ? deviceData.nodes.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)
)
: deviceData.nodes; : deviceData.nodes;
const csvData = data.reduce( const csvData = data.reduce(
@@ -417,7 +420,7 @@ const Devices = () => {
if (!deviceValueDialogOpen) { if (!deviceValueDialogOpen) {
selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData(); selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData();
} }
}); }, 3000);
const deviceValueDialogSave = async (devicevalue: DeviceValue) => { const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
const id = Number(device_select.state.id); const id = Number(device_select.state.id);
@@ -438,14 +441,10 @@ const Devices = () => {
const renderDeviceDetails = () => { const renderDeviceDetails = () => {
if (showDeviceInfo) { if (showDeviceInfo) {
const deviceIndex = coreData.devices.findIndex( const deviceIndex = coreData.devices.findIndex(
(d: Device) => d.id === device_select.state.id (d) => d.id === device_select.state.id
); );
if (deviceIndex === -1) { if (deviceIndex === -1) {
return null; return;
}
const deviceDetails = coreData.devices[deviceIndex];
if (!deviceDetails) {
return null;
} }
return ( return (
@@ -458,35 +457,47 @@ const Devices = () => {
<DialogContent dividers> <DialogContent dividers>
<List dense={true}> <List dense={true}>
<ListItem> <ListItem>
<ListItemText primary={LL.TYPE(0)} secondary={deviceDetails.tn} /> <ListItemText
primary={LL.TYPE(0)}
secondary={coreData.devices[deviceIndex].tn}
/>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText primary={LL.NAME(0)} secondary={deviceDetails.n} /> <ListItemText
primary={LL.NAME(0)}
secondary={coreData.devices[deviceIndex].n}
/>
</ListItem> </ListItem>
{deviceDetails.t !== DeviceType.CUSTOM && ( {coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && (
<> <>
<ListItem> <ListItem>
<ListItemText primary={LL.BRAND()} secondary={deviceDetails.b} /> <ListItemText
primary={LL.BRAND()}
secondary={coreData.devices[deviceIndex].b}
/>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={LL.ID_OF(LL.DEVICE())} primary={LL.ID_OF(LL.DEVICE())}
secondary={ secondary={
'0x' + '0x' +
('00' + deviceDetails.d.toString(16).toUpperCase()).slice(-2) (
'00' +
coreData.devices[deviceIndex].d.toString(16).toUpperCase()
).slice(-2)
} }
/> />
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={LL.ID_OF(LL.PRODUCT())} primary={LL.ID_OF(LL.PRODUCT())}
secondary={deviceDetails.p} secondary={coreData.devices[deviceIndex].p}
/> />
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={LL.VERSION()} primary={LL.VERSION()}
secondary={deviceDetails.v} secondary={coreData.devices[deviceIndex].v}
/> />
</ListItem> </ListItem>
</> </>
@@ -505,7 +516,6 @@ const Devices = () => {
</Dialog> </Dialog>
); );
} }
return null;
}; };
const renderCoreData = () => ( const renderCoreData = () => (
@@ -564,9 +574,7 @@ const Devices = () => {
const deviceValueDialogClose = () => { const deviceValueDialogClose = () => {
setDeviceValueDialogOpen(false); setDeviceValueDialogOpen(false);
if (selectedDevice !== undefined) {
void sendDeviceData(selectedDevice); void sendDeviceData(selectedDevice);
}
}; };
const renderDeviceData = () => { const renderDeviceData = () => {
@@ -595,27 +603,16 @@ const Devices = () => {
); );
const shown_data = onlyFav const shown_data = onlyFav
? deviceData.nodes.filter( ? deviceData.nodes.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
(dv: DeviceValue) => : deviceData.nodes;
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
)
: deviceData.nodes.filter((dv: DeviceValue) =>
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
);
const deviceIndex = coreData.devices.findIndex( const deviceIndex = coreData.devices.findIndex(
(d: Device) => d.id === device_select.state.id (d) => d.id === device_select.state.id
); );
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return;
} }
const deviceInfo = coreData.devices[deviceIndex];
if (!deviceInfo) {
return;
}
const [, height] = size;
return ( return (
<Box <Box
sx={{ sx={{
@@ -626,44 +623,26 @@ const Devices = () => {
bottom: 0, bottom: 0,
top: 64, top: 64,
zIndex: 'modal', zIndex: 'modal',
maxHeight: () => (height || 0) - 126, maxHeight: () => size[1] - 126,
border: '1px solid #177ac9' border: '1px solid #177ac9'
}} }}
> >
<Box sx={{ p: 1 }}> <Box sx={{ border: '1px solid #177ac9' }}>
<Grid container justifyContent="space-between"> <Typography noWrap variant="subtitle1" color="warning.main" sx={{ ml: 1 }}>
<Typography noWrap variant="subtitle1" color="warning.main"> {coreData.devices[deviceIndex].n}&nbsp;(
{deviceInfo.n}&nbsp;( {coreData.devices[deviceIndex].tn})
{deviceInfo.tn})
</Typography> </Typography>
<Grid justifyContent="flex-end">
<ButtonTooltip title={LL.CLOSE()}>
<IconButton onClick={resetDeviceSelect}>
<HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
</ButtonTooltip>
</Grid>
</Grid>
<TextField <Grid container justifyContent="space-between">
size="small" <Typography sx={{ ml: 1 }} variant="subtitle2" color="grey">
variant="outlined" {LL.SHOWING() +
sx={{ width: '22ch' }} ' ' +
placeholder={LL.SEARCH()} shown_data.length +
onChange={(event) => { '/' +
setSearch(event.target.value); coreData.devices[deviceIndex].e +
}} ' ' +
slotProps={{ LL.ENTITIES(shown_data.length)}
input: { <ButtonTooltip title="Info">
startAdornment: (
<InputAdornment position="start">
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
</InputAdornment>
)
}
}}
/>
<ButtonTooltip title={LL.DEVICE_DETAILS()}>
<IconButton onClick={() => setShowDeviceInfo(true)}> <IconButton onClick={() => setShowDeviceInfo(true)}>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} /> <InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton> </IconButton>
@@ -671,7 +650,7 @@ const Devices = () => {
{me.admin && ( {me.admin && (
<ButtonTooltip title={LL.CUSTOMIZATIONS()}> <ButtonTooltip title={LL.CUSTOMIZATIONS()}>
<IconButton onClick={customize}> <IconButton onClick={customize}>
<ConstructionIcon color="primary" sx={{ fontSize: 18 }} /> <FormatListNumberedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton> </IconButton>
</ButtonTooltip> </ButtonTooltip>
)} )}
@@ -680,34 +659,24 @@ const Devices = () => {
<DownloadIcon color="primary" sx={{ fontSize: 18 }} /> <DownloadIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton> </IconButton>
</ButtonTooltip> </ButtonTooltip>
<ButtonTooltip title={LL.FAVORITES()}> <ButtonTooltip title={LL.FAVORITES()}>
<ToggleButton <IconButton onClick={() => setOnlyFav(!onlyFav)}>
value="1"
size="small"
selected={onlyFav}
onChange={() => {
setOnlyFav(!onlyFav);
}}
>
{onlyFav ? ( {onlyFav ? (
<StarIcon color="primary" sx={{ fontSize: 18 }} /> <StarIcon color="primary" sx={{ fontSize: 18 }} />
) : ( ) : (
<StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18 }} /> <StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
)}{' '} )}
</ToggleButton> </IconButton>
</ButtonTooltip> </ButtonTooltip>
</Typography>
<span style={{ color: 'grey', fontSize: '12px' }}> <Grid justifyContent="flex-end">
&nbsp; <ButtonTooltip title={LL.CANCEL()}>
{LL.SHOWING() + <IconButton onClick={resetDeviceSelect}>
' ' + <HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />
shown_data.length + </IconButton>
'/' + </ButtonTooltip>
deviceInfo.e + </Grid>
' ' + </Grid>
LL.ENTITIES(shown_data.length)}
</span>
</Box> </Box>
<Table <Table

View File

@@ -11,12 +11,12 @@ import {
DialogContent, DialogContent,
DialogTitle, DialogTitle,
FormHelperText, FormHelperText,
Grid,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
TextField, TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator'; import type Schema from 'async-validator';
@@ -120,7 +120,7 @@ const DevicesDialog = ({
{editItem.l ? ( {editItem.l ? (
<TextField <TextField
name="v" name="v"
// label={LL.VALUE(0)} label={LL.VALUE(0)}
value={editItem.v} value={editItem.v}
disabled={!writeable} disabled={!writeable}
sx={{ width: '30ch' }} sx={{ width: '30ch' }}
@@ -135,7 +135,7 @@ const DevicesDialog = ({
</TextField> </TextField>
) : editItem.s || editItem.u !== DeviceValueUOM.NONE ? ( ) : editItem.s || editItem.u !== DeviceValueUOM.NONE ? (
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="v" name="v"
label={LL.VALUE(0)} label={LL.VALUE(0)}
value={numberValue(Math.round((editItem.v as number) * 10) / 10)} value={numberValue(Math.round((editItem.v as number) * 10) / 10)}
@@ -159,7 +159,7 @@ const DevicesDialog = ({
/> />
) : ( ) : (
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="v" name="v"
label={LL.VALUE(0)} label={LL.VALUE(0)}
value={editItem.v} value={editItem.v}

View File

@@ -43,7 +43,7 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
size="small" size="small"
color="secondary" color="secondary"
value={getMaskString(de.m)} value={getMaskString(de.m)}
onChange={(_event, mask: string[]) => { onChange={(event, mask: string[]) => {
de.m = getMaskNumber(mask); de.m = getMaskNumber(mask);
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;

View File

@@ -39,10 +39,9 @@ const Help = () => {
const [customSupportHTML, setCustomSupportHTML] = useState<string | null>(null); const [customSupportHTML, setCustomSupportHTML] = useState<string | null>(null);
const [notFound, setNotFound] = useState<boolean>(false); const [notFound, setNotFound] = useState<boolean>(false);
useRequest(() => callAction({ action: 'getCustomSupport' })).onSuccess((event) => { useRequest(() => callAction({ action: 'customSupport' })).onSuccess((event) => {
if (event && event.data && Object.keys(event.data).length !== 0) { if (event && event.data && Object.keys(event.data).length !== 0) {
const data = (event.data as { Support: { img_url?: string; html?: string[] } }) const data = event.data.Support;
.Support;
if (data.img_url) { if (data.img_url) {
setCustomSupportIMG(data.img_url); setCustomSupportIMG(data.img_url);
} }
@@ -52,6 +51,20 @@ const Help = () => {
} }
}); });
// const { send: sendExportAllValues } = useRequest(
// () => callAction({ action: 'export', param: 'allvalues' }),
// {
// immediate: false
// }
// )
// .onSuccess((event) => {
// saveFile(event.data, 'allvalues', '.txt');
// toast.info(LL.DOWNLOAD_SUCCESSFUL());
// })
// .onError((error) => {
// toast.error(error.message);
// });
const { send: sendAPI } = useRequest((data: APIcall) => API(data), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false
}) })
@@ -60,12 +73,11 @@ const Help = () => {
toast.info(LL.DOWNLOAD_SUCCESSFUL()); toast.info(LL.DOWNLOAD_SUCCESSFUL());
}) })
.onError((error) => { .onError((error) => {
toast.error(String(error.error?.message || 'An error occurred')); toast.error(error.message);
}); });
return ( return (
<SectionContent> <SectionContent>
{customSupportHTML && (
<Stack <Stack
padding={1} padding={1}
mb={2} mb={2}
@@ -79,7 +91,11 @@ const Help = () => {
}} }}
> >
<Typography variant="subtitle1"> <Typography variant="subtitle1">
{customSupportHTML ? (
<div dangerouslySetInnerHTML={{ __html: customSupportHTML }} /> <div dangerouslySetInnerHTML={{ __html: customSupportHTML }} />
) : (
LL.HELP_INFORMATION_5()
)}
</Typography> </Typography>
<Box <Box
component="img" component="img"
@@ -91,22 +107,15 @@ const Help = () => {
src={ src={
notFound notFound
? '' ? ''
: customSupportIMG || : customSupportIMG || 'https://emsesp.org/_media/images/installer.jpeg'
'https://docs.emsesp.org/_media/images/installer.jpeg'
} }
/> />
</Stack> </Stack>
)}
{me.admin && ( {me.admin && (
<List sx={{ borderRadius: 3, border: '2px solid grey' }}> <List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<ListItem> <ListItem>
<ListItemButton <ListItemButton component="a" href="https://emsesp.org">
component="a"
target="_blank"
rel="noreferrer"
href="https://docs.emsesp.org"
>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={{ bgcolor: '#72caf9' }}> <Avatar sx={{ bgcolor: '#72caf9' }}>
<MenuBookIcon /> <MenuBookIcon />
@@ -117,12 +126,7 @@ const Help = () => {
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemButton <ListItemButton component="a" href="https://discord.gg/3J3GgnzpyT">
component="a"
target="_blank"
rel="noreferrer"
href="https://discord.gg/3J3GgnzpyT"
>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={{ bgcolor: '#72caf9' }}> <Avatar sx={{ bgcolor: '#72caf9' }}>
<CommentIcon /> <CommentIcon />
@@ -135,8 +139,6 @@ const Help = () => {
<ListItem> <ListItem>
<ListItemButton <ListItemButton
component="a" component="a"
target="_blank"
rel="noreferrer"
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
> >
<ListItemAvatar> <ListItemAvatar>
@@ -164,16 +166,21 @@ const Help = () => {
</Button> </Button>
</Box> </Box>
{/* <Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => sendExportAllValues()}
>
{LL.DOWNLOAD(1)}&nbsp;{LL.ALLVALUES()}
</Button> */}
<Divider sx={{ mt: 4 }} /> <Divider sx={{ mt: 4 }} />
<Typography color="white" variant="subtitle1" align="center" mt={1}> <Typography color="white" variant="subtitle1" align="center" mt={1}>
&copy;&nbsp; &copy;&nbsp;
<Link <Link target="_blank" href="https://emsesp.org" color="primary">
target="_blank"
rel="noreferrer"
href="https://emsesp.org"
color="primary"
>
{'emsesp.org'} {'emsesp.org'}
</Link> </Link>
</Typography> </Typography>

View File

@@ -1,5 +1,5 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useBlocker } from 'react-router'; import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -133,15 +133,13 @@ const Modules = () => {
}; };
const saveModules = async () => { const saveModules = async () => {
await Promise.all( await updateModules({
modules.map((condensed_mi: ModuleItem) => modules: modules.map((condensed_mi) => ({
updateModules({
key: condensed_mi.key, key: condensed_mi.key,
enabled: condensed_mi.enabled, enabled: condensed_mi.enabled,
license: condensed_mi.license license: condensed_mi.license
}))
}) })
)
)
.then(() => { .then(() => {
toast.success(LL.MODULES_UPDATED()); toast.success(LL.MODULES_UPDATED());
}) })
@@ -156,9 +154,7 @@ const Modules = () => {
const renderContent = () => { const renderContent = () => {
if (!modules) { if (!modules) {
return ( return <FormLoader onRetry={fetchModules} errorMessage={error?.message} />;
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
);
} }
if (modules.length === 0) { if (modules.length === 0) {

View File

@@ -10,9 +10,9 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
TextField TextField
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { BlockFormControlLabel } from 'components'; import { BlockFormControlLabel } from 'components';

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useBlocker } from 'react-router'; import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
@@ -27,7 +27,6 @@ import {
useLayoutTitle useLayoutTitle
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { useInterval } from 'utils';
import { readSchedule, writeSchedule } from '../../api/app'; import { readSchedule, writeSchedule } from '../../api/app';
import SettingsSchedulerDialog from './SchedulerDialog'; import SettingsSchedulerDialog from './SchedulerDialog';
@@ -74,12 +73,6 @@ const Scheduler = () => {
); );
} }
useInterval(() => {
if (numChanges === 0) {
void fetchSchedule();
}
});
useEffect(() => { useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, { const formatter = new Intl.DateTimeFormat(locale, {
weekday: 'short', weekday: 'short',
@@ -135,8 +128,8 @@ const Scheduler = () => {
const saveSchedule = async () => { const saveSchedule = async () => {
await updateSchedule({ await updateSchedule({
schedule: schedule schedule: schedule
.filter((si: ScheduleItem) => !si.deleted) .filter((si) => !si.deleted)
.map((condensed_si: ScheduleItem) => ({ .map((condensed_si) => ({
id: condensed_si.id, id: condensed_si.id,
active: condensed_si.active, active: condensed_si.active,
flags: condensed_si.flags, flags: condensed_si.flags,
@@ -212,9 +205,7 @@ const Scheduler = () => {
const renderSchedule = () => { const renderSchedule = () => {
if (!schedule) { if (!schedule) {
return ( return <FormLoader onRetry={fetchSchedule} errorMessage={error?.message} />;
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
);
} }
const dayBox = (si: ScheduleItem, flag: number) => ( const dayBox = (si: ScheduleItem, flag: number) => (
@@ -253,8 +244,8 @@ const Scheduler = () => {
<Table <Table
data={{ data={{
nodes: schedule nodes: schedule
.filter((si: ScheduleItem) => !si.deleted) .filter((si) => !si.deleted)
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags) .sort((a, b) => a.flags - b.flags)
}} }}
theme={schedule_theme} theme={schedule_theme}
layout={{ custom: true }} layout={{ custom: true }}

View File

@@ -13,12 +13,12 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
TextField, TextField,
ToggleButton, ToggleButton,
ToggleButtonGroup, ToggleButtonGroup,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator'; import type Schema from 'async-validator';
@@ -144,10 +144,7 @@ const SchedulerDialog = ({
</Typography> </Typography>
); );
const handleClose = ( const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -328,7 +325,7 @@ const SchedulerDialog = ({
</> </>
)} )}
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="cmd" name="cmd"
label={LL.COMMAND(0)} label={LL.COMMAND(0)}
multiline multiline
@@ -347,7 +344,7 @@ const SchedulerDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="name" name="name"
label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'} label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'}
value={editItem.name} value={editItem.name}

View File

@@ -90,7 +90,7 @@ const Sensors = () => {
if (!temperatureDialogOpen && !analogDialogOpen) { if (!temperatureDialogOpen && !analogDialogOpen) {
void fetchSensorData(); void fetchSensorData();
} }
}); }, 3000);
const common_theme = useTheme({ const common_theme = useTheme({
BaseRow: ` BaseRow: `
@@ -438,9 +438,7 @@ const Sensors = () => {
<Cell stiff>{a.g}</Cell> <Cell stiff>{a.g}</Cell>
<Cell>{a.n}</Cell> <Cell>{a.n}</Cell>
<Cell stiff>{AnalogTypeNames[a.t]} </Cell> <Cell stiff>{AnalogTypeNames[a.t]} </Cell>
{(a.t === AnalogType.DIGITAL_OUT && a.g !== 25 && a.g !== 26) || {a.t === AnalogType.DIGITAL_OUT || a.t === AnalogType.DIGITAL_IN ? (
a.t === AnalogType.DIGITAL_IN ||
a.t === AnalogType.PULSE ? (
<Cell stiff>{a.v ? LL.ON() : LL.OFF()}</Cell> <Cell stiff>{a.v ? LL.ON() : LL.OFF()}</Cell>
) : ( ) : (
<Cell stiff>{a.t ? formatValue(a.v, a.u) : ''}</Cell> <Cell stiff>{a.t ? formatValue(a.v, a.u) : ''}</Cell>

View File

@@ -10,12 +10,12 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
TextField, TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator'; import type Schema from 'async-validator';
@@ -57,10 +57,7 @@ const SensorsAnalogDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = ( const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -91,7 +88,7 @@ const SensorsAnalogDialog = ({
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="g" name="g"
label="GPIO" label="GPIO"
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
@@ -110,7 +107,7 @@ const SensorsAnalogDialog = ({
)} )}
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="n" name="n"
label={LL.NAME(0)} label={LL.NAME(0)}
value={editItem.n} value={editItem.n}
@@ -135,9 +132,7 @@ const SensorsAnalogDialog = ({
))} ))}
</TextField> </TextField>
</Grid> </Grid>
{((editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE) || {editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
(editItem.t >= AnalogType.FREQ_0 &&
editItem.t <= AnalogType.FREQ_2)) && (
<Grid> <Grid>
<TextField <TextField
name="u" name="u"
@@ -176,27 +171,6 @@ const SensorsAnalogDialog = ({
/> />
</Grid> </Grid>
)} )}
{editItem.t === AnalogType.NTC && (
<Grid>
<TextField
name="o"
label={LL.OFFSET()}
value={numberValue(editItem.o)}
sx={{ width: '11ch' }}
type="number"
variant="outlined"
onChange={updateFormValue}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">°C</InputAdornment>
)
},
htmlInput: { min: '-20', max: '20', step: '0.1' }
}}
/>
</Grid>
)}
{editItem.t === AnalogType.COUNTER && ( {editItem.t === AnalogType.COUNTER && (
<Grid> <Grid>
<TextField <TextField
@@ -213,19 +187,6 @@ const SensorsAnalogDialog = ({
/> />
</Grid> </Grid>
)} )}
{editItem.t === AnalogType.RGB && (
<Grid>
<TextField
name="o"
label={'RGB ' + LL.VALUE(0)}
value={numberValue(editItem.o)}
type="number"
sx={{ width: '11ch' }}
variant="outlined"
onChange={updateFormValue}
/>
</Grid>
)}
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && ( {editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
<Grid> <Grid>
<TextField <TextField
@@ -353,42 +314,6 @@ const SensorsAnalogDialog = ({
</Grid> </Grid>
</> </>
)} )}
{editItem.t === AnalogType.PULSE && (
<>
<Grid>
<TextField
name="o"
label={LL.POLARITY()}
value={editItem.o}
sx={{ width: '11ch' }}
select
onChange={updateFormValue}
>
<MenuItem value={0}>{LL.ACTIVEHIGH()}</MenuItem>
<MenuItem value={1}>{LL.ACTIVELOW()}</MenuItem>
</TextField>
</Grid>
<Grid>
<TextField
name="f"
label="Pulse"
value={numberValue(editItem.f)}
type="number"
sx={{ width: '15ch' }}
variant="outlined"
onChange={updateFormValue}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">s</InputAdornment>
)
},
htmlInput: { min: '0', max: '10000', step: '0.1' }
}}
/>
</Grid>
</>
)}
</Grid> </Grid>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>

View File

@@ -9,11 +9,11 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
InputAdornment, InputAdornment,
TextField, TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator'; import type Schema from 'async-validator';
@@ -52,10 +52,7 @@ const SensorsTemperatureDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = ( const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -85,7 +82,7 @@ const SensorsTemperatureDialog = ({
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="n" name="n"
label={LL.NAME(0)} label={LL.NAME(0)}
value={editItem.n} value={editItem.n}

View File

@@ -34,12 +34,7 @@ export function formatValue(
if (value === undefined || typeof value === 'boolean') { if (value === undefined || typeof value === 'boolean') {
return ''; return '';
} }
return ( return value as string;
(value as string) +
(value === '' || uom === undefined || uom === 0
? ''
: ' ' + DeviceValueUOM_s[uom])
);
} }
switch (uom) { switch (uom) {

View File

@@ -21,7 +21,6 @@ export interface Settings {
dallas_gpio: number; dallas_gpio: number;
dallas_parasite: boolean; dallas_parasite: boolean;
led_gpio: number; led_gpio: number;
led_type: number;
hide_led: boolean; hide_led: boolean;
low_clock: boolean; low_clock: boolean;
notoken_api: boolean; notoken_api: boolean;
@@ -72,7 +71,7 @@ export interface Device {
d: number; // deviceid d: number; // deviceid
p: number; // productid p: number; // productid
v: string; // version v: string; // version
e: number; // total number of entities e: number; // entities
url?: string; // lowercase type name used in API URL url?: string; // lowercase type name used in API URL
} }
@@ -124,11 +123,6 @@ export interface DashboardItem {
nodes?: DashboardItem[]; // children nodes, optional nodes?: DashboardItem[]; // children nodes, optional
} }
export interface DashboardData {
connected: boolean; // true if connected to EMS bus
nodes: DashboardItem[];
}
export interface DeviceValue { export interface DeviceValue {
id: string; // index, contains mask+name id: string; // index, contains mask+name
v?: unknown; // value, Number, String or Boolean - can be undefined v?: unknown; // value, Number, String or Boolean - can be undefined
@@ -187,9 +181,7 @@ export enum DeviceValueUOM {
K, K,
VOLTS, VOLTS,
MBAR, MBAR,
LH, LH
CTKWH,
HZ
} }
export const DeviceValueUOM_s = [ export const DeviceValueUOM_s = [
@@ -218,9 +210,7 @@ export const DeviceValueUOM_s = [
'K', 'K',
'V', 'V',
'mbar', 'mbar',
'l/h', 'l/h'
'ct/kWh',
'Hz'
]; ];
export enum AnalogType { export enum AnalogType {
@@ -234,32 +224,20 @@ export enum AnalogType {
DIGITAL_OUT = 6, DIGITAL_OUT = 6,
PWM_0 = 7, PWM_0 = 7,
PWM_1 = 8, PWM_1 = 8,
PWM_2 = 9, PWM_2 = 9
NTC = 10,
RGB = 11,
PULSE = 12,
FREQ_0 = 13,
FREQ_1 = 14,
FREQ_2 = 15
} }
export const AnalogTypeNames = [ export const AnalogTypeNames = [
'(disabled)', '(disabled)',
'Digital In', 'Digital In',
'Counter', 'Counter',
'ADC In', 'ADC',
'Timer', 'Timer',
'Rate', 'Rate',
'Digital Out', 'Digital Out',
'PWM 0', 'PWM 0',
'PWM 1', 'PWM 1',
'PWM 2', 'PWM 2'
'NTC Temp.',
'RGB Led',
'Pulse',
'Freq 0',
'Freq 1',
'Freq 2'
]; ];
type BoardProfiles = Record<string, string>; type BoardProfiles = Record<string, string>;
@@ -269,7 +247,6 @@ export const BOARD_PROFILES: BoardProfiles = {
S32S3: 'BBQKees Gateway S3', S32S3: 'BBQKees Gateway S3',
E32: 'BBQKees Gateway E32', E32: 'BBQKees Gateway E32',
E32V2: 'BBQKees Gateway E32 V2', E32V2: 'BBQKees Gateway E32 V2',
E32V2_2: 'BBQKees Gateway E32 V2.2',
NODEMCU: 'NodeMCU 32S', NODEMCU: 'NodeMCU 32S',
'MH-ET': 'MH-ET Live D1 Mini', 'MH-ET': 'MH-ET Live D1 Mini',
LOLIN: 'Lolin D32', LOLIN: 'Lolin D32',
@@ -283,7 +260,6 @@ export const BOARD_PROFILES: BoardProfiles = {
export interface BoardProfile { export interface BoardProfile {
board_profile: string; board_profile: string;
led_gpio: number; led_gpio: number;
led_type: number;
dallas_gpio: number; dallas_gpio: number;
rx_gpio: number; rx_gpio: number;
tx_gpio: number; tx_gpio: number;
@@ -390,12 +366,11 @@ export interface EntityItem {
device_id: number | string; device_id: number | string;
type_id: number | string; type_id: number | string;
offset: number; offset: number;
factor: number | string; factor: number;
uom: number; uom: number;
value_type: number; value_type: number;
value?: unknown; value?: unknown;
writeable: boolean; writeable: boolean;
hide: boolean;
deleted?: boolean; deleted?: boolean;
o_id?: number; o_id?: number;
o_ram?: number; o_ram?: number;
@@ -403,13 +378,12 @@ export interface EntityItem {
o_device_id?: number | string; o_device_id?: number | string;
o_type_id?: number | string; o_type_id?: number | string;
o_offset?: number; o_offset?: number;
o_factor?: number | string; o_factor?: number;
o_uom?: number; o_uom?: number;
o_value_type?: number; o_value_type?: number;
o_deleted?: boolean; o_deleted?: boolean;
o_writeable?: boolean; o_writeable?: boolean;
o_value?: unknown; o_value?: unknown;
o_hide?: boolean;
} }
export interface Entities { export interface Entities {

View File

@@ -13,7 +13,7 @@ import type {
export const GPIO_VALIDATOR = { export const GPIO_VALIDATOR = {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: number, value: number,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -36,7 +36,7 @@ export const GPIO_VALIDATOR = {
export const GPIO_VALIDATORR = { export const GPIO_VALIDATORR = {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: number, value: number,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -60,7 +60,7 @@ export const GPIO_VALIDATORR = {
export const GPIO_VALIDATORC3 = { export const GPIO_VALIDATORC3 = {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: number, value: number,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -74,7 +74,7 @@ export const GPIO_VALIDATORC3 = {
export const GPIO_VALIDATORS2 = { export const GPIO_VALIDATORS2 = {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: number, value: number,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -94,7 +94,7 @@ export const GPIO_VALIDATORS2 = {
export const GPIO_VALIDATORS3 = { export const GPIO_VALIDATORS3 = {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: number, value: number,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -279,7 +279,7 @@ export const createSettingsValidator = (settings: Settings) =>
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({ export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
name: string, name: string,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -324,7 +324,7 @@ export const uniqueCustomNameValidator = (
o_name?: string o_name?: string
) => ({ ) => ({
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
name: string, name: string,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -353,7 +353,7 @@ export const entityItemValidation = (entity: EntityItem[], entityItem: EntityIte
device_id: [ device_id: [
{ {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: string, value: string,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -367,7 +367,7 @@ export const entityItemValidation = (entity: EntityItem[], entityItem: EntityIte
type_id: [ type_id: [
{ {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: string, value: string,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -382,14 +382,17 @@ export const entityItemValidation = (entity: EntityItem[], entityItem: EntityIte
{ required: true, message: 'Offset is required' }, { required: true, message: 'Offset is required' },
{ type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' } { type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' }
], ],
factor: [{ required: true, message: 'is required' }] factor: [
{ required: true, message: 'Bytes is required' },
{ type: 'number', min: 1, max: 255, message: 'Must be between 1 and 255' }
]
}); });
export const uniqueTemperatureNameValidator = ( export const uniqueTemperatureNameValidator = (
sensors: TemperatureSensor[], sensors: TemperatureSensor[],
o_name?: string o_name?: string
) => ({ ) => ({
validator(_rule: InternalRuleItem, n: string, callback: (error?: string) => void) { validator(rule: InternalRuleItem, n: string, callback: (error?: string) => void) {
if ( if (
(o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) && (o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) &&
n !== '' && n !== '' &&
@@ -419,7 +422,7 @@ export const temperatureSensorItemValidation = (
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({ export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
gpio: number, gpio: number,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {
@@ -435,7 +438,7 @@ export const uniqueAnalogNameValidator = (
sensors: AnalogSensor[], sensors: AnalogSensor[],
o_name?: string o_name?: string
) => ({ ) => ({
validator(_rule: InternalRuleItem, n: string, callback: (error?: string) => void) { validator(rule: InternalRuleItem, n: string, callback: (error?: string) => void) {
if ( if (
(o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) && (o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) &&
n !== '' && n !== '' &&
@@ -482,7 +485,7 @@ export const deviceValueItemValidation = (dv: DeviceValue) =>
{ required: true, message: 'Value is required' }, { required: true, message: 'Value is required' },
{ {
validator( validator(
_rule: InternalRuleItem, rule: InternalRuleItem,
value: unknown, value: unknown,
callback: (error?: string) => void callback: (error?: string) => void
) { ) {

View File

@@ -46,7 +46,7 @@ const APSettings = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.ACCESS_POINT(0)); useLayoutTitle(LL.SETTINGS_OF(LL.ACCESS_POINT(0)));
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -54,12 +54,12 @@ const APSettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue as (value: unknown) => void updateDataValue
); );
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -80,7 +80,7 @@ const APSettings = () => {
return ( return (
<> <>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="provision_mode" name="provision_mode"
label={LL.AP_PROVIDE() + '...'} label={LL.AP_PROVIDE() + '...'}
value={data.provision_mode} value={data.provision_mode}
@@ -103,7 +103,7 @@ const APSettings = () => {
{isAPEnabled(data) && ( {isAPEnabled(data) && (
<> <>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="ssid" name="ssid"
label={LL.ACCESS_POINT(2) + ' SSID'} label={LL.ACCESS_POINT(2) + ' SSID'}
fullWidth fullWidth
@@ -113,7 +113,7 @@ const APSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedPasswordField <ValidatedPasswordField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="password" name="password"
label={LL.ACCESS_POINT(2) + ' ' + LL.PASSWORD()} label={LL.ACCESS_POINT(2) + ' ' + LL.PASSWORD()}
fullWidth fullWidth
@@ -123,7 +123,7 @@ const APSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="channel" name="channel"
label={LL.AP_PREFERRED_CHANNEL()} label={LL.AP_PREFERRED_CHANNEL()}
value={numberValue(data.channel)} value={numberValue(data.channel)}
@@ -151,7 +151,7 @@ const APSettings = () => {
label={LL.AP_HIDE_SSID()} label={LL.AP_HIDE_SSID()}
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="max_clients" name="max_clients"
label={LL.AP_MAX_CLIENTS()} label={LL.AP_MAX_CLIENTS()}
value={numberValue(data.max_clients)} value={numberValue(data.max_clients)}
@@ -169,7 +169,7 @@ const APSettings = () => {
))} ))}
</ValidatedTextField> </ValidatedTextField>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="local_ip" name="local_ip"
label={LL.AP_LOCAL_IP()} label={LL.AP_LOCAL_IP()}
fullWidth fullWidth
@@ -179,7 +179,7 @@ const APSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="gateway_ip" name="gateway_ip"
label={LL.NETWORK_GATEWAY()} label={LL.NETWORK_GATEWAY()}
fullWidth fullWidth
@@ -189,7 +189,7 @@ const APSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="subnet_mask" name="subnet_mask"
label={LL.NETWORK_SUBNET()} label={LL.NETWORK_SUBNET()}
fullWidth fullWidth

View File

@@ -9,17 +9,17 @@ import {
Button, Button,
Checkbox, Checkbox,
Divider, Divider,
Grid,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
TextField, TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { readSystemStatus } from 'api/system'; import { readSystemStatus } from 'api/system';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import SystemMonitor from 'app/status/SystemMonitor'; import RestartMonitor from 'app/status/RestartMonitor';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
@@ -75,7 +75,7 @@ const ApplicationSettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue as (value: unknown) => void updateDataValue
); );
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -121,11 +121,14 @@ const ApplicationSettings = () => {
}); });
}; };
useLayoutTitle(LL.APPLICATION()); useLayoutTitle(LL.SETTINGS_OF(LL.APPLICATION()));
const SecondsInputProps = { const SecondsInputProps = {
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}; };
const MilliSecondsInputProps = {
endAdornment: <InputAdornment position="end">ms</InputAdornment>
};
const MinutesInputProps = { const MinutesInputProps = {
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment> endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
}; };
@@ -135,7 +138,7 @@ const ApplicationSettings = () => {
const content = () => { const content = () => {
if (!data || !hardwareData) { if (!data || !hardwareData) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -204,22 +207,13 @@ const ApplicationSettings = () => {
disabled={!hardwareData.psram} disabled={!hardwareData.psram}
/> />
} }
label={ label={LL.ENABLE_MODBUS()}
<Typography color={!hardwareData.psram ? 'grey' : 'default'}>
{LL.ENABLE_MODBUS()}
{!hardwareData.psram && (
<Typography variant="caption">
&nbsp; &#40;{LL.IS_REQUIRED('PSRAM')}&#41;
</Typography>
)}
</Typography>
}
/> />
{data.modbus_enabled && ( {data.modbus_enabled && (
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="modbus_max_clients" name="modbus_max_clients"
label={LL.AP_MAX_CLIENTS()} label={LL.AP_MAX_CLIENTS()}
variant="outlined" variant="outlined"
@@ -231,7 +225,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="modbus_port" name="modbus_port"
label="Port" label="Port"
variant="outlined" variant="outlined"
@@ -243,11 +237,11 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="modbus_timeout" name="modbus_timeout"
label="Timeout" label="Timeout"
slotProps={{ slotProps={{
input: SecondsInputProps input: MilliSecondsInputProps
}} }}
variant="outlined" variant="outlined"
value={numberValue(data.modbus_timeout)} value={numberValue(data.modbus_timeout)}
@@ -273,7 +267,7 @@ const ApplicationSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="syslog_host" name="syslog_host"
label="Host" label="Host"
variant="outlined" variant="outlined"
@@ -284,7 +278,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="syslog_port" name="syslog_port"
label="Port" label="Port"
variant="outlined" variant="outlined"
@@ -315,7 +309,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="syslog_mark_interval" name="syslog_mark_interval"
label={LL.MARK_INTERVAL()} label={LL.MARK_INTERVAL()}
slotProps={{ slotProps={{
@@ -485,7 +479,7 @@ const ApplicationSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="rx_gpio" name="rx_gpio"
label={LL.GPIO_OF('Rx')} label={LL.GPIO_OF('Rx')}
fullWidth fullWidth
@@ -498,7 +492,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="tx_gpio" name="tx_gpio"
label={LL.GPIO_OF('Tx')} label={LL.GPIO_OF('Tx')}
fullWidth fullWidth
@@ -511,7 +505,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="pbutton_gpio" name="pbutton_gpio"
label={LL.GPIO_OF(LL.BUTTON())} label={LL.GPIO_OF(LL.BUTTON())}
fullWidth fullWidth
@@ -524,7 +518,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="dallas_gpio" name="dallas_gpio"
label={ label={
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')' LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
@@ -539,7 +533,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="led_gpio" name="led_gpio"
label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'} label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'}
fullWidth fullWidth
@@ -550,23 +544,6 @@ const ApplicationSettings = () => {
margin="normal" margin="normal"
/> />
</Grid> </Grid>
{data.led_gpio !== 0 && (
<Grid>
<TextField
name="led_type"
label={'LED ' + LL.TYPE(0)}
value={data.led_type}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>LED</MenuItem>
<MenuItem value={1}>RGB-LED</MenuItem>
</TextField>
</Grid>
)}
<Grid> <Grid>
<TextField <TextField
name="phy_type" name="phy_type"
@@ -743,7 +720,7 @@ const ApplicationSettings = () => {
{data.remote_timeout_en && ( {data.remote_timeout_en && (
<Box mt={2}> <Box mt={2}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="remote_timeout" name="remote_timeout"
label={LL.REMOTE_TIMEOUT()} label={LL.REMOTE_TIMEOUT()}
slotProps={{ slotProps={{
@@ -783,7 +760,7 @@ const ApplicationSettings = () => {
{data.shower_timer && ( {data.shower_timer && (
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="shower_min_duration" name="shower_min_duration"
label={LL.MIN_DURATION()} label={LL.MIN_DURATION()}
slotProps={{ slotProps={{
@@ -801,7 +778,7 @@ const ApplicationSettings = () => {
<> <>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="shower_alert_trigger" name="shower_alert_trigger"
label={LL.TRIGGER_TIME()} label={LL.TRIGGER_TIME()}
slotProps={{ slotProps={{
@@ -817,7 +794,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="shower_alert_coldshot" name="shower_alert_coldshot"
label={LL.COLD_SHOT_DURATION()} label={LL.COLD_SHOT_DURATION()}
slotProps={{ slotProps={{
@@ -876,7 +853,7 @@ const ApplicationSettings = () => {
return ( return (
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <SystemMonitor /> : content()} {restarting ? <RestartMonitor /> : content()}
</SectionContent> </SectionContent>
); );
}; };

View File

@@ -2,14 +2,15 @@ import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
import { Box, Button, Grid, Typography } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
import Grid from '@mui/material/Grid2';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { API, callAction } from 'api/app'; import { API, callAction } from 'api/app';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types'; import type { APIcall } from 'app/main/types';
import SystemMonitor from 'app/status/SystemMonitor'; import RestartMonitor from 'app/status/RestartMonitor';
import { import {
FormLoader, FormLoader,
SectionContent, SectionContent,
@@ -35,7 +36,7 @@ const DownloadUpload = () => {
toast.info(LL.DOWNLOAD_SUCCESSFUL()); toast.info(LL.DOWNLOAD_SUCCESSFUL());
}) })
.onError((error) => { .onError((error) => {
toast.error(String(error.error?.message || 'An error occurred')); toast.error(error.message);
}); });
const { send: sendAPI } = useRequest((data: APIcall) => API(data), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
@@ -57,7 +58,7 @@ const DownloadUpload = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (
@@ -108,15 +109,6 @@ const DownloadUpload = () => {
{LL.SCHEDULE(0)} {LL.SCHEDULE(0)}
</Button> </Button>
</Grid> </Grid>
<Button
sx={{ ml: 2, mt: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => sendExportData('allvalues')}
>
{LL.ALLVALUES()}
</Button>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()} {LL.UPLOAD()}
@@ -126,13 +118,13 @@ const DownloadUpload = () => {
<Typography variant="body1">{LL.UPLOAD_TEXT()}.</Typography> <Typography variant="body1">{LL.UPLOAD_TEXT()}.</Typography>
</Box> </Box>
<SingleUpload text={LL.UPLOAD_DRAG()} doRestart={doRestart} /> <SingleUpload doRestart={doRestart} />
</> </>
); );
}; };
return ( return (
<SectionContent>{restarting ? <SystemMonitor /> : content()}</SectionContent> <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
); );
}; };

View File

@@ -5,12 +5,12 @@ import WarningIcon from '@mui/icons-material/Warning';
import { import {
Button, Button,
Checkbox, Checkbox,
Grid,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
TextField, TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
@@ -48,7 +48,7 @@ const MqttSettings = () => {
}); });
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle('MQTT'); useLayoutTitle(LL.SETTINGS_OF('MQTT'));
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -56,7 +56,7 @@ const MqttSettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue as (value: unknown) => void updateDataValue
); );
const SecondsInputProps = { const SecondsInputProps = {
@@ -65,7 +65,7 @@ const MqttSettings = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -93,7 +93,7 @@ const MqttSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="host" name="host"
label={LL.ADDRESS_OF(LL.BROKER())} label={LL.ADDRESS_OF(LL.BROKER())}
multiline multiline
@@ -105,7 +105,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="port" name="port"
label="Port" label="Port"
variant="outlined" variant="outlined"
@@ -117,7 +117,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="base" name="base"
label={LL.BASE_TOPIC()} label={LL.BASE_TOPIC()}
variant="outlined" variant="outlined"
@@ -158,7 +158,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="keep_alive" name="keep_alive"
label="Keep Alive" label="Keep Alive"
slotProps={{ slotProps={{
@@ -254,6 +254,7 @@ const MqttSettings = () => {
} }
label={LL.MQTT_RESPONSE()} label={LL.MQTT_RESPONSE()}
/> />
{!data.ha_enabled && (
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<BlockFormControlLabel <BlockFormControlLabel
@@ -262,7 +263,6 @@ const MqttSettings = () => {
name="publish_single" name="publish_single"
checked={data.publish_single} checked={data.publish_single}
onChange={updateFormValue} onChange={updateFormValue}
disabled={data.ha_enabled}
/> />
} }
label={LL.MQTT_PUBLISH_TEXT_1()} label={LL.MQTT_PUBLISH_TEXT_1()}
@@ -283,6 +283,8 @@ const MqttSettings = () => {
</Grid> </Grid>
)} )}
</Grid> </Grid>
)}
{!data.publish_single && (
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<BlockFormControlLabel <BlockFormControlLabel
@@ -291,7 +293,6 @@ const MqttSettings = () => {
name="ha_enabled" name="ha_enabled"
checked={data.ha_enabled} checked={data.ha_enabled}
onChange={updateFormValue} onChange={updateFormValue}
disabled={data.publish_single}
/> />
} }
label={LL.MQTT_PUBLISH_TEXT_3()} label={LL.MQTT_PUBLISH_TEXT_3()}
@@ -348,13 +349,14 @@ const MqttSettings = () => {
</Grid> </Grid>
)} )}
</Grid> </Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto) {LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto)
</Typography> </Typography>
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="publish_time_heartbeat" name="publish_time_heartbeat"
label="Heartbeat" label="Heartbeat"
slotProps={{ slotProps={{
@@ -440,7 +442,7 @@ const MqttSettings = () => {
<Grid> <Grid>
<TextField <TextField
name="publish_time_sensor" name="publish_time_sensor"
label={LL.SENSORS()} label={LL.TEMP_SENSORS()}
variant="outlined" variant="outlined"
value={numberValue(data.publish_time_sensor)} value={numberValue(data.publish_time_sensor)}
type="number" type="number"

View File

@@ -1,27 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { import { Button, Checkbox, MenuItem } from '@mui/material';
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as NTPApi from 'api/ntp'; import * as NTPApi from 'api/ntp';
import { readNTPSettings } from 'api/ntp'; import { readNTPSettings } from 'api/ntp';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import { updateState } from 'alova/client'; import { updateState } from 'alova/client';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import { import {
@@ -34,8 +19,8 @@ import {
useLayoutTitle useLayoutTitle
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NTPSettingsType, Time } from 'types'; import type { NTPSettingsType } from 'types';
import { formatLocalDateTime, updateValueDirty, useRest } from 'utils'; 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';
@@ -59,103 +44,20 @@ const NTPSettings = () => {
}); });
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle('NTP'); useLayoutTitle(LL.SETTINGS_OF('NTP'));
const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false);
const { send: updateTime } = useRequest(
(local_time: Time) => NTPApi.updateTime(local_time),
{
immediate: false
}
);
const updateFormValue = updateValueDirty( const updateFormValue = updateValueDirty(
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue as (value: unknown) => void updateDataValue
); );
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
setLocalTime(event.target.value);
const openSetTime = () => {
setLocalTime(formatLocalDateTime(new Date()));
setSettingTime(true);
};
const configureTime = async () => {
setProcessing(true);
await updateTime({ local_time: formatLocalDateTime(new Date(localTime)) })
.then(async () => {
toast.success(LL.TIME_SET());
setSettingTime(false);
await loadData();
})
.catch(() => {
toast.error(LL.PROBLEM_UPDATING());
})
.finally(() => {
setProcessing(false);
});
};
const renderSetTimeDialog = () => (
<Dialog
sx={dialogStyle}
open={settingTime}
onClose={() => setSettingTime(false)}
>
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
</Box>
<TextField
label={LL.LOCAL_TIME(0)}
type="datetime-local"
value={localTime}
onChange={updateLocalTime}
disabled={processing}
fullWidth
slotProps={{
inputLabel: {
shrink: true
}
}}
/>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setSettingTime(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<AccessTimeIcon />}
variant="outlined"
onClick={configureTime}
disabled={processing}
color="primary"
>
{LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -169,12 +71,12 @@ const NTPSettings = () => {
}; };
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => { const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFormValue(event);
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({ void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
...settings, ...settings,
tz_label: event.target.value, tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value] tz_format: TIME_ZONES[event.target.value]
})); }));
updateFormValue(event);
}; };
return ( return (
@@ -190,7 +92,7 @@ const NTPSettings = () => {
label={LL.ENABLE_NTP()} label={LL.ENABLE_NTP()}
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="server" name="server"
label={LL.NTP_SERVER()} label={LL.NTP_SERVER()}
fullWidth fullWidth
@@ -200,7 +102,7 @@ const NTPSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="tz_label" name="tz_label"
label={LL.TIME_ZONE()} label={LL.TIME_ZONE()}
fullWidth fullWidth
@@ -213,25 +115,6 @@ const NTPSettings = () => {
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem> <MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
{timeZoneSelectItems()} {timeZoneSelectItems()}
</ValidatedTextField> </ValidatedTextField>
<Box display="flex" flexWrap="wrap">
{!data.enabled && !dirtyFlags.length && (
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
onClick={openSetTime}
variant="outlined"
color="primary"
startIcon={<AccessTimeIcon />}
>
{LL.SET_TIME(0)}
</Button>
</ButtonRow>
</Box>
)}
</Box>
{renderSetTimeDialog()}
{dirtyFlags && dirtyFlags.length !== 0 && ( {dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow> <ButtonRow>
<Button <Button

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ImportExportIcon from '@mui/icons-material/ImportExport'; import ImportExportIcon from '@mui/icons-material/ImportExport';
@@ -20,7 +21,7 @@ import {
List List
} from '@mui/material'; } from '@mui/material';
import { API } from 'api/app'; import { API, callAction } from 'api/app';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
@@ -39,6 +40,11 @@ const Settings = () => {
immediate: false immediate: false
}); });
// call checkUpgrade with no param to fetch EMS-ESP version
const { data } = useRequest(() => callAction({ action: 'checkUpgrade' }), {
initialData: { emsesp_version: '...' }
});
const doFormat = async () => { const doFormat = async () => {
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => { await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
setConfirmFactoryReset(false); setConfirmFactoryReset(false);
@@ -77,6 +83,14 @@ const Settings = () => {
const content = () => ( const content = () => (
<> <>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}> <List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"
label={LL.EMS_ESP_VER()}
text={data.emsesp_version}
to="version"
/>
<ListMenuItem <ListMenuItem
icon={TuneIcon} icon={TuneIcon}
bgcolor="#134ba2" bgcolor="#134ba2"
@@ -137,7 +151,7 @@ const Settings = () => {
bgcolor="#5d89f7" bgcolor="#5d89f7"
label={LL.DOWNLOAD_UPLOAD()} label={LL.DOWNLOAD_UPLOAD()}
text={LL.DOWNLOAD_UPLOAD_1()} text={LL.DOWNLOAD_UPLOAD_1()}
to="downloadUpload" to="upload"
/> />
</List> </List>

View File

@@ -0,0 +1,277 @@
import { useState } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Link,
Typography
} from '@mui/material';
import Grid from '@mui/material/Grid2';
import * as SystemApi from 'api/system';
import { callAction } from 'api/app';
import { getDevVersion, getStableVersion } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import RestartMonitor from 'app/status/RestartMonitor';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Version = () => {
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>(false);
const [openDialog, setOpenDialog] = useState<boolean>(false);
const [useDev, setUseDev] = useState<boolean>(false);
const [upgradeAvailable, setUpgradeAvailable] = useState<boolean>(false);
const { send: sendCheckUpgrade } = useRequest(
(version: string) => callAction({ action: 'checkUpgrade', param: version }),
{
immediate: false
}
).onSuccess((event) => {
const data = event.data as { emsesp_version: string; upgradeable: boolean };
setUpgradeAvailable(data.upgradeable);
});
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
const { send: sendUploadURL } = useRequest(
(url: string) => callAction({ action: 'uploadURL', param: url }),
{
immediate: false
}
);
// called immediately to get the latest version, on page load
const { data: latestVersion } = useRequest(getStableVersion, {
// uncomment next 2 lines for testing, uses https://github.com/emsesp/EMS-ESP32/releases/download/v3.6.5/EMS-ESP-3_6_5-ESP32-16MB+.bin
// immediate: false,
// initialData: '3.6.5'
});
// called immediately to get the latest version, on page load, then check for upgrade (works for both dev and stable)
const { data: latestDevVersion } = useRequest(getDevVersion, {
// uncomment next 2 lines for testing, uses https://github.com/emsesp/EMS-ESP32/releases/download/latest/EMS-ESP-3_7_0-dev_31-ESP32-16MB+.bin
// immediate: false,
// initialData: '3.7.0-dev.32'
}).onSuccess((event) => {
void sendCheckUpgrade(event.data);
});
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const getBinURL = (useDevVersion: boolean) => {
if (!latestVersion || !latestDevVersion) {
return '';
}
const filename =
'EMS-ESP-' +
(useDevVersion ? latestDevVersion : latestVersion).replaceAll('.', '_') +
'-' +
getPlatform() +
'.bin';
return useDevVersion
? DEV_URL + filename
: STABLE_URL + 'v' + latestVersion + '/' + filename;
};
const getPlatform = () => {
return (
[data.esp_platform, data.flash_chip_size >= 16384 ? '16MB' : '4MB'].join('-') +
(data.psram ? '+' : '')
);
};
const installFirmwareURL = async (url: string) => {
await sendUploadURL(url).catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
useLayoutTitle(LL.EMS_ESP_VER());
const internet_live =
latestDevVersion !== undefined && latestVersion !== undefined;
const renderUploadDialog = () => {
if (!internet_live) {
return null;
}
return (
<Dialog
sx={dialogStyle}
open={openDialog}
onClose={() => setOpenDialog(false)}
>
<DialogTitle>
{LL.INSTALL('') +
' ' +
(useDev ? LL.DEVELOPMENT() : LL.STABLE()) +
' Firmware'}
</DialogTitle>
<DialogContent dividers>
<Typography mb={2}>
{LL.INSTALL_VERSION(useDev ? latestDevVersion : latestVersion)}
</Typography>
<Link
target="_blank"
href={useDev ? DEV_RELNOTES_URL : STABLE_RELNOTES_URL}
color="primary"
>
changelog
</Link>
&nbsp;|&nbsp;
<Link target="_blank" href={getBinURL(useDev)} color="primary">
{LL.DOWNLOAD(1)}
</Link>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setOpenDialog(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
variant="outlined"
onClick={() => installFirmwareURL(getBinURL(useDev))}
color="primary"
>
{LL.INSTALL('')}
</Button>
</DialogActions>
</Dialog>
);
};
// useDevVersion = true to force using the dev version
const showFirmwareDialog = (useDevVersion: boolean) => {
if (useDevVersion || data.emsesp_version.includes('dev')) {
setUseDev(true);
}
setOpenDialog(true);
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
const isDev = data.emsesp_version.includes('dev');
return (
<>
<Box p={2} border="1px solid grey" borderRadius={2}>
<Grid container spacing={3}>
<Grid mb={1}>
<Typography mb={1} fontWeight={'fontWeightBold'}>
{LL.VERSION()}
</Typography>
<Typography mb={1} fontWeight={'fontWeightBold'}>
Platform
</Typography>
<Typography mb={1} fontWeight={'fontWeightBold'}>
Release
</Typography>
</Grid>
<Grid>
<Typography mb={1}>
{data.emsesp_version}
{data.build_flags && (
<Typography variant="caption">
&nbsp; &#40;{data.build_flags}&#41;
</Typography>
)}
</Typography>
<Typography mb={1}>{getPlatform()}</Typography>
<Typography>
{isDev ? LL.DEVELOPMENT() : LL.STABLE()}&nbsp;
<Link
target="_blank"
href={useDev ? DEV_RELNOTES_URL : STABLE_RELNOTES_URL}
color="primary"
>
(changelog)
</Link>
</Typography>
</Grid>
</Grid>
<Divider />
{!isDev && (
<Button
sx={{ mt: 2 }}
variant="outlined"
color="primary"
size="small"
onClick={() => showFirmwareDialog(true)}
>
{LL.SWITCH_DEV()}
</Button>
)}
<Typography mt={2} color="warning">
<InfoOutlinedIcon color="warning" sx={{ verticalAlign: 'middle' }} />
&nbsp;&nbsp;
{upgradeAvailable ? LL.UPGRADE_AVAILABLE() : LL.LATEST_VERSION()}
{upgradeAvailable &&
internet_live &&
(data.psram ? (
<Button
sx={{ ml: 2, textTransform: 'none' }}
variant="outlined"
color="primary"
size="small"
onClick={() => showFirmwareDialog(false)}
>
{isDev
? LL.INSTALL('v' + latestDevVersion)
: LL.INSTALL('v' + latestVersion)}
</Button>
) : (
<>
&nbsp;&nbsp;
<Link target="_blank" href={getBinURL(isDev)} color="primary">
{LL.DOWNLOAD(1)}&nbsp;v
{isDev ? latestDevVersion : latestVersion}
</Link>
</>
))}
</Typography>
{renderUploadDialog()}
</Box>
</>
);
};
return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
};
export default Version;

View File

@@ -1,16 +1,9 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
Navigate,
Route,
Routes,
matchRoutes,
useLocation,
useNavigate
} from 'react-router';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { RouterTabs, useLayoutTitle } from 'components'; import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { WiFiNetwork } from 'types'; import type { WiFiNetwork } from 'types';
@@ -20,22 +13,9 @@ import WiFiNetworkScanner from './WiFiNetworkScanner';
const Network = () => { const Network = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK(0)); useLayoutTitle(LL.SETTINGS_OF(LL.NETWORK(0)));
// this also works! const { routerTab } = useRouterTab();
// const routerTab = useMatch(`settings/network/:path/*`)?.pathname || false;
const matchedRoutes = matchRoutes(
[
{
path: '/settings/network/settings',
element: <NetworkSettings />,
dog: 'woof'
},
{ path: '/settings/network/scan', element: <WiFiNetworkScanner /> }
],
useLocation()
);
const routerTab = matchedRoutes?.[0]?.route.path || false;
const navigate = useNavigate(); const navigate = useNavigate();
@@ -44,7 +24,7 @@ const Network = () => {
const selectNetwork = useCallback( const selectNetwork = useCallback(
(network: WiFiNetwork) => { (network: WiFiNetwork) => {
setSelectedNetwork(network); setSelectedNetwork(network);
void navigate('/settings/network/settings'); navigate('settings');
}, },
[navigate] [navigate]
); );
@@ -56,25 +36,19 @@ const Network = () => {
return ( return (
<WiFiConnectionContext.Provider <WiFiConnectionContext.Provider
value={{ value={{
...(selectedNetwork && { selectedNetwork }), selectedNetwork,
selectNetwork, selectNetwork,
deselectNetwork deselectNetwork
}} }}
> >
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab <Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} />
value="/settings/network/settings" <Tab value="scan" label={LL.NETWORK_SCAN()} />
label={LL.SETTINGS_OF(LL.NETWORK(1))}
/>
<Tab value="/settings/network/scan" label={LL.NETWORK_SCAN()} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="scan" element={<WiFiNetworkScanner />} /> <Route path="scan" element={<WiFiNetworkScanner />} />
<Route path="settings" element={<NetworkSettings />} /> <Route path="settings" element={<NetworkSettings />} />
<Route <Route path="*" element={<Navigate replace to="settings" />} />
path="*"
element={<Navigate replace to="/settings/network/settings" />}
/>
</Routes> </Routes>
</WiFiConnectionContext.Provider> </WiFiConnectionContext.Provider>
); );

View File

@@ -43,7 +43,7 @@ import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network'; import { createNetworkSettingsValidator } from 'validators/network';
import SystemMonitor from '../../status/SystemMonitor'; import RestartMonitor from '../../status/RestartMonitor';
import { WiFiConnectionContext } from './WiFiConnectionContext'; import { WiFiConnectionContext } from './WiFiConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector'; import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
@@ -104,7 +104,7 @@ const NetworkSettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue as (value: unknown) => void updateDataValue
); );
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -113,7 +113,7 @@ const NetworkSettings = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -172,7 +172,7 @@ const NetworkSettings = () => {
</List> </List>
) : ( ) : (
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="ssid" name="ssid"
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'} label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
fullWidth fullWidth
@@ -183,7 +183,7 @@ const NetworkSettings = () => {
/> />
)} )}
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="bssid" name="bssid"
label={'BSSID (' + LL.NETWORK_BLANK_BSSID() + ')'} label={'BSSID (' + LL.NETWORK_BLANK_BSSID() + ')'}
fullWidth fullWidth
@@ -194,7 +194,7 @@ const NetworkSettings = () => {
/> />
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && ( {(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
<ValidatedPasswordField <ValidatedPasswordField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="password" name="password"
label={LL.PASSWORD()} label={LL.PASSWORD()}
fullWidth fullWidth
@@ -251,7 +251,7 @@ const NetworkSettings = () => {
{LL.GENERAL_OPTIONS()} {LL.GENERAL_OPTIONS()}
</Typography> </Typography>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="hostname" name="hostname"
label={LL.HOSTNAME()} label={LL.HOSTNAME()}
fullWidth fullWidth
@@ -304,7 +304,7 @@ const NetworkSettings = () => {
{data.static_ip_config && ( {data.static_ip_config && (
<> <>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="local_ip" name="local_ip"
label={LL.AP_LOCAL_IP()} label={LL.AP_LOCAL_IP()}
fullWidth fullWidth
@@ -314,7 +314,7 @@ const NetworkSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="gateway_ip" name="gateway_ip"
label={LL.NETWORK_GATEWAY()} label={LL.NETWORK_GATEWAY()}
fullWidth fullWidth
@@ -324,7 +324,7 @@ const NetworkSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="subnet_mask" name="subnet_mask"
label={LL.NETWORK_SUBNET()} label={LL.NETWORK_SUBNET()}
fullWidth fullWidth
@@ -334,7 +334,7 @@ const NetworkSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="dns_ip_1" name="dns_ip_1"
label="DNS #1" label="DNS #1"
fullWidth fullWidth
@@ -344,7 +344,7 @@ const NetworkSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="dns_ip_2" name="dns_ip_2"
label="DNS #2" label="DNS #2"
fullWidth fullWidth
@@ -400,7 +400,7 @@ const NetworkSettings = () => {
return ( return (
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <SystemMonitor /> : content()} {restarting ? <RestartMonitor /> : content()}
</SectionContent> </SectionContent>
); );
}; };

View File

@@ -50,7 +50,9 @@ const WiFiNetworkScanner = () => {
const renderNetworkScanner = () => { const renderNetworkScanner = () => {
if (!networkList) { if (!networkList) {
return <FormLoader errorMessage={errorMessage || ''} />; return (
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
);
} }
return <WiFiNetworkSelector networkList={networkList} />; return <WiFiNetworkSelector networkList={networkList} />;
}; };

View File

@@ -1,10 +1,10 @@
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { useBlocker } from 'react-router'; import { useBlocker } from 'react-router-dom';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import CheckIcon from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import PersonAddIcon from '@mui/icons-material/PersonAdd'; import PersonAddIcon from '@mui/icons-material/PersonAdd';
import VpnKeyIcon from '@mui/icons-material/VpnKey'; import VpnKeyIcon from '@mui/icons-material/VpnKey';
@@ -97,7 +97,7 @@ const ManageUsers = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
const noAdminConfigured = () => !data.users.find((u) => u.admin); const noAdminConfigured = () => !data.users.find((u) => u.admin);
@@ -260,11 +260,7 @@ const ManageUsers = () => {
</Box> </Box>
</Box> </Box>
<GenerateToken <GenerateToken username={generatingToken} onClose={closeGenerateToken} />
username={generatingToken || ''}
onClose={closeGenerateToken}
/>
{user && (
<User <User
user={user} user={user}
setUser={setUser} setUser={setUser}
@@ -273,7 +269,6 @@ const ManageUsers = () => {
onCancelEditing={cancelEditingUser} onCancelEditing={cancelEditingUser}
validator={createUserValidator(data.users, creating)} validator={createUserValidator(data.users, creating)}
/> />
)}
</> </>
); );
}; };

View File

@@ -1,8 +1,8 @@
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router'; import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { RouterTabs, useLayoutTitle } from 'components'; import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import ManageUsers from './ManageUsers'; import ManageUsers from './ManageUsers';
@@ -10,33 +10,20 @@ import SecuritySettings from './SecuritySettings';
const Security = () => { const Security = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.SECURITY(0)); useLayoutTitle(LL.SETTINGS_OF(LL.SECURITY(0)));
const matchedRoutes = matchRoutes( const { routerTab } = useRouterTab();
[
{ path: '/settings/security/settings', element: <ManageUsers />, dog: 'woof' },
{ path: '/settings/security/users', element: <SecuritySettings /> }
],
useLocation()
);
const routerTab = matchedRoutes?.[0]?.route.path || false;
return ( return (
<> <>
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab <Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
value="/settings/security/settings" <Tab value="users" label={LL.MANAGE_USERS()} />
label={LL.SETTINGS_OF(LL.SECURITY(1))}
/>
<Tab value="/settings/security/users" label={LL.MANAGE_USERS()} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="users" element={<ManageUsers />} /> <Route path="users" element={<ManageUsers />} />
<Route path="settings" element={<SecuritySettings />} /> <Route path="settings" element={<SecuritySettings />} />
<Route <Route path="*" element={<Navigate replace to="settings" />} />
path="*"
element={<Navigate replace to="/settings/security/settings" />}
/>
</Routes> </Routes>
</> </>
); );

View File

@@ -47,12 +47,12 @@ const SecuritySettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue as (value: unknown) => void updateDataValue
); );
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -69,7 +69,7 @@ const SecuritySettings = () => {
return ( return (
<> <>
<ValidatedPasswordField <ValidatedPasswordField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="jwt_secret" name="jwt_secret"
label={LL.SU_PASSWORD()} label={LL.SU_PASSWORD()}
fullWidth fullWidth

View File

@@ -82,7 +82,7 @@ const User: FC<UserFormProps> = ({
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="username" name="username"
label={LL.USERNAME(1)} label={LL.USERNAME(1)}
fullWidth fullWidth
@@ -93,7 +93,7 @@ const User: FC<UserFormProps> = ({
margin="normal" margin="normal"
/> />
<ValidatedPasswordField <ValidatedPasswordField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors}
name="password" name="password"
label={LL.PASSWORD()} label={LL.PASSWORD()}
fullWidth fullWidth

View File

@@ -14,12 +14,11 @@ import type { Theme } from '@mui/material';
import * as APApi from 'api/ap'; import * as APApi from 'api/ap';
import { useRequest } from 'alova/client'; import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { APStatusType } from 'types'; import type { APStatusType } from 'types';
import { APNetworkStatus } from 'types'; import { APNetworkStatus } from 'types';
import { useInterval } from 'utils';
export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => { export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
switch (status) { switch (status) {
@@ -35,14 +34,14 @@ export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
}; };
const APStatus = () => { const APStatus = () => {
const { data, send: loadData, error } = useRequest(APApi.readAPStatus); const {
data,
useInterval(() => { send: loadData,
void loadData(); error
}); } = useAutoRequest(APApi.readAPStatus, { pollingTime: 3000 });
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.ACCESS_POINT(0)); useLayoutTitle(LL.STATUS_OF(LL.ACCESS_POINT(0)));
const theme = useTheme(); const theme = useTheme();
@@ -61,7 +60,7 @@ const APStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (

View File

@@ -8,21 +8,20 @@ import {
Table Table
} from '@table-library/react-table-library/table'; } from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme'; import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova/client'; import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { Translation } from 'i18n/i18n-types'; import type { Translation } from 'i18n/i18n-types';
import { useInterval } from 'utils';
import { readActivity } from '../../api/app'; import { readActivity } from '../../api/app';
import type { Stat } from '../main/types'; import type { Stat } from '../main/types';
const SystemActivity = () => { const SystemActivity = () => {
const { data, send: loadData, error } = useRequest(readActivity); const {
data,
useInterval(() => { send: loadData,
void loadData(); error
}); } = useAutoRequest(readActivity, { pollingTime: 3000 });
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -67,8 +66,7 @@ const SystemActivity = () => {
}); });
const showName = (id: number) => { const showName = (id: number) => {
const name: keyof Translation['STATUS_NAMES'] = const name: keyof Translation['STATUS_NAMES'] = id;
id.toString() as keyof Translation['STATUS_NAMES'];
return LL.STATUS_NAMES[name](); return LL.STATUS_NAMES[name]();
}; };
@@ -88,7 +86,7 @@ const SystemActivity = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (

View File

@@ -17,10 +17,9 @@ import {
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { useRequest } from 'alova/client'; import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { useInterval } from 'utils';
import BBQKeesIcon from './bbqkees.svg'; import BBQKeesIcon from './bbqkees.svg';
@@ -31,17 +30,17 @@ function formatNumber(num: number) {
const HardwareStatus = () => { const HardwareStatus = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.HARDWARE()); useLayoutTitle(LL.STATUS_OF(LL.HARDWARE()));
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus); const {
data,
useInterval(() => { send: loadData,
void loadData(); error
}); } = useAutoRequest(SystemApi.readSystemStatus, { pollingTime: 3000 });
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (
@@ -99,13 +98,7 @@ const HardwareStatus = () => {
' @ ' + ' @ ' +
data.cpu_freq_mhz + data.cpu_freq_mhz +
' Mhz' + ' Mhz' +
// bit of a hack : if the CPU temp is higher than 90 (=32 Fahrenheit if using Celsius), show F, otherwise C (data.temperature ? ', T: ' + data.temperature + ' °C' : '')
(data.temperature
? ', T: ' +
data.temperature +
' °' +
(data.temperature > 90 ? 'F' : 'C')
: '')
} }
/> />
</ListItem> </ListItem>

View File

@@ -15,12 +15,11 @@ import type { Theme } from '@mui/material';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
import { useRequest } from 'alova/client'; import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { MqttStatusType } from 'types'; import type { MqttStatusType } from 'types';
import { MqttDisconnectReason } from 'types'; import { MqttDisconnectReason } from 'types';
import { useInterval } from 'utils';
export const mqttStatusHighlight = ( export const mqttStatusHighlight = (
{ enabled, connected }: MqttStatusType, { enabled, connected }: MqttStatusType,
@@ -55,14 +54,14 @@ export const mqttQueueHighlight = (
}; };
const MqttStatus = () => { const MqttStatus = () => {
const { data, send: loadData, error } = useRequest(MqttApi.readMqttStatus); const {
data,
useInterval(() => { send: loadData,
void loadData(); error
}); } = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 3000 });
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle('MQTT'); useLayoutTitle(LL.STATUS_OF('MQTT'));
const theme = useTheme(); const theme = useTheme();
@@ -71,9 +70,9 @@ const MqttStatus = () => {
return LL.NOT_ENABLED(); return LL.NOT_ENABLED();
} }
if (connected) { if (connected) {
return LL.CONNECTED(0) + ' (' + connect_count + ')'; return LL.CONNECTED(0) + (connect_count > 1 ? ' (' + connect_count + ')' : '');
} }
return LL.DISCONNECTED() + ' (' + connect_count + ')'; return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : '');
}; };
const disconnectReason = ({ disconnect_reason }: MqttStatusType) => { const disconnectReason = ({ disconnect_reason }: MqttStatusType) => {
@@ -99,7 +98,7 @@ const MqttStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
const renderConnectionStatus = () => ( const renderConnectionStatus = () => (

View File

@@ -1,40 +1,65 @@
import { useState } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel';
import DnsIcon from '@mui/icons-material/Dns'; import DnsIcon from '@mui/icons-material/Dns';
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle'; import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
import UpdateIcon from '@mui/icons-material/Update'; import UpdateIcon from '@mui/icons-material/Update';
import { import {
Avatar, Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider, Divider,
List, List,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
TextField,
Typography,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import * as NTPApi from 'api/ntp'; import * as NTPApi from 'api/ntp';
import { useRequest } from 'alova/client'; import { dialogStyle } from 'CustomTheme';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useAutoRequest, useRequest } from 'alova/client';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NTPStatusType } from 'types'; import type { NTPStatusType, Time } from 'types';
import { NTPSyncStatus } from 'types'; import { NTPSyncStatus } from 'types';
import { useInterval } from 'utils'; import { formatDateTime, formatLocalDateTime } from 'utils';
import { formatDateTime } from 'utils';
const NTPStatus = () => { const NTPStatus = () => {
const { data, send: loadData, error } = useRequest(NTPApi.readNTPStatus); const {
data,
send: loadData,
error
} = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 3000 });
useInterval(() => { const [localTime, setLocalTime] = useState<string>('');
void loadData(); const [settingTime, setSettingTime] = useState<boolean>(false);
}); const [processing, setProcessing] = useState<boolean>(false);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle('NTP'); useLayoutTitle(LL.STATUS_OF('NTP'));
const { send: updateTime } = useRequest(
(local_time: Time) => NTPApi.updateTime(local_time),
{
immediate: false
}
);
NTPApi.updateTime; NTPApi.updateTime;
const isNtpActive = ({ status }: NTPStatusType) =>
status === NTPSyncStatus.NTP_ACTIVE;
const isNtpEnabled = ({ status }: NTPStatusType) => const isNtpEnabled = ({ status }: NTPStatusType) =>
status !== NTPSyncStatus.NTP_DISABLED; status !== NTPSyncStatus.NTP_DISABLED;
@@ -51,6 +76,14 @@ const NTPStatus = () => {
} }
}; };
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
setLocalTime(event.target.value);
const openSetTime = () => {
setLocalTime(formatLocalDateTime(new Date()));
setSettingTime(true);
};
const theme = useTheme(); const theme = useTheme();
const ntpStatus = ({ status }: NTPStatusType) => { const ntpStatus = ({ status }: NTPStatusType) => {
@@ -66,9 +99,73 @@ const NTPStatus = () => {
} }
}; };
const configureTime = async () => {
setProcessing(true);
await updateTime({ local_time: formatLocalDateTime(new Date(localTime)) })
.then(async () => {
toast.success(LL.TIME_SET());
setSettingTime(false);
await loadData();
})
.catch(() => {
toast.error(LL.PROBLEM_UPDATING());
})
.finally(() => {
setProcessing(false);
});
};
const renderSetTimeDialog = () => (
<Dialog
sx={dialogStyle}
open={settingTime}
onClose={() => setSettingTime(false)}
>
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
</Box>
<TextField
label={LL.LOCAL_TIME(0)}
type="datetime-local"
value={localTime}
onChange={updateLocalTime}
disabled={processing}
fullWidth
slotProps={{
inputLabel: {
shrink: true
}
}}
/>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setSettingTime(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<AccessTimeIcon />}
variant="outlined"
onClick={configureTime}
disabled={processing}
color="primary"
>
{LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (
@@ -121,6 +218,23 @@ const NTPStatus = () => {
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</List> </List>
<Box display="flex" flexWrap="wrap">
{data && !isNtpActive(data) && (
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
onClick={openSetTime}
variant="outlined"
color="primary"
startIcon={<AccessTimeIcon />}
>
{LL.SET_TIME(0)}
</Button>
</ButtonRow>
</Box>
)}
</Box>
{renderSetTimeDialog()}
</> </>
); );
}; };

View File

@@ -18,12 +18,11 @@ import type { Theme } from '@mui/material';
import * as NetworkApi from 'api/network'; import * as NetworkApi from 'api/network';
import { useRequest } from 'alova/client'; import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NetworkStatusType } from 'types'; import type { NetworkStatusType } from 'types';
import { NetworkConnectionStatus } from 'types'; import { NetworkConnectionStatus } from 'types';
import { useInterval } from 'utils';
const isConnected = ({ status }: NetworkStatusType) => const isConnected = ({ status }: NetworkStatusType) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED || status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
@@ -82,21 +81,19 @@ const IPs = (status: NetworkStatusType) => {
}; };
const NetworkStatus = () => { const NetworkStatus = () => {
const { data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus); const {
data,
useInterval(() => { send: loadData,
void loadData(); error
}); } = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 3000 });
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK(1)); useLayoutTitle(LL.STATUS_OF(LL.NETWORK(1)));
const theme = useTheme(); const theme = useTheme();
const networkStatus = ({ status }: NetworkStatusType) => { const networkStatus = ({ status }: NetworkStatusType) => {
switch (status) { switch (status) {
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD: case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return LL.INACTIVE(1); return LL.INACTIVE(1);
case NetworkConnectionStatus.WIFI_STATUS_IDLE: case NetworkConnectionStatus.WIFI_STATUS_IDLE:
@@ -104,13 +101,13 @@ const NetworkStatus = () => {
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL: case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return 'No SSID Available'; return 'No SSID Available';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED: case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (WiFi) (' + data.reconnect_count + ')'; return LL.CONNECTED(0) + ' (WiFi)';
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED: case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return ( return LL.CONNECTED(1) + ' ' + LL.FAILED(0);
LL.CONNECTED(1) + ' ' + LL.FAILED(0) + ' (' + data.reconnect_count + ')'
);
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST: case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return LL.CONNECTED(1) + ' ' + LL.LOST() + ' (' + data.reconnect_count + ')'; return LL.CONNECTED(1) + ' ' + LL.LOST();
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED: case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
return LL.DISCONNECTED(); return LL.DISCONNECTED();
default: default:
@@ -120,7 +117,7 @@ const NetworkStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (

View File

@@ -0,0 +1,81 @@
import { useState } from 'react';
import {
Box,
CircularProgress,
Dialog,
DialogContent,
Typography
} from '@mui/material';
import { readSystemStatus } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useAutoRequest } from 'alova/client';
import MessageBox from 'components/MessageBox';
import { useI18nContext } from 'i18n/i18n-react';
const RestartMonitor = () => {
const [errorMessage, setErrorMessage] = useState<string>();
const { LL } = useI18nContext();
let count = 0;
const { data } = useAutoRequest(readSystemStatus, {
pollingTime: 1000,
force: true,
initialData: { status: 'Getting ready...' },
async middleware(_, next) {
if (count++ >= 1) {
// skip first request (1 second) to allow AsyncWS to send its response
await next();
}
}
})
.onSuccess((event) => {
if (event.data.status === 'ready' || event.data.status === undefined) {
document.location.href = '/';
}
})
.onError((error) => {
setErrorMessage(error.message);
});
return (
<Dialog fullWidth={true} sx={dialogStyle} open={true}>
<DialogContent dividers>
<Box m={0} py={0} display="flex" alignItems="center" flexDirection="column">
<Typography
color="secondary"
variant="h6"
fontWeight={400}
textAlign="center"
>
{data?.status === 'uploading'
? LL.WAIT_FIRMWARE()
: data?.status === 'restarting'
? LL.APPLICATION_RESTARTING()
: data?.status === 'ready'
? LL.RESTARTING_PRE()
: LL.RESTARTING_POST()}
&hellip;
</Typography>
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center">
{LL.PLEASE_WAIT()}
</Typography>
{errorMessage ? (
<MessageBox my={2} level="error" message={errorMessage} />
) : (
<Box py={2}>
<CircularProgress size={32} />
</Box>
)}
</Box>
</DialogContent>
</Dialog>
);
};
export default RestartMonitor;

View File

@@ -2,7 +2,6 @@ import { useContext, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
@@ -31,17 +30,15 @@ import { API } from 'api/app';
import { readSystemStatus } from 'api/system'; import { readSystemStatus } from 'api/system';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client'; import { useAutoRequest, useRequest } from 'alova/client';
import { type APIcall, busConnectionStatus } from 'app/main/types'; import { type APIcall, busConnectionStatus } from 'app/main/types';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { NTPSyncStatus, NetworkConnectionStatus } from 'types'; import { NTPSyncStatus, NetworkConnectionStatus } from 'types';
import { useInterval } from 'utils';
import { formatDateTime } from 'utils/time';
import SystemMonitor from './SystemMonitor'; import RestartMonitor from './RestartMonitor';
const SystemStatus = () => { const SystemStatus = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -61,8 +58,9 @@ const SystemStatus = () => {
data, data,
send: loadData, send: loadData,
error error
} = useRequest(readSystemStatus, { } = useAutoRequest(readSystemStatus, {
initialData: [], initialData: [],
pollingTime: 3000,
async middleware(_, next) { async middleware(_, next) {
if (!restarting) { if (!restarting) {
await next(); await next();
@@ -70,10 +68,6 @@ const SystemStatus = () => {
} }
}); });
useInterval(() => {
void loadData();
});
const theme = useTheme(); const theme = useTheme();
const formatDurationSec = (duration_sec: number) => { const formatDurationSec = (duration_sec: number) => {
@@ -140,12 +134,7 @@ const SystemStatus = () => {
case NTPSyncStatus.NTP_INACTIVE: case NTPSyncStatus.NTP_INACTIVE:
return LL.INACTIVE(0); return LL.INACTIVE(0);
case NTPSyncStatus.NTP_ACTIVE: case NTPSyncStatus.NTP_ACTIVE:
return ( return LL.ACTIVE();
LL.ACTIVE() +
(data.ntp_time !== undefined
? ' (' + formatDateTime(data.ntp_time) + ')'
: '')
);
default: default:
return LL.UNKNOWN(); return LL.UNKNOWN();
} }
@@ -248,20 +237,12 @@ const SystemStatus = () => {
const content = () => { const content = () => {
if (!data || !LL) { if (!data || !LL) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (
<> <>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}> <List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"
label="EMS-ESP Firmware"
text={'v' + data.emsesp_version}
to="version"
/>
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}> <Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
@@ -320,7 +301,7 @@ const SystemStatus = () => {
icon={DeviceHubIcon} icon={DeviceHubIcon}
bgcolor={activeHighlight(data.mqtt_status)} bgcolor={activeHighlight(data.mqtt_status)}
label="MQTT" label="MQTT"
text={data.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)} text={data.mqtt_status ? LL.ACTIVE() : LL.INACTIVE(0)}
to="/status/mqtt" to="/status/mqtt"
/> />
@@ -358,7 +339,7 @@ const SystemStatus = () => {
}; };
return ( return (
<SectionContent>{restarting ? <SystemMonitor /> : content()}</SectionContent> <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
); );
}; };

View File

@@ -8,12 +8,12 @@ import {
Box, Box,
Button, Button,
Checkbox, Checkbox,
Grid,
IconButton, IconButton,
MenuItem, MenuItem,
TextField, TextField,
styled styled
} from '@mui/material'; } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { API } from 'api/app'; import { API } from 'api/app';
import { fetchLogES, readLogSettings, updateLogSettings } from 'api/system'; import { fetchLogES, readLogSettings, updateLogSettings } from 'api/system';
@@ -31,14 +31,13 @@ import type { LogEntry, LogSettings } from 'types';
import { LogLevel } from 'types'; import { LogLevel } from 'types';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
const TextColors: Record<LogLevel, string> = { const TextColors = {
[LogLevel.ERROR]: '#ff0000', // red [LogLevel.ERROR]: '#ff0000', // red
[LogLevel.WARNING]: '#ff0000', // red [LogLevel.WARNING]: '#ff0000', // red
[LogLevel.NOTICE]: '#ffffff', // white [LogLevel.NOTICE]: '#ffffff', // white
[LogLevel.INFO]: '#ffcc00', // yellow [LogLevel.INFO]: '#ffcc00', // yellow
[LogLevel.DEBUG]: '#00ffff', // cyan [LogLevel.DEBUG]: '#00ffff', // cyan
[LogLevel.TRACE]: '#00ffff', // cyan [LogLevel.TRACE]: '#00ffff' // cyan
[LogLevel.ALL]: '#ffffff' // white
}; };
const LogEntryLine = styled('span')( const LogEntryLine = styled('span')(
@@ -102,7 +101,6 @@ const SystemLog = () => {
const [readOpen, setReadOpen] = useState(false); const [readOpen, setReadOpen] = useState(false);
const [logEntries, setLogEntries] = useState<LogEntry[]>([]); const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const [autoscroll, setAutoscroll] = useState(true); const [autoscroll, setAutoscroll] = useState(true);
const [lastId, setLastId] = useState<number>(-1);
const ALPHA_NUMERIC_DASH_REGEX = /^[a-fA-F0-9 ]+$/; const ALPHA_NUMERIC_DASH_REGEX = /^[a-fA-F0-9 ]+$/;
@@ -110,20 +108,17 @@ const SystemLog = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue as (value: unknown) => void updateDataValue
); );
useSSE(fetchLogES, { useSSE(fetchLogES, {
immediate: true, immediate: true,
interceptByGlobalResponded: false interceptByGlobalResponded: false
}) })
.onMessage((message: { data: string }) => { .onMessage((message: { id: number; data: string }) => {
const rawData = message.data; const rawData = message.data;
const logentry = JSON.parse(rawData) as LogEntry; const logentry = JSON.parse(rawData) as LogEntry;
if (lastId < logentry.i) {
setLogEntries((log) => [...log, logentry]); setLogEntries((log) => [...log, logentry]);
setLastId(logentry.i);
}
}) })
.onError(() => { .onError(() => {
toast.error('No connection to Log service'); toast.error('No connection to Log service');
@@ -191,7 +186,7 @@ const SystemLog = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
} }
return ( return (
@@ -202,7 +197,7 @@ const SystemLog = () => {
name="level" name="level"
label={LL.LOG_LEVEL()} label={LL.LOG_LEVEL()}
value={data.level} value={data.level}
sx={{ width: '14ch' }} sx={{ width: '10ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
margin="normal" margin="normal"
@@ -266,6 +261,16 @@ const SystemLog = () => {
> >
{LL.EXPORT()} {LL.EXPORT()}
</Button> </Button>
{dirtyFlags && dirtyFlags.length !== 0 && (
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
color="info"
onClick={saveSettings}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
)}
</Grid> </Grid>
{readOpen ? ( {readOpen ? (
@@ -310,19 +315,6 @@ const SystemLog = () => {
)} )}
</> </>
)} )}
{dirtyFlags && dirtyFlags.length !== 0 && (
<Grid>
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
color="info"
onClick={saveSettings}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
</Grid>
)}
</Grid> </Grid>
<Box <Box

View File

@@ -1,125 +0,0 @@
import { useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import { Box, Button, Dialog, DialogContent, Typography } from '@mui/material';
import { callAction } from 'api/app';
import { readSystemStatus } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import MessageBox from 'components/MessageBox';
import { useI18nContext } from 'i18n/i18n-react';
import { SystemStatusCodes } from 'types';
import { useInterval } from 'utils';
import { LinearProgressWithLabel } from '../../components/upload/LinearProgressWithLabel';
const SystemMonitor = () => {
const [errorMessage, setErrorMessage] = useState<string>();
const { LL } = useI18nContext();
let count = 0;
const { send: setSystemStatus } = useRequest(
(status: string) => callAction({ action: 'systemStatus', param: status }),
{
immediate: false
}
);
const { data, send } = useRequest(readSystemStatus, {
force: true,
async middleware(_, next) {
if (count++ >= 1) {
// skip first request (1 second) to allow AsyncWS to send its response
await next();
}
}
})
.onSuccess((event) => {
if (
event.data.status === SystemStatusCodes.SYSTEM_STATUS_NORMAL ||
event.data.status === undefined
) {
document.location.href = '/';
} else if (
event.data.status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
) {
setErrorMessage('Please check system logs for possible causes');
}
})
.onError((error) => {
setErrorMessage(String(error.error?.message || 'An error occurred'));
});
useInterval(() => {
void send();
}, 1000); // check every 1 second
const onCancel = async () => {
setErrorMessage(undefined);
await setSystemStatus(
SystemStatusCodes.SYSTEM_STATUS_NORMAL as unknown as string
);
document.location.href = '/';
};
return (
<Dialog fullWidth={true} sx={dialogStyle} open={true}>
<DialogContent dividers>
<Box m={0} py={0} display="flex" alignItems="center" flexDirection="column">
<Typography
color="secondary"
variant="h6"
fontWeight={400}
textAlign="center"
>
{data?.status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
? LL.WAIT_FIRMWARE()
: data?.status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
? LL.APPLICATION_RESTARTING()
: data?.status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
? LL.RESTARTING_PRE()
: data?.status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
? 'Upload Failed'
: LL.RESTARTING_POST()}
</Typography>
{errorMessage ? (
<MessageBox my={2} level="error" message={errorMessage}>
<Button
size="small"
sx={{ ml: 2 }}
startIcon={<CancelIcon />}
variant="contained"
color="error"
onClick={onCancel}
>
{LL.RESTART()}
</Button>
</MessageBox>
) : (
<>
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center">
{LL.PLEASE_WAIT()}&hellip;
</Typography>
{data && data.status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING && (
<Box width="100%" pl={2} pr={2} py={2}>
<LinearProgressWithLabel
value={Math.round(
data?.status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING
)}
/>
</Box>
)}
</>
)}
</Box>
</DialogContent>
</Dialog>
);
};
export default SystemMonitor;

View File

@@ -1,496 +0,0 @@
import { useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Done';
import DownloadIcon from '@mui/icons-material/GetApp';
import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
Grid,
Link,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
import { API, callAction } from 'api/app';
import { getDevVersion, getStableVersion } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import SystemMonitor from 'app/status/SystemMonitor';
import {
FormLoader,
SectionContent,
SingleUpload,
useLayoutTitle
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
const Version = () => {
const { LL, locale } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
const [restarting, setRestarting] = useState<boolean>(false);
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false);
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false);
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
useState<boolean>(false);
const [internetLive, setInternetLive] = useState<boolean>(false);
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const { send: sendCheckUpgrade } = useRequest(
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }),
{
immediate: false
}
).onSuccess((event) => {
const data = event.data as {
emsesp_version: string;
dev_upgradeable: boolean;
stable_upgradeable: boolean;
};
setDevUpgradeAvailable(data.dev_upgradeable);
setStableUpgradeAvailable(data.stable_upgradeable);
});
const {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
// older version of EMS-ESP using ESP32 (not S3) and no PSRAM, can't use OTA because of SSL support in HttpClient
if (event.data.arduino_version.startsWith('Tasmota')) {
setDownloadOnly(true);
}
setUsingDevVersion(event.data.emsesp_version.includes('dev'));
});
const { send: sendUploadURL } = useRequest(
(url: string) => callAction({ action: 'uploadURL', param: url }),
{
immediate: false
}
);
// called immediately to get the latest versions on page load
const { data: latestVersion } = useRequest(getStableVersion);
const { data: latestDevVersion } = useRequest(getDevVersion);
useEffect(() => {
if (latestVersion && latestDevVersion) {
sendCheckUpgrade(latestDevVersion.name + ',' + latestVersion.name)
.catch((error: Error) => {
toast.error('Failed to check for upgrades: ' + error.message);
})
.finally(() => {
setInternetLive(true);
});
}
}, [latestVersion, latestDevVersion]);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const DIVISIONS: Array<{ amount: number; name: string }> = [
{ amount: 60, name: 'seconds' },
{ amount: 60, name: 'minutes' },
{ amount: 24, name: 'hours' },
{ amount: 7, name: 'days' },
{ amount: 4.34524, name: 'weeks' },
{ amount: 12, name: 'months' },
{ amount: Number.POSITIVE_INFINITY, name: 'years' }
];
function formatTimeAgo(date: Date) {
let duration = (date.getTime() - new Date().getTime()) / 1000;
for (let i = 0; i < DIVISIONS.length; i++) {
const division = DIVISIONS[i];
if (division && Math.abs(duration) < division.amount) {
return rtf.format(
Math.round(duration),
division.name as Intl.RelativeTimeFormatUnit
);
}
if (division) {
duration /= division.amount;
}
}
return rtf.format(0, 'seconds');
}
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const doRestart = async () => {
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const getBinURL = (showingDev: boolean) => {
if (!internetLive) {
return '';
}
const filename =
'EMS-ESP-' +
(showingDev ? latestDevVersion.name : latestVersion.name).replaceAll(
'.',
'_'
) +
'-' +
getPlatform() +
'.bin';
return showingDev
? DEV_URL + filename
: STABLE_URL + 'v' + latestVersion.name + '/' + filename;
};
const getPlatform = () => {
return (
[data.esp_platform, data.flash_chip_size >= 16384 ? '16MB' : '4MB'].join('-') +
(data.psram ? '+' : '')
);
};
const installFirmwareURL = async (url: string) => {
await sendUploadURL(url).catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
useLayoutTitle('EMS-ESP Firmware');
const renderInstallDialog = () => {
const binURL = getBinURL(fetchDevVersion);
return (
<Dialog
sx={dialogStyle}
open={openInstallDialog}
onClose={() => closeInstallDialog()}
>
<DialogTitle>
{LL.UPDATE() +
' ' +
(fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()) +
' Firmware'}
</DialogTitle>
<DialogContent dividers>
<Typography mb={2}>
{LL.INSTALL_VERSION(
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
fetchDevVersion ? latestDevVersion?.name : latestVersion?.name
)}
</Typography>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => closeInstallDialog()}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
onClick={() => closeInstallDialog()}
color="primary"
>
<Link underline="none" target="_blank" href={binURL} color="primary">
{LL.DOWNLOAD(0)}
</Link>
</Button>
{!downloadOnly && (
<Button
startIcon={<WarningIcon color="warning" />}
variant="outlined"
onClick={() => installFirmwareURL(binURL)}
color="primary"
>
{LL.INSTALL()}
</Button>
)}
</DialogActions>
</Dialog>
);
};
const showFirmwareDialog = (useDevVersion: boolean) => {
setFetchDevVersion(useDevVersion);
setOpenInstallDialog(true);
};
const closeInstallDialog = () => {
setOpenInstallDialog(false);
};
const showButtons = (showingDev: boolean) => {
const choice = showingDev
? !usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
: devUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined
: usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
: stableUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined;
if (!choice) {
return (
<>
<CheckIcon
color="success"
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
/>
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
</span>
<Button
sx={{ ml: 2 }}
variant="outlined"
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{LL.REINSTALL()}
</Button>
</>
);
}
if (!me.admin) {
return;
}
return (
<Button
sx={{ ml: 2 }}
variant="outlined"
color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{choice}
</Button>
);
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
const isDev = data.emsesp_version.includes('dev');
return (
<>
<Box p={2} border="1px solid grey" borderRadius={2}>
<Typography mb={2} variant="h6" color="primary">
{LL.THIS_VERSION()}
</Typography>
<Grid
container
direction="row"
rowSpacing={1}
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
}}
>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.VERSION()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{data.emsesp_version}
{data.build_flags && (
<Typography variant="caption">
&nbsp; &#40;{data.build_flags}&#41;
</Typography>
)}
</Typography>
</Grid>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.PLATFORM()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{getPlatform()}
<Typography variant="caption">
&nbsp; &#40;
{data.psram ? (
<CheckIcon
color="success"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
) : (
<CloseIcon
color="error"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
)}
PSRAM&#41;
</Typography>
</Typography>
</Grid>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.RELEASE_TYPE()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<FormControlLabel
disabled={!isDev}
control={
<Checkbox
sx={{
'&.Mui-checked': {
color: 'lightblue'
}
}}
/>
}
slotProps={{
typography: {
color: 'grey'
}
}}
checked={!isDev}
label={LL.STABLE()}
sx={{ '& .MuiSvgIcon-root': { fontSize: 16 } }}
/>
<FormControlLabel
disabled={isDev}
control={
<Checkbox
sx={{
'&.Mui-checked': {
color: 'lightblue'
}
}}
/>
}
slotProps={{
typography: {
color: 'grey'
}
}}
checked={isDev}
label={LL.DEVELOPMENT()}
sx={{ '& .MuiSvgIcon-root': { fontSize: 16 } }}
/>
</Grid>
</Grid>
{internetLive ? (
<>
<Typography mt={2} mb={2} variant="h6" color="primary">
{LL.AVAILABLE_VERSION()}
</Typography>
<Grid
container
direction="row"
rowSpacing={1}
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
}}
>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.STABLE()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{latestVersion.name}
</Link>
{latestVersion.published_at && (
<Typography component="span" variant="caption">
&nbsp;(
{formatTimeAgo(new Date(latestVersion.published_at))})
</Typography>
)}
{showButtons(false)}
</Typography>
</Grid>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
{latestDevVersion.name}
</Link>
{latestDevVersion.published_at && (
<Typography component="span" variant="caption">
&nbsp;(
{formatTimeAgo(new Date(latestDevVersion.published_at))})
</Typography>
)}
{showButtons(true)}
</Typography>
</Grid>
</Grid>
</>
) : (
<Typography mt={2} color="warning">
<WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} />
{LL.INTERNET_CONNECTION_REQUIRED()}
</Typography>
)}
{me.admin && (
<>
{renderInstallDialog()}
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()}
</Typography>
<SingleUpload text={LL.UPLOAD_DROP_TEXT()} doRestart={doRestart} />
</>
)}
</Box>
</>
);
};
return (
<SectionContent>{restarting ? <SystemMonitor /> : content()}</SectionContent>
);
};
export default Version;

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