mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Compare commits
14 Commits
22eb5436ab
...
v3.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57fdaa4b3 | ||
|
|
4841e42286 | ||
|
|
8c2d2b06ed | ||
|
|
38c8b1b7f0 | ||
|
|
6fb5933a02 | ||
|
|
c0944433be | ||
|
|
478e6362c9 | ||
|
|
4d6354db78 | ||
|
|
beab0f0c77 | ||
|
|
c17749bd22 | ||
|
|
2bad769c5c | ||
|
|
8ad89ca64b | ||
|
|
9244d8daec | ||
|
|
02d01334b2 |
@@ -2,7 +2,7 @@ Language: Cpp
|
||||
BasedOnStyle: LLVM
|
||||
UseTab: Never
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 160
|
||||
ColumnLimit: 220
|
||||
TabWidth: 4
|
||||
#BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
|
||||
152
.clang-tidy
152
.clang-tidy
@@ -1,152 +0,0 @@
|
||||
---
|
||||
Checks: >-
|
||||
*,
|
||||
-abseil-*,
|
||||
-android-*,
|
||||
-boost-*,
|
||||
-bugprone-branch-clone,
|
||||
-bugprone-narrowing-conversions,
|
||||
-bugprone-signed-char-misuse,
|
||||
-bugprone-too-small-loop-variable,
|
||||
-cert-dcl50-cpp,
|
||||
-cert-err58-cpp,
|
||||
-cert-oop57-cpp,
|
||||
-cert-str34-c,
|
||||
-clang-analyzer-optin.cplusplus.UninitializedObject,
|
||||
-clang-analyzer-osx.*,
|
||||
-clang-diagnostic-delete-abstract-non-virtual-dtor,
|
||||
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
|
||||
-clang-diagnostic-shadow-field,
|
||||
-clang-diagnostic-sign-compare,
|
||||
-clang-diagnostic-unused-variable,
|
||||
-clang-diagnostic-unused-const-variable,
|
||||
-cppcoreguidelines-avoid-c-arrays,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-init-variables,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-narrowing-conversions,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-const-cast,
|
||||
-cppcoreguidelines-pro-type-cstyle-cast,
|
||||
-cppcoreguidelines-pro-type-member-init,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-static-cast-downcast,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cppcoreguidelines-special-member-functions,
|
||||
-fuchsia-default-arguments,
|
||||
-fuchsia-multiple-inheritance,
|
||||
-fuchsia-overloaded-operator,
|
||||
-fuchsia-statically-constructed-objects,
|
||||
-fuchsia-default-arguments-declarations,
|
||||
-fuchsia-default-arguments-calls,
|
||||
-google-build-using-namespace,
|
||||
-google-explicit-constructor,
|
||||
-google-readability-braces-around-statements,
|
||||
-google-readability-casting,
|
||||
-google-readability-todo,
|
||||
-google-runtime-references,
|
||||
-hicpp-*,
|
||||
-llvm-else-after-return,
|
||||
-llvm-header-guard,
|
||||
-llvm-include-order,
|
||||
-llvm-qualified-auto,
|
||||
-llvmlibc-*,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-misc-no-recursion,
|
||||
-misc-unused-parameters,
|
||||
-modernize-avoid-c-arrays,
|
||||
-modernize-return-braced-init-list,
|
||||
-modernize-use-auto,
|
||||
-modernize-use-default-member-init,
|
||||
-modernize-use-equals-default,
|
||||
-modernize-use-trailing-return-type,
|
||||
-mpi-*,
|
||||
-objc-*,
|
||||
-readability-braces-around-statements,
|
||||
-readability-const-return-type,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-else-after-return,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-isolate-declaration,
|
||||
-readability-magic-numbers,
|
||||
-readability-make-member-function-const,
|
||||
-readability-named-parameter,
|
||||
-readability-qualified-auto,
|
||||
-readability-redundant-access-specifiers,
|
||||
-readability-redundant-member-init,
|
||||
-readability-redundant-string-init,
|
||||
-readability-uppercase-literal-suffix,
|
||||
-readability-use-anyofallof,
|
||||
-warnings-as-errors
|
||||
WarningsAsErrors: '*'
|
||||
AnalyzeTemporaryDtors: false
|
||||
FormatStyle: google
|
||||
CheckOptions:
|
||||
- key: google-readability-braces-around-statements.ShortStatementLines
|
||||
value: '1'
|
||||
- key: google-readability-function-size.StatementThreshold
|
||||
value: '800'
|
||||
- key: google-readability-namespace-comments.ShortNamespaceLines
|
||||
value: '10'
|
||||
- key: google-readability-namespace-comments.SpacesBeforeComments
|
||||
value: '2'
|
||||
- key: modernize-loop-convert.MaxCopySize
|
||||
value: '16'
|
||||
- key: modernize-loop-convert.MinConfidence
|
||||
value: reasonable
|
||||
- key: modernize-loop-convert.NamingStyle
|
||||
value: CamelCase
|
||||
- key: modernize-pass-by-value.IncludeStyle
|
||||
value: llvm
|
||||
- key: modernize-replace-auto-ptr.IncludeStyle
|
||||
value: llvm
|
||||
- key: modernize-use-nullptr.NullMacros
|
||||
value: 'NULL'
|
||||
- key: readability-identifier-naming.LocalVariableCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
value: 'CamelCase'
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: 'CamelCase'
|
||||
- key: readability-identifier-naming.EnumCase
|
||||
value: 'CamelCase'
|
||||
- key: readability-identifier-naming.EnumConstantCase
|
||||
value: 'UPPER_CASE'
|
||||
- key: readability-identifier-naming.StaticConstantCase
|
||||
value: 'UPPER_CASE'
|
||||
- key: readability-identifier-naming.StaticVariableCase
|
||||
value: 'UPPER_CASE'
|
||||
- key: readability-identifier-naming.GlobalConstantCase
|
||||
value: 'UPPER_CASE'
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.PrivateMemberPrefix
|
||||
value: 'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED'
|
||||
- key: readability-identifier-naming.PrivateMethodPrefix
|
||||
value: 'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED'
|
||||
- key: readability-identifier-naming.ClassMemberCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.ClassMemberCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.ProtectedMemberCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.ProtectedMemberSuffix
|
||||
value: '_'
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.ClassMethodCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.ProtectedMethodCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.ProtectedMethodSuffix
|
||||
value: '_'
|
||||
- key: readability-identifier-naming.VirtualMethodCase
|
||||
value: 'lower_case'
|
||||
- key: readability-identifier-naming.VirtualMethodSuffix
|
||||
value: ''
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "EMS-ESP Devcontainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers-extra/features/pnpm:2": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {},
|
||||
"ghcr.io/shyim/devcontainers-features/bun:0": {}
|
||||
},
|
||||
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
3080
|
||||
],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "cd mock-api && pnpm install && cd .. && cd interface && pnpm install",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"platformio.platformio-ide"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
132
.github/CODE_OF_CONDUCT.md
vendored
132
.github/CODE_OF_CONDUCT.md
vendored
@@ -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
|
||||
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,50 +1,35 @@
|
||||
---
|
||||
name: Problem Report/Change Request
|
||||
about: Create a Report to help us improve
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Thanks for reporting an issue for this project. READ THIS FIRST:
|
||||
*Before creating a new issue please check that you have:*
|
||||
|
||||
Please DO NOT OPEN AN ISSUE if your EMS-ESP version is not the latest from the dev branch, please update your device before submitting your issue. Your issue might already be solved. The latest precompiled binaries of EMS-ESP can be downloaded from https://github.com/emsesp/EMS-ESP32/releases/tag/latest
|
||||
* *searched the existing [issues](https://github.com/emsesp/EMS-ESP32/issues) (both open and closed)*
|
||||
* *searched the [documentation help section](https://emsesp.github.io/docs)*
|
||||
|
||||
Please take a few minutes to complete the requested information below.
|
||||
*Completing this template will help developers and contributors to address the issue. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.*
|
||||
|
||||
-->
|
||||
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.*
|
||||
|
||||
### DESCRIPTION
|
||||
**Bug description**
|
||||
*A clear and concise description of what the bug is. Mention which EMS-ESP version you're using.*
|
||||
|
||||
_A clear and concise description of what the problem is or the change requested._
|
||||
**Steps to reproduce**
|
||||
*Steps to reproduce the behavior.*
|
||||
|
||||
### REQUESTED INFORMATION
|
||||
**Expected behavior**
|
||||
*A clear and concise description of what you expected to happen.*
|
||||
|
||||
_Make sure your have performed every step and checked the applicable boxes before submitting your issue. Thank you!_
|
||||
**Screenshots**
|
||||
*If applicable, add screenshots to help explain your problem.*
|
||||
|
||||
- [ ] 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 the [docs](https://docs.emsesp.org/Troubleshooting/)
|
||||
- [ ] Searched the issue in the [chat](https://discord.gg/3J3GgnzpyT)
|
||||
- [ ] Provide the System information in the area below, taken from `http://<IP>/api/system`
|
||||
**Device information**
|
||||
*Copy-paste here the information as it is outputted by the device. You can get this information by from http://ems-esp.local/api?device=system&cmd=info*
|
||||
|
||||
```json
|
||||
Paste System information here....
|
||||
|
||||
|
||||
```
|
||||
|
||||
### TO REPRODUCE
|
||||
|
||||
_Steps to reproduce the behavior:_
|
||||
|
||||
### EXPECTED BEHAVIOUR
|
||||
|
||||
_A clear and concise description of what you expected to happen._
|
||||
|
||||
### SCREENSHOTS
|
||||
|
||||
_If applicable, add screenshots to help explain your issue._
|
||||
|
||||
### ADDITIONAL CONTEXT
|
||||
|
||||
_Add any other context about the issue here._
|
||||
|
||||
**(Please remember to close the issue when it has been addressed)**
|
||||
**Additional context**
|
||||
*Add any other context about the problem here.*
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: EMS-ESP Docs
|
||||
url: https://docs.emsesp.org
|
||||
about: All the information related to EMS-ESP.
|
||||
- name: EMS-ESP Discussions and Support
|
||||
url: https://github.com/emsesp/EMS-ESP32/discussions
|
||||
about: EMS-ESP usage Questions, Feature Requests and Projects.
|
||||
- name: EMS-ESP Users Chat
|
||||
url: https://discord.gg/3J3GgnzpyT
|
||||
about: Chat for feedback, questions and troubleshooting.
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
*Before creating a new feature request please check that you have searched the existing [issues](https://github.com/emsesp/EMS-ESP32/issues) (both open and closed)*
|
||||
|
||||
*Completing this template will help developers and contributors evaluating the feature. If the information provided is not enough the issue will likely be closed.*
|
||||
|
||||
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the request then you can delete them.*
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
*A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]*
|
||||
|
||||
**Describe the solution you'd like**
|
||||
*A clear and concise description of what you want to happen.*
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
*A clear and concise description of any alternative solutions or features you've considered.*
|
||||
|
||||
**Additional context**
|
||||
*Add any other context or screenshots about the feature request here.*
|
||||
29
.github/ISSUE_TEMPLATE/questions---troubleshooting.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/questions---troubleshooting.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Questions & Troubleshooting
|
||||
about: Anything not a bug or feature request
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
*Before creating a new issue please check that you have:*
|
||||
|
||||
* *searched the existing [issues](https://github.com/emsesp/EMS-ESP32/issues) (both open and closed)*
|
||||
* *searched the [documentation help section](https://emsesp.github.io/docs)*
|
||||
|
||||
*Completing this template will help developers and contributors help you. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.*
|
||||
|
||||
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.*
|
||||
|
||||
**Question**
|
||||
*A clear and concise description of what the problem/doubt is.*
|
||||
|
||||
**Screenshots**
|
||||
*If applicable, add screenshots to help explain your problem.*
|
||||
|
||||
**Device information**
|
||||
*Copy-paste here the information as it is outputted by the device. You can get this information from http://ems-esp.local/api?device=system&cmd=info*
|
||||
|
||||
**Additional context**
|
||||
*Add any other context about the problem here.*
|
||||
1
.github/SUPPORT.md
vendored
1
.github/SUPPORT.md
vendored
@@ -1 +0,0 @@
|
||||
# Support
|
||||
37
.github/workflows/check_code.yml
vendored
Normal file
37
.github/workflows/check_code.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Code Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
pull_request:
|
||||
branches: [ dev ]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['cpp']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- run: |
|
||||
make
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
101
.github/workflows/dev_release.yml
vendored
101
.github/workflows/dev_release.yml
vendored
@@ -1,101 +0,0 @@
|
||||
name: 'Build dev release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'src/emsesp_version.h'
|
||||
branches:
|
||||
- 'dev'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
pre-release:
|
||||
name: 'Build Dev Release'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Install python 3.13
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install Node.js 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable pnpm
|
||||
|
||||
- name: Get the EMS-ESP version
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'`
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
python -m pip install intelhex
|
||||
|
||||
- name: Build webUI
|
||||
run: |
|
||||
platformio run -e build_webUI
|
||||
|
||||
- name: Build modbus
|
||||
run: |
|
||||
platformio run -e build_modbus
|
||||
|
||||
- name: Build standalone
|
||||
run: |
|
||||
platformio run -e build_standalone
|
||||
|
||||
- name: Build all PIO target environments, from default_envs
|
||||
run: |
|
||||
platformio run
|
||||
|
||||
- name: Commit the generated files
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "chore: update generated files"
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
- name: Check for changes and commit
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Changes detected, committing..."
|
||||
git add .
|
||||
git commit -m "Auto-commit build artifacts and configuration updates
|
||||
|
||||
- Updated build configurations
|
||||
- Generated build artifacts
|
||||
- Version: ${{steps.build_info.outputs.VERSION}}"
|
||||
|
||||
echo "Pushing changes to repository..."
|
||||
git push origin dev
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: 'automatic_releases'
|
||||
uses: emsesp/action-automatic-releases@v1.0.0
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
||||
automatic_release_tag: 'latest'
|
||||
prerelease: true
|
||||
files: |
|
||||
CHANGELOG_LATEST.md
|
||||
./build/firmware/*.*
|
||||
26
.github/workflows/github-releases-to-discord.yml
vendored
26
.github/workflows/github-releases-to-discord.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: 'Publish releases to discord'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
github-releases-to-discord:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: GitHub Releases To Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1.13.1
|
||||
with:
|
||||
webhook_url: ${{ secrets.WEBHOOK_URL }}
|
||||
color: '2105893'
|
||||
username: 'Release Changelog'
|
||||
avatar_url: 'https://cdn.discordapp.com/icons/816637840644505620/0b14718532d855c452903851b4f0c9a2.png'
|
||||
content: '||@everyone||'
|
||||
footer_title: 'Changelog'
|
||||
footer_icon_url: 'https://cdn.discordapp.com/icons/816637840644505620/0b14718532d855c452903851b4f0c9a2.png'
|
||||
footer_timestamp: true
|
||||
32
.github/workflows/pr_check.yml
vendored
32
.github/workflows/pr_check.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: 'Pre-check on PR'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: dev
|
||||
paths:
|
||||
- 'src/**'
|
||||
|
||||
jobs:
|
||||
pre-release:
|
||||
name: 'Automatic pre-release build'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install python 3.13
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
pip install wheel
|
||||
pip install -U platformio
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
platformio run -e native-test -t exec
|
||||
61
.github/workflows/pre_release.yml
vendored
Normal file
61
.github/workflows/pre_release.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: "pre-release"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev"
|
||||
|
||||
jobs:
|
||||
pre-release:
|
||||
|
||||
name: "Automatic pre-release build"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get build variables
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
echo "::set-output name=version::$version"
|
||||
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
echo "::set-output name=platform::$platform"
|
||||
|
||||
- name: Compile locally
|
||||
run: make
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Install pio
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
platformio update
|
||||
|
||||
- name: Build web
|
||||
run: |
|
||||
cd interface
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
run: |
|
||||
platformio run -e ci
|
||||
|
||||
- name: Release
|
||||
id: "automatic_releases"
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
title: ${{steps.build_info.outputs.platform}} Development Build v${{steps.build_info.outputs.version}}
|
||||
automatic_release_tag: "latest"
|
||||
prerelease: true
|
||||
files: |
|
||||
CHANGELOG_LATEST.md
|
||||
./build/firmware/*.*
|
||||
|
||||
33
.github/workflows/sonar_check.yml
vendored
33
.github/workflows/sonar_check.yml
vendored
@@ -1,33 +0,0 @@
|
||||
# see https://github.com/marketplace/actions/sonarcloud-scan-for-c-and-c#usage
|
||||
name: Sonar Check
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- 'src/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
if: github.repository == 'emsesp/EMS-ESP32'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_WRAPPER_OUT_DIR: bw-output
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
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 }}
|
||||
|
||||
63
.github/workflows/stable_release.yml
vendored
63
.github/workflows/stable_release.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: 'Build stable release'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
tagged-release:
|
||||
name: 'Build Stable Release'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Install python 3.13
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install Node.js 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable pnpm
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
python -m pip install intelhex
|
||||
|
||||
- name: Build webUI
|
||||
run: |
|
||||
platformio run -e build_webUI
|
||||
|
||||
- name: Build modbus
|
||||
run: |
|
||||
platformio run -e build_modbus
|
||||
|
||||
- name: Build standalone
|
||||
run: |
|
||||
platformio run -e build_standalone
|
||||
|
||||
- name: Build all PIO target environments, from default_envs
|
||||
run: |
|
||||
platformio run
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: emsesp/action-automatic-releases@v1.0.0
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
prerelease: false
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
./build/firmware/*.*
|
||||
27
.github/workflows/stale_issues.yml
vendored
27
.github/workflows/stale_issues.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: "Mark or close stale issues and PRs"
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
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"
|
||||
56
.github/workflows/tagged_release.yml
vendored
Normal file
56
.github/workflows/tagged_release.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: "tagged-release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
tagged-release:
|
||||
|
||||
name: "Tagged Release"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get build variables
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
echo "::set-output name=version::$version"
|
||||
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
echo "::set-output name=platform::$platform"
|
||||
|
||||
- name: Compile locally
|
||||
run: make
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Install pio
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
platformio update
|
||||
|
||||
- name: Build web
|
||||
run: |
|
||||
cd interface
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
run: |
|
||||
platformio run -e ci
|
||||
|
||||
- name: Release
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
./build/firmware/*.*
|
||||
75
.github/workflows/test_release.yml
vendored
75
.github/workflows/test_release.yml
vendored
@@ -1,75 +0,0 @@
|
||||
name: 'Build test release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'test'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pre-release:
|
||||
name: 'Build Test Release'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
||||
- name: Install python 3.13
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install Node.js 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable pnpm
|
||||
|
||||
- name: Get the EMS-ESP version
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'`
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
python -m pip install intelhex
|
||||
|
||||
- name: Build webUI
|
||||
run: |
|
||||
platformio run -e build_webUI
|
||||
|
||||
- name: Build modbus
|
||||
run: |
|
||||
platformio run -e build_modbus
|
||||
|
||||
- name: Build standalone
|
||||
run: |
|
||||
platformio run -e build_standalone
|
||||
|
||||
- name: Build all PIO target environments, from default_envs
|
||||
run: |
|
||||
platformio run
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: 'automatic_releases'
|
||||
uses: emsesp/action-automatic-releases@v1.0.0
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
title: Test Build v${{steps.build_info.outputs.VERSION}}
|
||||
automatic_release_tag: 'test'
|
||||
prerelease: true
|
||||
files: |
|
||||
CHANGELOG_LATEST.md
|
||||
./build/firmware/*.*
|
||||
|
||||
68
.gitignore
vendored
68
.gitignore
vendored
@@ -1,76 +1,30 @@
|
||||
# vscode
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/extensions.json
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
.vscode
|
||||
.directory
|
||||
workspace.code-workspace
|
||||
|
||||
# c++ compiling
|
||||
# build
|
||||
build/
|
||||
.clang_complete
|
||||
.gcc-flags.json
|
||||
cppcheck.out.xml
|
||||
debug.log
|
||||
|
||||
# platformio
|
||||
.pio
|
||||
pio_local.ini
|
||||
/.VSCodeCounter
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
*Thumbs.db
|
||||
|
||||
# web specific
|
||||
build/
|
||||
dist/
|
||||
# project specfic
|
||||
/scripts/stackdmp.txt
|
||||
emsesp
|
||||
/data/www
|
||||
/lib/framework/WWWData.h
|
||||
/interface/build
|
||||
node_modules
|
||||
/interface/.eslintcache
|
||||
stats.html
|
||||
*.sln
|
||||
*.sw?
|
||||
.pnp.*
|
||||
analyse.html
|
||||
interface/vite.config.ts.timestamp*
|
||||
*.local
|
||||
src/ESP32React/WWWData.h
|
||||
|
||||
# i18n generated files
|
||||
interface/src/i18n/i18n-react.tsx
|
||||
interface/src/i18n/i18n-types.ts
|
||||
interface/src/i18n/i18n-util.ts
|
||||
interface/src/i18n/i18n-util.sync.ts
|
||||
interface/src/i18n/i18n-util.async.ts
|
||||
|
||||
# scripts
|
||||
test.sh
|
||||
scripts/run.sh
|
||||
scripts/__pycache__
|
||||
scripts/stackdmp.txt
|
||||
|
||||
# sonar
|
||||
.scannerwork/
|
||||
sonar/
|
||||
bw-output/
|
||||
|
||||
# standalone executable for testing
|
||||
emsesp
|
||||
interface/tsconfig.tsbuildinfo
|
||||
|
||||
# python virtual environment
|
||||
venv/
|
||||
|
||||
# cspell
|
||||
words-found-verbose.txt
|
||||
|
||||
# sonarlint
|
||||
compile_commands.json
|
||||
|
||||
# pioarduino + hybrid
|
||||
managed_components
|
||||
dependencies.lock
|
||||
CMakeLists.txt
|
||||
.dummy/*
|
||||
logs/*
|
||||
sdkconfig.*
|
||||
sdkconfig_tasmota_esp32
|
||||
pnpm-lock.yaml
|
||||
package.json
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"MD033": false,
|
||||
"MD013": false,
|
||||
"MD045": false,
|
||||
"MD041": false
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
*/node_modules/
|
||||
build/
|
||||
dist/*
|
||||
interface/src/i18n/*
|
||||
|
||||
.typesafe-i18n.json
|
||||
13
.prettierrc
13
.prettierrc
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 85,
|
||||
"bracketSpacing": true,
|
||||
"importOrder": ["^react", "^@mui/(.*)$", "^api*/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"importOrderGroupNamespaceSpecifiers": true
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"sonarCloudOrganization": "emsesp",
|
||||
"projectKey": "emsesp_EMS-ESP32"
|
||||
}
|
||||
101
.vscode/settings.json
vendored
101
.vscode/settings.json
vendored
@@ -1,101 +0,0 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"**/.yarn": true,
|
||||
"**/.pnp.*": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"eslint.validate": [
|
||||
"typescript"
|
||||
],
|
||||
"eslint.codeActionsOnSave.rules": null,
|
||||
"eslint.nodePath": "interface/.yarn/sdks",
|
||||
"eslint.workingDirectories": ["interface"],
|
||||
"prettier.prettierPath": "",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"files.associations": {
|
||||
"*.tsx": "typescriptreact",
|
||||
"*.tcc": "cpp",
|
||||
"optional": "cpp",
|
||||
"istream": "cpp",
|
||||
"ostream": "cpp",
|
||||
"ratio": "cpp",
|
||||
"system_error": "cpp",
|
||||
"array": "cpp",
|
||||
"functional": "cpp",
|
||||
"regex": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"string": "cpp",
|
||||
"string_view": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bitset": "cpp",
|
||||
"cctype": "cpp",
|
||||
"chrono": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"list": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"iterator": "cpp",
|
||||
"map": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"numeric": "cpp",
|
||||
"random": "cpp",
|
||||
"set": "cpp",
|
||||
"fstream": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"limits": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"typeinfo": "cpp"
|
||||
},
|
||||
"todo-tree.filtering.excludeGlobs": [
|
||||
"**/vendor/**",
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/bower_components/**",
|
||||
"**/build/**",
|
||||
"**/.vscode/**",
|
||||
"**/.github/**",
|
||||
"**/_output/**",
|
||||
"**/*.min.*",
|
||||
"**/*.map",
|
||||
"**/ArduinoJson/**"
|
||||
],
|
||||
"cSpell.enableFiletypes": [
|
||||
"ini",
|
||||
"makefile"
|
||||
],
|
||||
"typescript.preferences.preferTypeOnlyAutoImports": true,
|
||||
"sonarlint.pathToCompileCommands": "${workspaceFolder}/compile_commands.json",
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "emsesp",
|
||||
"projectKey": "emsesp_EMS-ESP32"
|
||||
}
|
||||
}
|
||||
753
CHANGELOG.md
753
CHANGELOG.md
@@ -5,712 +5,13 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.7.2] 22 March 2025
|
||||
|
||||
## 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**
|
||||
|
||||
- "ww" and "wwc" has been renamed to "dhw". It is nested JSON object in both the MQTT and API outputs. The old prefix has also been removed from MQTT topics ([#1634](https://github.com/emsesp/EMS-ESP32/issues/1634)). This will impact historical data in home automation systems like Home Assistant and IOBroker. To preserve the current value of dhw energy (was previously nrgww) refer to this issue [#1938](https://github.com/emsesp/EMS-ESP32/issues/1938).
|
||||
- dhw entities from the MM100/SM100 have been moved under a new Device called 'water'.
|
||||
- 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.
|
||||
|
||||
For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
|
||||
|
||||
## Added
|
||||
|
||||
- some more entities for dhw with SM100 module
|
||||
- thermostat second dhw circuit [#1634](https://github.com/emsesp/EMS-ESP32/issues/1634)
|
||||
- remote thermostat emulation for RC100H, RC200 and FB10 [#1287](https://github.com/emsesp/EMS-ESP32/discussions/1287), [#1602](https://github.com/emsesp/EMS-ESP32/discussions/1602), [#1551](https://github.com/emsesp/EMS-ESP32/discussions/1551)
|
||||
- heatpump dhw stop temperatures [#1624](https://github.com/emsesp/EMS-ESP32/issues/1624)
|
||||
- reset history [#1695](https://github.com/emsesp/EMS-ESP32/issues/1695)
|
||||
- heatpump entities `fan` and `shutdown` [#1690](https://github.com/emsesp/EMS-ESP32/discussions/1690)
|
||||
- mqtt HA-mode 3 for v3.6 compatible HA entities, set on update v3.6->v3.7
|
||||
- HP input states [#1723](https://github.com/emsesp/EMS-ESP32/discussions/1723)
|
||||
- holiday settings for rego 3000 [#1735](https://github.com/emsesp/EMS-ESP32/issues/1735)
|
||||
- Added scripts for OTA (scripts/upload.py and upload_cli.py) [#1738](https://github.com/emsesp/EMS-ESP32/issues/1738)
|
||||
- timeout for remote thermostat emulation [#1680](https://github.com/emsesp/EMS-ESP32/discussions/1680), [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||
- CR120 thermostat as own model() [#1779](https://github.com/emsesp/EMS-ESP32/discussions/1779)
|
||||
- modules - external linkable module library [#1778](https://github.com/emsesp/EMS-ESP32/issues/1778)
|
||||
- scheduler onChange and Conditions [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806)
|
||||
- make remote control timeout editable [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||
- added extra pump characteristics (mode and pressure for EMS+) by @SLTKA [#1802](https://github.com/emsesp/EMS-ESP32/pull/1802)
|
||||
- allow device name to be customized [#1174](https://github.com/emsesp/EMS-ESP32/issues/1174)
|
||||
- Modbus support by @mheyse [#1744](https://github.com/emsesp/EMS-ESP32/issues/1744)
|
||||
- System Message command [#1854](https://github.com/emsesp/EMS-ESP32/issues/1854)
|
||||
- scheduler can use web get/post for values and commands [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806)
|
||||
- RT800 remote emulation [#1867](https://github.com/emsesp/EMS-ESP32/issues/1867)
|
||||
- RC310 cooling parameters [#1857](https://github.com/emsesp/EMS-ESP32/issues/1857)
|
||||
- command `api/device/entities` [#1897](https://github.com/emsesp/EMS-ESP32/issues/1897)
|
||||
- switchprogmode [#1903](https://github.com/emsesp/EMS-ESP32/discussions/1903)
|
||||
- autodetect and download firmware upgrades via the WebUI
|
||||
- command 'show log' that lists out the current weblog buffer, showing last messages.
|
||||
- default web log buffer to 25 lines for ESP32s with no PSRAM
|
||||
- try and determine correct board profile if none is set during boot
|
||||
- auto Scroll in WebLog UI - reduced delay so incoming logs are faster
|
||||
- uploading custom support info, shown to Guest users in Help page [#2054](https://github.com/emsesp/EMS-ESP32/issues/2054)
|
||||
- feature: Dashboard showing all data (favorites, sensors, custom) [#1958](https://github.com/emsesp/EMS-ESP32/issues/1958)
|
||||
- entity for low-temperature boilers pump start temp (pumpOnTemp) #2088 [#2088](https://github.com/emsesp/EMS-ESP32/issues/2088)
|
||||
- internal ESP32 temperature sensor on the S3 [#2077](https://github.com/emsesp/EMS-ESP32/issues/2077)
|
||||
- MQTT status topic (used in connect and last will) set to Retain [#2086](https://github.com/emsesp/EMS-ESP32/discussions/2086)
|
||||
- Czech language [2096](https://github.com/emsesp/EMS-ESP32/issues/2096)
|
||||
- Developer Mode and send EMS Read Commands from WebUI [#2116](https://github.com/emsesp/EMS-ESP32/issues/2116)
|
||||
- Scheduler functions [#2115](https://github.com/emsesp/EMS-ESP32/issues/2115)
|
||||
- Set device custom name from telegram 0x01 [#2073](https://github.com/emsesp/EMS-ESP32/issues/2073)
|
||||
|
||||
## Fixed
|
||||
|
||||
- remote thermostat emulation for RC200 on Rego2000/3000 thermostats [#1691](https://github.com/emsesp/EMS-ESP32/discussions/1691)
|
||||
- log shows data for F7/F9 requests
|
||||
- Detection of LittleFS for factory setting wasn't working
|
||||
- Check for bad GPIOs with Ethernet before the ethernet is initialized
|
||||
- Show values with factor 50 on webUI [#2064](https://github.com/emsesp/EMS-ESP32/issues/2064)
|
||||
- Rendering of values between -1 and 0
|
||||
- Value for 32bit times not-set [#2109](https://github.com/emsesp/EMS-ESP32/issues/2109)
|
||||
|
||||
## Changed
|
||||
|
||||
- use flag for BC400 compatible thermostats, manage different mode settings
|
||||
- use factory partition for 16M flash
|
||||
- store digital out states to nvs
|
||||
- Refresh UI - moving settings to one location [#1665](https://github.com/emsesp/EMS-ESP32/issues/1665)
|
||||
- rename DeviceValueTypes, add UINT32 for custom entities
|
||||
- dynamic register dhw circuits for thermostat
|
||||
- removed OTA feature [#1738](https://github.com/emsesp/EMS-ESP32/issues/1738)
|
||||
- added shower min duration [#1801](https://github.com/emsesp/EMS-ESP32/issues/1801)
|
||||
- Include TXT file along with the generated CSV for Device Data export/download
|
||||
- thermostat/remotetemp as command [#1835](https://github.com/emsesp/EMS-ESP32/discussions/1835)
|
||||
- temperaturesensor id notation with underscore [#1794](https://github.com/emsesp/EMS-ESP32/discussions/1794)
|
||||
- Change key-names in JSON to be compliant and consistent [#1860](https://github.com/emsesp/EMS-ESP32/issues/1860)
|
||||
- Updates to webUI [#1920](https://github.com/emsesp/EMS-ESP32/issues/1920)
|
||||
- Correct firmware naming #1933 [#1933](https://github.com/emsesp/EMS-ESP32/issues/1933)
|
||||
- Don't start Serial console if not connected to a Serial port. Will initiate manually after a CTRL-C/CTRL-S
|
||||
- WebLog UI matches color schema of the terminal console correctly
|
||||
- Updated Web libraries, ArduinoJson
|
||||
- Help page doesn't show detailed tech info if the user is not 'admin' role [#2054](https://github.com/emsesp/EMS-ESP32/issues/2054)
|
||||
- removed system command `allvalues` and moved to an action called `export`
|
||||
- Show ems-esp internal devices in device list of system/info
|
||||
- Scheduler and mqtt run async on systems with psram
|
||||
- Show IPv6 address type (local/global/ula) in log
|
||||
|
||||
## [3.6.5] March 23 2024
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
- The Wifi Tx Power setting in Network Settings will be reset to Auto
|
||||
|
||||
## Added
|
||||
|
||||
- thermostat boost mode and boost time [#1446](https://github.com/emsesp/EMS-ESP32/issues/1446)
|
||||
- heatpump energy meters [#1463](https://github.com/emsesp/EMS-ESP32/issues/1463)
|
||||
- heatpump max power [#1475](https://github.com/emsesp/EMS-ESP32/issues/1475)
|
||||
- checkbox for MQTT-TLS enable [#1474](https://github.com/emsesp/EMS-ESP32/issues/1474)
|
||||
- added SK (Slovak) language. Thanks @misa1515
|
||||
- CPU info [#1497](https://github.com/emsesp/EMS-ESP32/pull/1497)
|
||||
- Show network hostname in Web UI under Network Status
|
||||
- Improved HA Discovery so each section (EMS device, Scheduler, Analog, Temperature, Custom, Shower) have their own section
|
||||
- boiler Bosch C1200W, id 12, [#1536](https://github.com/emsesp/EMS-ESP32/issues/1536)
|
||||
- mixer MM100 telegram 0x2CC [#1554](https://github.com/emsesp/EMS-ESP32/issues/1554)
|
||||
- boiler hpSetDiffPressure [#1563](https://github.com/emsesp/EMS-ESP32/issues/1563)
|
||||
- custom variables [#1423](https://github.com/emsesp/EMS-ESP32/issues/1423)
|
||||
- weather compensation [#1642](https://github.com/emsesp/EMS-ESP32/issues/1642)
|
||||
- env and partitions for DevKitC-1-N32R8 [#1635](https://github.com/emsesp/EMS-ESP32/discussions/1635)
|
||||
- command `restart partitionname` and button long press to start with other partition [#1657](https://github.com/emsesp/EMS-ESP32/issues/1657)
|
||||
- command `set service <mqtt|ota|ntp|ap> <enable|disable>` [#1663](https://github.com/emsesp/EMS-ESP32/issues/1663)
|
||||
|
||||
## Fixed
|
||||
|
||||
- exhaust temperature for some boilers
|
||||
- add back boil2hyst [#1477](https://github.com/emsesp/EMS-ESP32/issues/1477)
|
||||
- subscribed MQTT topics not detecting changes by EMS-ESP [#1494](https://github.com/emsesp/EMS-ESP32/issues/1494)
|
||||
- changed HA name and grouping to be consistent [#1528](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
||||
- MQTT autodiscovery in Domoticz not working [#1360](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
||||
- dhw comfort for new ems+, [#1495](https://github.com/emsesp/EMS-ESP32/issues/1495)
|
||||
- added writeable icon to Web's Custom Entity page for each entity shown in the table
|
||||
- Wifi Tx Power not adjusted [#1614](https://github.com/emsesp/EMS-ESP32/issues/1614)
|
||||
- MQTT discovery of custom entity doesn't consider type of data [#1587](https://github.com/emsesp/EMS-ESP32/issues/1587)
|
||||
- WiFi TxPower wasn't correctly used. Added an 'Auto' setting, which is the default.
|
||||
- dns w/wo IPv6 [#1644](https://github.com/emsesp/EMS-ESP32/issues/1644)
|
||||
|
||||
## Changed
|
||||
|
||||
- HA don't set entity_category to Diagnostic/Configuration for EMS entities [#1459](https://github.com/emsesp/EMS-ESP32/discussions/1459)
|
||||
- upgraded ArduinoJson to 7.0.0 #1538 and then 7.0.2
|
||||
- small changes to the API for analog and temperature sensors
|
||||
- Length of mqtt Broker adress [#1619](https://github.com/emsesp/EMS-ESP32/issues/1619)
|
||||
- C++ optimizations - see <https://github.com/emsesp/EMS-ESP32/pull/1615>
|
||||
- Send MQTT heartbeat immediately after connection [#1628](https://github.com/emsesp/EMS-ESP32/issues/1628)
|
||||
- 16MB partitions with second nvs, larger FS, Coredump, optional factory partition
|
||||
- stop fetching empty telegrams after 5 min
|
||||
|
||||
## [3.6.4] November 24 2023
|
||||
|
||||
## **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.
|
||||
|
||||
## Added
|
||||
|
||||
- humidity for ventilation devices
|
||||
- telegrams for RC100H, hc2, etc. (seen on discord, not tested)
|
||||
- names for BC400, GB192i.2, read temperatures for low loss header and heatblock [#1317](https://github.com/emsesp/EMS-ESP32/discussions/1317)
|
||||
- option for `forceheatingoff` [#1262](https://github.com/emsesp/EMS-ESP32/issues/1262)
|
||||
- remote thermostat emulation RC100H for RC3xx [#1278](https://github.com/emsesp/EMS-ESP32/discussions/1278)
|
||||
- shower_data MQTT payload contains the timestamp [#1329](https://github.com/emsesp/EMS-ESP32/issues/1329)
|
||||
- HA discovery for writeable text entities [#1337](https://github.com/emsesp/EMS-ESP32/pull/1377)
|
||||
- autodetect board_profile, store in nvs, add telnet command option, add E32V2
|
||||
- heat pump high res energy counters [#1348, #1349. #1350](https://github.com/emsesp/EMS-ESP32/issues/1348)
|
||||
- optional bssid in network settings
|
||||
- extension module EM100 [#1315](https://github.com/emsesp/EMS-ESP32/discussions/1315)
|
||||
- digital_out with new options for polarity and startup state
|
||||
- added 'system allvalues' command that dumps all the EMS device values, plus sensors and any custom entities
|
||||
|
||||
## Fixed
|
||||
|
||||
- remove command `remoteseltemp`, thermostat accept it only from remote thermostat
|
||||
- shower_data MQTT payload contains the timestamp [#1329](https://github.com/emsesp/EMS-ESP32/issues/1329)
|
||||
- fixed helper text in Web Device Entity dialog box for numerical ranges
|
||||
- MQTT base with paths not working in HA [#1393](https://github.com/emsesp/EMS-ESP32/issues/1393)
|
||||
- set/read thermostat mode for RC100-RC300, [#1440](https://github.com/emsesp/EMS-ESP32/issues/1440) [#1442](https://github.com/emsesp/EMS-ESP32/issues/1442)
|
||||
- some setting commands for ems-boiler have used wrong ems+ telegram in 3.6.3
|
||||
|
||||
## Changed
|
||||
|
||||
- update to platform 6.4.0, arduino 2.0.14 / idf 4.4.6
|
||||
- small changes for arduino 3.0.0 / idf 5.1 compatibility (not backward compatible to platform 6.3.2 and before)
|
||||
- AP start after 10 sec, stay until station/eth connected
|
||||
- tested wifi-all-channel-scan (3.6.3-dev4 a-e), removed again because of connect issues
|
||||
- mqtt disconnect stops queue
|
||||
|
||||
## [3.6.2] October 1 2023
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
## Added
|
||||
|
||||
- Power entities
|
||||
- Optional input of BSSID for AP connection
|
||||
- Return empty json if no entries in scheduler/custom/analogsensor/temperaturesensor
|
||||
|
||||
## Fixed
|
||||
|
||||
- Wifi full scan to get strongest AP
|
||||
- Add missing dhw tags
|
||||
- Sending a dash/- to the Reset command doesn't return an error [#1308](https://github.com/emsesp/EMS-ESP32/discussions/1308)
|
||||
|
||||
## Changed
|
||||
|
||||
- MQTT queue max 300 messages, check heap and maxAlloc
|
||||
- API call commands are logged as WARN in the log
|
||||
- Reset Command renamed to 'reset' in lowercase in EN
|
||||
|
||||
## [3.6.1] September 9 2023
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
- `shower_data` MQTT topic shows duration is seconds (was previously a full english sentence)
|
||||
|
||||
## Added
|
||||
|
||||
- Show WiFi rssi in Network Status Page, show quality as color
|
||||
|
||||
## Fixed
|
||||
|
||||
- Issue in espMqttClient causing a memory leak when MQTT broker is disconnected due to network unavailability [#1264](https://github.com/emsesp/EMS-ESP32/issues/1264)
|
||||
- Using MQTT enum values correctly formatted in MQTT Discovery [#1280](https://github.com/emsesp/EMS-ESP32/issues/1280)
|
||||
|
||||
## Changed
|
||||
|
||||
- MQTT free mem check set to 60 kb
|
||||
- Small cosmetic changes to Searching in Customization web page
|
||||
- Updated to espressif32@6.4.0
|
||||
|
||||
# [3.6.0] August 13 2023
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please read carefully before applying the update.
|
||||
|
||||
- The sensors have been renamed. `dallassensor` is now `temperaturesensor` in the MQTT topic and named `ts` in the Customizations file. Likewise `analogs` is now `analogsensor` in MQTT and called `as` in the Customizations file. If you have previous customizations you will need to manually update by downloading, changing the JSON file and uploading. It's also recommended cleaning up any old MQTT topics from your broker using an application like MQTTExplorer.
|
||||
|
||||
## Added
|
||||
|
||||
- Workaround for better Domoticz MQTT integration? [#904](https://github.com/emsesp/EMS-ESP32/issues/904)
|
||||
- Show MAC address without connecting to network enhancement [#933](https://github.com/emsesp/EMS-ESP32/issues/933)
|
||||
- Warn user in WebUI of unsaved changes [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
||||
- Detect old Tado thermostat, device-id 0x19, no entities
|
||||
- Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||
- Added Scheduler [#701](https://github.com/emsesp/EMS-ESP32/issues/701)
|
||||
- Added Custom Entities read/write from EMS bus
|
||||
- Build S3 binary with github actions
|
||||
- Greenstar HIU [#1158](https://github.com/emsesp/EMS-ESP32/issues/1158)
|
||||
- AM200 code 10 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
|
||||
- Ventilation device (Logavent HRV176) [#1172](https://github.com/emsesp/EMS-ESP32/issues/1172)
|
||||
- Turn ETH off on wifi connect [#1167](https://github.com/emsesp/EMS-ESP32/issues/1167)
|
||||
- Support for multiple EMS-ESPs with HA [#1196](https://github.com/emsesp/EMS-ESP32/issues/1196)
|
||||
- Italian translation [#1199](https://github.com/emsesp/EMS-ESP32/issues/1199)
|
||||
- Turkish language support [#1076](https://github.com/emsesp/EMS-ESP32/issues/1076)
|
||||
- Buderus GB182 - HC1 mode change not work bug [#1193](https://github.com/emsesp/EMS-ESP32/issues/1193)
|
||||
- Minimal flow temperature enhancement [#1192](https://github.com/emsesp/EMS-ESP32/issues/1192)
|
||||
- Roomtemperature Switching Difference enhancement [#1191](https://github.com/emsesp/EMS-ESP32/issues/1191)
|
||||
- Dew Point Temperature Difference enhancement [#1190](https://github.com/emsesp/EMS-ESP32/issues/1190)
|
||||
- Control of heating circuit mode enhancement [#1187](https://github.com/emsesp/EMS-ESP32/issues/1187)
|
||||
- Warn user in WebUI of unsaved changes enhancement [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
||||
- Create safebuild app to fit into factory partition to give ESP32 more flash memory enhancement [#608](https://github.com/emsesp/EMS-ESP32/issues/608)
|
||||
- Support ESP32 S2, C3 mini and S3 [#605](https://github.com/emsesp/EMS-ESP32/issues/605)
|
||||
- Support Buderus AM200 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
|
||||
- Custom telegram handler [#1155](https://github.com/emsesp/EMS-ESP32/issues/1155)
|
||||
- Added support for TLS in MQTT (ESP32-S3 only) [#1178](https://github.com/emsesp/EMS-ESP32/issues/1178)
|
||||
- Boardprofile BBQKees Gateway S3
|
||||
- Custom entity type RAW [#1212](https://github.com/emsesp/EMS-ESP32/discussions/1212)
|
||||
- API command response [#1212](https://github.com/emsesp/EMS-ESP32/discussions/1212)
|
||||
|
||||
## Fixed
|
||||
|
||||
- HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035)
|
||||
- Enum order of RC3x nofrost mode
|
||||
- Heartbeat interval
|
||||
- Exhaust temperature always zero on GB125/MC110/RC310 bug [#1147](https://github.com/emsesp/EMS-ESP32/issues/1147)
|
||||
- thermostat modetype is not changing when mode changes (e.g. to night) bugSomething isn't working [#1098](https://github.com/emsesp/EMS-ESP32/issues/1098)
|
||||
- NTP: cant apply changed timezone [#1182](https://github.com/emsesp/EMS-ESP32/issues/1182)
|
||||
- Missing Status of VS1 for Buderus SM200 enhancement [#1034](https://github.com/emsesp/EMS-ESP32/issues/1034)
|
||||
- Allowed gpios for S3
|
||||
|
||||
## Changed
|
||||
|
||||
- Optional upgrade to platform-espressif32 6.3.0 (after 5.3.0) [#862](https://github.com/emsesp/EMS-ESP32/issues/862)
|
||||
- Use byte 3 for detection RC30 active heatingcircuit [#786](https://github.com/emsesp/EMS-ESP32/issues/786)
|
||||
- Write repeated selflowtemp if tx-queue is empty without verify [#954](https://github.com/emsesp/EMS-ESP32/issues/954)
|
||||
- HA discovery recreate after disconnect by device [#1067](https://github.com/emsesp/EMS-ESP32/issues/1067)
|
||||
- File upload: check flash size (overflow) instead of filesize
|
||||
- Improved HA Discovery so previous configs no longer need to be removed when starting [#1077](https://github.com/emsesp/EMS-ESP32/pull/1077) (thanks @pswid)
|
||||
- Enlarge UART-Stack to 2,5k
|
||||
- Retry timeout for Mqtt-QOS1/2 10seconds
|
||||
- Optimize WebUI rendering when using Dialog Boxes [#1116](https://github.com/emsesp/EMS-ESP32/issues/1116)
|
||||
- Optimize Web libraries to reduce bundle size (3.6.x) [#1112](https://github.com/emsesp/EMS-ESP32/issues/1112)
|
||||
- Use [espMqttClient](https://github.com/bertmelis/espMqttClient) with integrated queue [#1178](https://github.com/emsesp/EMS-ESP32/issues/1178)
|
||||
- Move Sensors from Web dashboard to it's own tab enhancement [#1170](https://github.com/emsesp/EMS-ESP32/issues/1170)
|
||||
- Optimize WebUI dashboard data [#1169](https://github.com/emsesp/EMS-ESP32/issues/1169)
|
||||
- Replace React core library with Preact to save on memory footprint
|
||||
- Response to `system/send` raw reads gives combined data for telegrams with more parts
|
||||
|
||||
# [3.5.0] February 6 2023
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
|
||||
|
||||
## Added
|
||||
|
||||
- Translations in Web UI and all device entity names (DE, NL, SV, PL, NO, FR) [#22](https://github.com/emsesp/EMS-ESP32/issues/22)
|
||||
- Add support for Lolin C3 mini [#620](https://github.com/emsesp/EMS-ESP32/pull/620)
|
||||
- Add support for ESP32-S2 [#667](https://github.com/emsesp/EMS-ESP32/pull/667)
|
||||
- Add devices: Greenstar 30Ri boiler, Junkers FW500 thermostat, Buderus BC30 controller
|
||||
- Add program memory info
|
||||
- Add mqtt queue and connection infos
|
||||
- Adapt min/max if ems-value is not in this range
|
||||
- Add heat pump settings for inputs and limits [#600](https://github.com/emsesp/EMS-ESP32/issues/600)
|
||||
- Add hybrid heatpump [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||
- Add translated tags
|
||||
- Add min/max to customization table [#686](https://github.com/emsesp/EMS-ESP32/issues/686)
|
||||
- Add MD5 check [#637](https://github.com/emsesp/EMS-ESP32/issues/637)
|
||||
- Add more bus-ids [#673](https://github.com/emsesp/EMS-ESP32/issues/673)
|
||||
- Use HA connectivity device class for Status, added boot time [#751](https://github.com/emsesp/EMS-ESP32/issues/751)
|
||||
- Add commands for analog sensors outputs
|
||||
- Support for multiple EMS-ESPs with MQTT and HA [[#759](https://github.com/emsesp/EMS-ESP32/issues/759)]
|
||||
- Settings for heatpump silent mode and additional heater [[#802](https://github.com/emsesp/EMS-ESP32/issues/802)] [[#803](https://github.com/emsesp/EMS-ESP32/issues/803)]
|
||||
- Zone module MZ100 [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
|
||||
- Default MQTT hostname is blank [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
|
||||
- wwCurFlow for ems+ devices [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
|
||||
- Add Rego 3000, TR120RF thermostats [#917](https://github.com/emsesp/EMS-ESP32/issues/917)
|
||||
- Add config for ESP32-S3
|
||||
- Add heatpump silent mode and other entities [#896](https://github.com/emsesp/EMS-ESP32/issues/896)
|
||||
- Allow reboot to other partition (factory or asymetric OTA)
|
||||
- Blacklist entities to remove from memory [#891](https://github.com/emsesp/EMS-ESP32/issues/891)
|
||||
- Add boiler pump operating mode [#944](https://github.com/emsesp/EMS-ESP32/issues/944)
|
||||
|
||||
## Fixed
|
||||
|
||||
- Factory Reset not working [#628](https://github.com/emsesp/EMS-ESP32/issues/628)
|
||||
- Valid 4 byte values [#820](https://github.com/emsesp/EMS-ESP32/issues/820)
|
||||
- Commands for multiple thermostats [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
|
||||
- API queries for multiple devices [#865](https://github.com/emsesp/EMS-ESP32/issues/865)
|
||||
- Console crash when using call with command `hcx` only. [#841](https://github.com/emsesp/EMS-ESP32/issues/841)
|
||||
- `heatingPump2Mod` was wrong, changed to absBurnPow [[#908](https://github.com/emsesp/EMS-ESP32/issues/908)
|
||||
- Rounding of web input values
|
||||
- Analog sensor with single gpio number [#915](https://github.com/emsesp/EMS-ESP32/issues/915)
|
||||
- HA dallas and analog configs: remove/rebuild on change [#888](https://github.com/emsesp/EMS-ESP32/issues/888)
|
||||
- Modes and set seltemp for RC30 and RC20 [#932](https://github.com/emsesp/EMS-ESP32/issues/932)
|
||||
|
||||
## Changed
|
||||
|
||||
- Discovery in HomeAssistant don't work with custom base topic. [#596](https://github.com/emsesp/EMS-ESP32/issues/596) Base topic containing `/` are changed to `_`
|
||||
- RF room temperature sensor are shown as thermostat
|
||||
- Render mqtt float json values with trailing zero
|
||||
- Removed flash strings, to increase available heap memory
|
||||
- Reload page after restart button is pressed
|
||||
- Analog/dallas values command as list like ems-devices
|
||||
- Analog/dallas HA-entities based on id
|
||||
- MQTT Base is a mandatory field. Removed MQTT topic length from settings
|
||||
- HA duration class for time entities [[#822](https://github.com/emsesp/EMS-ESP32/issues/822)
|
||||
- AM200 alternative heatsource as class heatsource [[#857](https://github.com/emsesp/EMS-ESP32/issues/857)
|
||||
|
||||
# [3.4.2] September 18 2022
|
||||
|
||||
## Added
|
||||
|
||||
- RC310 additions [#520](https://github.com/emsesp/EMS-ESP32/pull/520)
|
||||
- damping
|
||||
- wwprio for RC310 heating circuits
|
||||
- switchonoptimization for RC310 heating circuits
|
||||
- enum_controlmode for RC310 (new enum list)
|
||||
- nofrostmode, reducemode, reducetemp & noreducetemp for RC310
|
||||
- emergencyops and emergencytemp, wwmaxtemp, wwflowtempoffset and wwcomfort1 for RC310
|
||||
- HM200 hybrid module [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||
- AM200 alternative heatsource module [#573](https://github.com/emsesp/EMS-ESP32/issues/573)
|
||||
- EM10 error module as gateway [#575](https://github.com/emsesp/EMS-ESP32/issues/575)
|
||||
|
||||
## Fixed
|
||||
|
||||
- fix Table resizing in WebUI [#519](https://github.com/emsesp/EMS-ESP32/issues/519)
|
||||
- allow larger customization files [#570](https://github.com/emsesp/EMS-ESP32/issues/570)
|
||||
- losing entity wwcomfort [#581](https://github.com/emsesp/EMS-ESP32/issues/581)
|
||||
|
||||
## Changed
|
||||
|
||||
- Shorten "friendly names" in Home Assistant [#555](https://github.com/emsesp/EMS-ESP32/issues/555)
|
||||
- platformio 2.3.0 (IDF 4, Arduino 2)
|
||||
- remove master-thermostat, support multiple thermostats
|
||||
- merge up- and download in webui [#577](https://github.com/emsesp/EMS-ESP32/issues/577)
|
||||
|
||||
# [3.4.1] May 29 2022
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fix memory leak in api [#524](https://github.com/emsesp/EMS-ESP32/issues/524)
|
||||
|
||||
## Changed
|
||||
|
||||
- Controller data in web-ui only for IVT [#522](https://github.com/emsesp/EMS-ESP32/issues/522)
|
||||
- Rename hidden `climate` to a more explaining name [#523](https://github.com/emsesp/EMS-ESP32/issues/523)
|
||||
- Minor changes to the Customizations web page [#527](https://github.com/emsesp/EMS-ESP32/pull/527)
|
||||
|
||||
# [3.4.0] May 23 2022
|
||||
|
||||
## Added
|
||||
|
||||
- WebUI optimizations, updated look&feel and better performance [#124](https://github.com/emsesp/EMS-ESP32/issues/124)
|
||||
- Auto refresh of WebUI after successful firmware upload [#178](https://github.com/emsesp/EMS-ESP32/issues/178)
|
||||
- New Customization Service in WebUI. First feature is the ability to enable/disabled Entities (device values) from EMS devices [#206](https://github.com/emsesp/EMS-ESP32/issues/206)
|
||||
- Option to disable Telnet Console [#209](https://github.com/emsesp/EMS-ESP32/issues/209)
|
||||
- Added Hide SSID, Max Clients and Preferred Channel to Access Point
|
||||
- Merged in MichaelDvP's changes like Fahrenheit conversion, publish single (for IOBroker) and a few other critical optimizations
|
||||
- Enabled bi-directional read/write with Home Assistant, so values can be changed automatically from the UI without scripting [#265](https://github.com/emsesp/EMS-ESP32/issues/265)
|
||||
- Added GC7000F Boiler [#270](https://github.com/emsesp/EMS-ESP32/issues/270)
|
||||
- Revised LED flash sequence on boot up to show system health (1 flash=no ems, 2 flashes=no wifi) [#224](https://github.com/emsesp/EMS-ESP32/issues/224)
|
||||
- Analog Sensor support [#271](https://github.com/emsesp/EMS-ESP32/issues/271)
|
||||
- Solar cylinder priority [#247](https://github.com/emsesp/EMS-ESP32/issues/247)
|
||||
- Read only mode in Settings, where EMS Tx/Write commands are blocked [#286](https://github.com/emsesp/EMS-ESP32/issues/286)
|
||||
- Added 8700i Boiler device
|
||||
- Added Cascade CM10 Controller device
|
||||
- Add Olimex ESP32-POE-ISO to board profiles plus settings to customize Ethernet modules [#301](https://github.com/emsesp/EMS-ESP32/issues/301)
|
||||
- Help text for string commands in WebUI [#320](https://github.com/emsesp/EMS-ESP32/issues/320)
|
||||
- Germany translations (at compile time)
|
||||
- #entities added to system/info` endpoint [#322](https://github.com/emsesp/EMS-ESP32/issues/322)
|
||||
- analog outputs digital/pwm/dac
|
||||
- remove MQTT retained configs if discovery is disabled
|
||||
- timeout 10 min for MQTT-QoS wait
|
||||
- Moduline 300 auto-temperatures T1-T4, RC300 romminfluencefactor
|
||||
- RC35 parameters [#392](https://github.com/emsesp/EMS-ESP32/issues/392), [#398](https://github.com/emsesp/EMS-ESP32/issues/398)
|
||||
- sync time with thermostat [#386](https://github.com/emsesp/EMS-ESP32/issues/386), [#408](https://github.com/emsesp/EMS-ESP32/issues/408)
|
||||
- set mode has immediate effect [#395](https://github.com/emsesp/EMS-ESP32/issues/395)
|
||||
- min/max in web value setting
|
||||
- Extend customization to select if an entity is to be shown in the WebUI or forced as read-only [#317](https://github.com/emsesp/EMS-ESP32/issues/317)
|
||||
- Added Moduline 400 installation parameters [PR #449 by @kwertie01](https://github.com/emsesp/EMS-ESP32/pull/449)
|
||||
- Read time from IVT-controller [#439](https://github.com/emsesp/EMS-ESP32/issues/439)
|
||||
- Hybrid Heatpump product-id 168 [#459](https://github.com/emsesp/EMS-ESP32/issues/459), thermostat settings
|
||||
- Junkers ISM2 and IPM in warm water mode [#437](https://github.com/emsesp/EMS-ESP32/issues/437)
|
||||
- Added Shower Alert trigger time and cold shot time [#436](https://github.com/emsesp/EMS-ESP32/issues/436)
|
||||
- Improved Table layout in Web UI (searching, filtering, sorting, exporting to CSV)
|
||||
- API fetch individual attributes from an entity [#462](https://github.com/emsesp/EMS-ESP32/issues/462)
|
||||
- Option to disable mDNS
|
||||
- Option for rendering booleans on dashboard [#456](https://github.com/emsesp/EMS-ESP32/issues/456)
|
||||
- Upload customization settings from a file [#256](https://github.com/emsesp/EMS-ESP32/issues/256)
|
||||
|
||||
## Fixed
|
||||
|
||||
- lastcode broke MQTT JSON structure [#228](https://github.com/emsesp/EMS-ESP32/issues/228)
|
||||
- fixed issue with overlapping while reading sequence of EMS1.0 telegrams
|
||||
- fixed redundant telegram readings (because of offset overflow)
|
||||
- added missing RC30/Moduline 400 [#243](https://github.com/emsesp/EMS-ESP32/issues/243)
|
||||
- Correct modes for RC25 [#106](https://github.com/emsesp/EMS-ESP32/issues/106)
|
||||
- Clean up old HA config's in MQTT before publishing data. This will prevent HA giving the 'dict' warnings [#229](https://github.com/emsesp/EMS-ESP32/issues/229)
|
||||
- RC25 temperature setting [#272](https://github.com/emsesp/EMS-ESP32/issues/272)
|
||||
- Buderus RC25 - "hc1 mode type" incorrect value [#273](https://github.com/emsesp/EMS-ESP32/issues/273)
|
||||
- Increased number of Mixers and Heating Circuits [#294](https://github.com/emsesp/EMS-ESP32/issues/294)
|
||||
- Check receive status before removing a telegram fetch [#268](https://github.com/emsesp/EMS-ESP32/issues/268), [#282](https://github.com/emsesp/EMS-ESP32/issues/282)
|
||||
- Fix uploading firmware on OSX [#345](https://github.com/emsesp/EMS-ESP32/issues/345)
|
||||
- Non-nested MQTT would corrupt the json [#354](https://github.com/emsesp/EMS-ESP32/issues/354)
|
||||
- Burner selected max power can have a value higher than 100% [#314](https://github.com/emsesp/EMS-ESP32/issues/314)
|
||||
- some missing fahrenheit calculations
|
||||
- limited number of exclusions [#339](https://github.com/emsesp/EMS-ESP32/issues/339)
|
||||
- MQTT sometimes would not reconnect after a WiFi outage
|
||||
|
||||
## Changed
|
||||
|
||||
- Use flash system to show system health (1 flash=no ems, 2 flashes=no wifi) [#224](https://github.com/emsesp/EMS-ESP32/issues/224)
|
||||
- Renamed Dallas Sensor to Temperature Sensor in UI
|
||||
- Dallas Format removed. Use the name to give each sensor an alias
|
||||
- No longer MQTT subscribes to topic `/thermostat_hc<n>` as it supports a path similar to the API endpoint construct
|
||||
- Show Sensors quality in WebUI
|
||||
- Controller not shown in WebUI dashboard
|
||||
- renamed "Home Assistant Integration" to "MQTT Discovery" in MQTT Settings [#290](https://github.com/emsesp/EMS-ESP32/issues/290)
|
||||
- Show ems tx reads and writes separately
|
||||
- Show ems device handlers separated for received, fetched and pending handlers.
|
||||
- Wired renamed to Ethernet
|
||||
- removed system/pin command, new commands in analogsensors
|
||||
- system/info device-info split to name/version/brand
|
||||
- exclude list uses short-names, possible flags for web/api/mqtt excludes, readonly and favorite (selection not yet implemented)
|
||||
- thermostat clock formate date-time: dd.mm.yyyy hh:mm
|
||||
- RC300 summermode as other thermostats `winter/summer` instead of `off/on`
|
||||
|
||||
## **BREAKING CHANGES:**
|
||||
|
||||
- Settings:
|
||||
- order of Boolean Format has changed in Application Settings - check your settings
|
||||
- Dallas Format setting removed. Now customize name of each Dallas sensor via the UI
|
||||
- MQTT/API
|
||||
- Boiler `wwheat` renamed to `ww3wayon` [#211](https://github.com/emsesp/EMS-ESP32/issues/211)
|
||||
- Boiler `ww` tag renamed to `dhw`. Any custom Home Assistant lovelace dashboards will need updating.
|
||||
- Renamed description of `wwtapactivated` to "turn on/off DHW". Otherwise would have looked like "boiler_dhw_turn_on_off_dhw" in HA.
|
||||
- `/api/system/info` endpoint has updated keys. Now lowercase, no underscores and not capitalized. Replace "handlers" with "handlers received", "handlers fetched" and "handlers pending".
|
||||
|
||||
# [3.3.1] January 20 2022
|
||||
|
||||
- lastcode broke MQTT JSON structure [#228](https://github.com/emsesp/EMS-ESP32/issues/228)
|
||||
- overlapping while reading sequence of EMS1.0 telegrams
|
||||
- redundant telegram readings (because of offset overflow)
|
||||
- added missing RC30/Moduline400 [#243](https://github.com/emsesp/EMS-ESP32/issues/243)
|
||||
- check received status before toggling fetch on empty telegram [#268][#282]
|
||||
|
||||
# [3.3.0] November 28 2021
|
||||
|
||||
## Added
|
||||
|
||||
- Add system commands for syslog level and watch [#98](https://github.com/emsesp/EMS-ESP32/issues/98)
|
||||
- Added pool data to telegrams 0x494 & 0x495 [#102](https://github.com/emsesp/EMS-ESP32/issues/102)
|
||||
- Add RC300 second summermode telegram [#108](https://github.com/emsesp/EMS-ESP32/issues/108)
|
||||
- Add support for the RC25 thermostat [#106](https://github.com/emsesp/EMS-ESP32/issues/106)
|
||||
- Add new command 'entities' for a device, e.g. <http://ems-esp/api/boiler/entities> to show the shortname, description and HA Entity name (if HA enabled) [#116](https://github.com/emsesp/EMS-ESP32/issues/116)
|
||||
- Support for Junkers program and remote (fb10/fb110) temperature
|
||||
- Home Assistant `state_class` attribute for Wh, kWh, W and KW [#129](https://github.com/emsesp/EMS-ESP32/issues/129)
|
||||
- Add current room influence for RC300 [#136](https://github.com/emsesp/EMS-ESP32/issues/136)
|
||||
- Added Home Assistant device_class to sensor entities
|
||||
- Added another Buderus RC10 thermostat with Product ID 65 [#160](https://github.com/emsesp/EMS-ESP32/issues/160)
|
||||
- Added support for mDNS [#161](https://github.com/emsesp/EMS-ESP32/issues/161)
|
||||
- Added last system ESP32 reset code to log (and `system info` output)
|
||||
- Firmware Checker in WebUI [#168](https://github.com/emsesp/EMS-ESP32/issues/168)
|
||||
- Added new MQTT setting for enabling 'response' topic
|
||||
- Support for non-standard Thermostats like Tado [#174](https://github.com/emsesp/EMS-ESP32/issues/174)
|
||||
- Include MQTT connection status in 'api/system/info'
|
||||
- Include Network status in 'api/system/info' and also the MQTT topic `info` [#202](https://github.com/emsesp/EMS-ESP32/issues/202)
|
||||
- Added Ethernet PHY module as an option in the Board Profile [#210](https://github.com/emsesp/EMS-ESP32/issues/210)
|
||||
|
||||
## Fixed
|
||||
|
||||
- MQTT reconnecting after WiFi reconnect [#99](https://github.com/emsesp/EMS-ESP32/issues/99)
|
||||
- Manually Controlling Solar Circuit [#107](https://github.com/emsesp/EMS-ESP32/issues/107)
|
||||
- Fix thermostat commands not defaulting to the master thermostat [#110](https://github.com/emsesp/EMS-ESP32/issues/110)
|
||||
- Enlarge parse-buffer for long names like `cylinderpumpmodulation`
|
||||
- MQTT not subscribing to all device entities [#166](https://github.com/emsesp/EMS-ESP32/issues/166)
|
||||
- Help fix issues with WebUI unable to fully load UI over Ethernet [#177](https://github.com/emsesp/EMS-ESP32/issues/177)
|
||||
- Shower alert never reset after limit reached when enabled [(PR #185)]
|
||||
- Remove HA entity entries when a device value goes dormant [#196](https://github.com/emsesp/EMS-ESP32/issues/196)
|
||||
- deciphering last error code dates on 0xC2 telegram [#204](https://github.com/emsesp/EMS-ESP32/issues/204)
|
||||
|
||||
## Changed
|
||||
|
||||
- Syslog BOM only for utf-8 messages [#91](https://github.com/emsesp/EMS-ESP32/issues/91)
|
||||
- Check for KM200 by device-id 0x48, remove tx-delay [#90](https://github.com/emsesp/EMS-ESP32/issues/90)
|
||||
- rename `fastheatupfactor` to `fastheatup` and add percent [#122](https://github.com/emsesp/EMS-ESP32/issues/122)
|
||||
- "unit" renamed to "uom" in API call to recall a Device Value
|
||||
- initial backend React changes to replace the class components (HOCs) with React Hooks
|
||||
- Use program-names instead of numbers
|
||||
- Boiler's maintenancemessage always published in MQTT (to prevent HA missing entity)
|
||||
- Unit of Measure 'times' added to MQTT Fails, Rx fails, Rx received, Tx fails, Tx reads & Tx writes
|
||||
- Improved API. Restful HTTP API works in the same way as MQTT calls
|
||||
- Removed settings for MQTT subscribe format [#173](https://github.com/emsesp/EMS-ESP32/issues/173)
|
||||
- Improve Nefit Moduline 200 functionality [#183](https://github.com/emsesp/EMS-ESP32/issues/183)
|
||||
- `status` in the MQTT heartbeat renamed to `bus_status`
|
||||
- Layout changes in the WebUI, showing stripped table rows in Dashboard
|
||||
- Alternative font for log window [#219](https://github.com/emsesp/EMS-ESP32/issues/219)
|
||||
|
||||
## **BREAKING CHANGES**
|
||||
|
||||
- API: "unit" renamed to "uom" in API call to recall a Device Value
|
||||
- HA: `sensor.boiler_boiler_temperature` renamed to `sensor.actual_boiler_temperature`
|
||||
- HA: `binary_sensor.boiler_ww_disinfecting` renamed to `binary_sensor.boiler_ww_disinfection`
|
||||
- HA: # removed from counts in MQTT Fails, Rx fails, Rx received, Tx fails, Tx reads & Tx writes
|
||||
- `txread` renamed to `txreads` and `txwrite` renamed to `txwrites` in MQTT heartbeat payload
|
||||
- 'dallas sensors' in api/system/info moved to the "System" section. Renamed "uptime (seconds)" and "reset reason"
|
||||
- `status` in the MQTT heartbeat renamed to `bus_status`
|
||||
|
||||
# [3.2.1] August 8 2021
|
||||
|
||||
## Added
|
||||
|
||||
- json body in API can now take device, name, cmd, hc and id
|
||||
- added example of how to use API directly to control values from Home Assistant
|
||||
- API calls are shown in debug log (For troubleshooting)
|
||||
|
||||
## Fixed
|
||||
|
||||
- fixed issue with Home Assistant entity naming where boiler's ww was duplicated in entity name
|
||||
- fixed issue where wwSetTemp was written too instead of wwSelTemp
|
||||
|
||||
## Changed
|
||||
|
||||
- fixed case on mqtt names, like 'wwtankmiddletemp'
|
||||
- renamed Product ID to 'EMS Product ID' in Home Assistant
|
||||
- removed brackets around tags, e.g. (hc1) selected room temperature" is now just "hc1 selected room temperature"
|
||||
|
||||
# [3.2.0] August 6 2021
|
||||
|
||||
## Added
|
||||
|
||||
- support for IPv6 (web/api/mqtt, not syslog yet) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||
- System Log in Web UI will show current time if the NTP Service is enabled [#82](https://github.com/emsesp/EMS-ESP32/issues/82)
|
||||
- Network settings for Tx-power, WiFi-bandwidth, WiFi-sleepmode [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||
- optional low CPU clockrate (160 MHz) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||
- select format for enumerated values in web
|
||||
- settings for water hysteresis on/off
|
||||
- dallas sensor name editable. `sensorname` console-command, replace sensorid with a unique name [#84](https://github.com/emsesp/EMS-ESP32/issues/84)
|
||||
- 'restart' system command. Can be invoked via API with authentication. [#87](https://github.com/emsesp/EMS-ESP32/issues/87)
|
||||
- add Download button in Web UI for log
|
||||
|
||||
## Fixed
|
||||
|
||||
- set mode allow numbers
|
||||
- Junkers thermostat shows mode as selected by set_mode
|
||||
- HA thermostat mode if bool-format: numbers is selected
|
||||
- Web UI System Log sometimes skipped a few log messages when watching real-time
|
||||
- fix wwactivated [#89](https://github.com/emsesp/EMS-ESP32/issues/89)
|
||||
- don't show commands (like reset) as Device values in the Web or Console
|
||||
|
||||
## Changed
|
||||
|
||||
- removed Rx echo failures counting as incomplete telegrams. Bad telegrams show as Warning and not Errors. [#80](https://github.com/emsesp/EMS-ESP32/issues/80)
|
||||
- add upload_sec to `api/system/info` and removed # from some names to keep consistent with MQTT heartbeat
|
||||
- added debug target to PlatformIO build to help hunt down system crashes
|
||||
- enumerated values always start at zero
|
||||
- maintenance settings for time/date as extra setting
|
||||
- move api/mqtt formats to `settings`, add `enum format`
|
||||
- UI improvements for editing Dallas Sensor details
|
||||
- RESTful GET commands can also require authentication (via bearer access token) for better security
|
||||
- Updated AsyncMqttClient to 0.9.0 and ArduinoJson to 6.18.3
|
||||
- Download buttons for settings and info under the Help tab
|
||||
|
||||
# [3.1.1] June 26 2021
|
||||
|
||||
## Added
|
||||
|
||||
- new command called `commands` which lists all available commands. `ems-esp/api/{device}/commands`
|
||||
- More Home Assistant icons to match the UOMs
|
||||
- new API. Using secure access tokens and OpenAPI standard. See `doc/EMS-ESP32 API.md` and [#50](https://github.com/emsesp/EMS-ESP32/issues/50)
|
||||
- show log messages in Web UI [#71](https://github.com/emsesp/EMS-ESP32/issues/71)
|
||||
|
||||
## Fixed
|
||||
|
||||
- HA thermostat mode was not in sync with actual mode [#66](https://github.com/emsesp/EMS-ESP32/issues/66)
|
||||
- Don't publish rssi if Wifi is disabled and ethernet is being used
|
||||
- Booleans are shown as true/false in API GETs
|
||||
|
||||
## Changed
|
||||
|
||||
- `info` command always shows full names in API. For short names query the device or name directly, e.g. `http://ems-esp/api/boiler`
|
||||
- free memory is shown in kilobytes
|
||||
- boiler's warm water entities have ww added to the Home Assistant entity name [#67](https://github.com/emsesp/EMS-ESP32/issues/67)
|
||||
- improved layout and rendering of device values in the WebUI, also the edit value screen
|
||||
|
||||
# [3.1.0] May 4 2021
|
||||
|
||||
## Added
|
||||
## [3.1.0] May 4 2021
|
||||
|
||||
- Mock API to simulate an ESP, for testing web
|
||||
- Able to write values from the Web UI
|
||||
- check values with `"cmd":<valuename>` and data empty or `?`
|
||||
- set hc for values and commands by id or prefix `hc<x>`+separator, separator can be any char
|
||||
|
||||
|
||||
## Fixed
|
||||
|
||||
- Don't create Home Assistant MQTT discovery entries for device values that don't exists (#756 on EMS-ESP repo)
|
||||
@@ -726,19 +27,57 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
||||
- lowercased Flow temp in commands
|
||||
- system console commands to main
|
||||
|
||||
# [3.0.1] March 30 2021
|
||||
## Removed
|
||||
|
||||
## [3.0.1] March 30 2021
|
||||
|
||||
## Added
|
||||
|
||||
- power settings, disabling BLE and turning off Wifi sleep
|
||||
- Rx and Tx counts to Heartbeat MQTT payload
|
||||
- ethernet support
|
||||
- id to info command to show only a heatingcircuit
|
||||
- add sending devices that are not listed to 0x07
|
||||
- extra MQTT boolean option for "ON" and "OFF"
|
||||
- support for chunked MQTT payloads to allow large data sets > 2kb
|
||||
- external Button support (#708) for resetting to factory defaults and other actions
|
||||
- new console set command in `system`, `set board_profile <profile>` for quickly enabling cabled ethernet connections without using the captive wifi portal
|
||||
- added in MQTT nested mode, for thermostat and mixer, like we had back in v2
|
||||
- cascade MC400 (product-id 210) (3.0.0b6), power values for heating sources (3.0.1b1)
|
||||
- values for wwMaxPower, wwFlowtempOffset
|
||||
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
||||
- syslog port selectable (#744)
|
||||
- individual mqtt commands (#31)
|
||||
- board Profiles (#11)
|
||||
|
||||
## Fixed
|
||||
|
||||
- telegrams matched to masterthermostat 0x18
|
||||
- multiple roomcontrollers
|
||||
- readback after write with delay (give ems-devices time to set the value)
|
||||
- thermostat ES72/RC20 device 66 to command-set RC20_2
|
||||
- MQTT payloads not adding to queue when MQTT is re-connecting (fixes #369)
|
||||
- fix for HA topics with invalid command formats (#728)
|
||||
- wrong position of values #723, #732
|
||||
- OTA Upload via Web on OSX
|
||||
- Rx and Tx quality % would sometimes show > 100
|
||||
|
||||
## Changed
|
||||
|
||||
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
||||
- split `show values` in smaller packages (edited)
|
||||
- extended length of IP/hostname from 32 to 48 chars (#676)
|
||||
- check flowsensor for `tap_water_active`
|
||||
- mqtt prefixed with `Base`
|
||||
- count Dallas sensor fails
|
||||
- switch from SPIFFS to LITTLEFS
|
||||
- added ID to MQTT payloads which is the Device's product ID and used in HA to identify a unique HA device
|
||||
- increased MQTT buffer and reduced wait time between publishes
|
||||
- updated to the latest ArduinoJson library
|
||||
- some names of mqtt-tags like in v2.2.1
|
||||
- new ESP32 partition side to allow for smoother OTA and fallback
|
||||
- network Gateway IP is optional (#682)emsesp/EMS-ESP
|
||||
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
|
||||
- invert LED changed to Hide LED. Default is off.
|
||||
- renamed Scan Network to Scan WiFi Network
|
||||
- added version to cmd=settings
|
||||
@@ -748,9 +87,9 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
||||
|
||||
- Shower Alert (disabled for now)
|
||||
|
||||
# [3.0.0] March 18 2021
|
||||
## [3.0.0] March 18 2021
|
||||
|
||||
## Added
|
||||
### Added
|
||||
|
||||
- Power settings, disabling BLE and turning off Wifi sleep
|
||||
- Rx and Tx counts to Heartbeat MQTT payload
|
||||
@@ -767,7 +106,7 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
||||
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
||||
- Syslog port selectable (#744)
|
||||
|
||||
## Fixed
|
||||
### Fixed
|
||||
|
||||
- telegrams matched to masterthermostat 0x18
|
||||
- multiple roomcontrollers
|
||||
@@ -778,7 +117,7 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
||||
- wrong position of values #723, #732
|
||||
- OTA Upload via Web on OSX
|
||||
|
||||
## Changed
|
||||
### Changed
|
||||
|
||||
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
||||
- split `show values` in smaller packages (edited)
|
||||
@@ -793,4 +132,4 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
||||
- some names of mqtt-tags like in v2.2.1
|
||||
- new ESP32 partition side to allow for smoother OTA and fallback
|
||||
- Network Gateway IP is optional (#682)emsesp/EMS-ESP
|
||||
- moved to a new GitHub repo <https://github.com/emsesp/EMS-ESP32>
|
||||
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
|
||||
|
||||
@@ -1,59 +1,9 @@
|
||||
# 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)
|
||||
- new boiler entities VR0,VR1, compressor speed [#2669](https://github.com/emsesp/EMS-ESP32/issues/2669)
|
||||
- solar temperature TS16 [#2690](https://github.com/emsesp/EMS-ESP32/issues/2690)
|
||||
|
||||
## 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)
|
||||
- syslog timestamp [#2704](https://github.com/emsesp/EMS-ESP32/issues/2704)
|
||||
|
||||
## 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)
|
||||
## Removed
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<img src="/media/EMS-ESP_logo_dark.png" alt="Logo" align="right" height="76"/>
|
||||
|
||||
# Contributing
|
||||
|
||||
**Any contribution helps EMS-ESP get better for the entire community!**
|
||||
@@ -6,7 +8,7 @@ Everybody is welcome and invited to contribute to the EMS-ESP Project by:
|
||||
|
||||
- providing Pull Requests (Features, Fixes, suggestions)
|
||||
- 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.github.io/docs)
|
||||
|
||||
This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs.
|
||||
|
||||
@@ -22,17 +24,17 @@ This document describes rules that are in effect for this repository, meant for
|
||||
|
||||
## Triaging of Issues/PR's
|
||||
|
||||
1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so.
|
||||
2. An issue that needs to be closed, either due to not complying with this policy, or for other reasons, should be closed by a contributor.
|
||||
3. Issues that are accepted should be marked with appropriate labels.
|
||||
4. Issues that could impact functionality for many users should be considered severe.
|
||||
5. Issues caused by the SDK or chip should not be marked severe, as there usually isn’t much to be done. Common sense should be applied when deciding. Such issues should be documented in the documentation, for reference by users.
|
||||
6. Issues with feature requests should be discussed for viability/desirability.
|
||||
7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified.
|
||||
8. Feature requests that are not accompanied by a PR:
|
||||
- could be closed immediately (denied).
|
||||
- could be closed after some predetermined period of time (left as candidate for somebody to pick up).
|
||||
9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot.
|
||||
1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so.
|
||||
2. An issue that needs to be closed, either due to not complying with this policy, or for other reasons, should be closed by a contributor.
|
||||
3. Issues that are accepted should be marked with appropriate labels.
|
||||
4. Issues that could impact functionality for many users should be considered severe.
|
||||
5. Issues caused by the SDK or chip should not be marked severe, as there usually isn’t much to be done. Common sense should be applied when deciding. Such issues should be documented in the documentation, for reference by users.
|
||||
6. Issues with feature requests should be discussed for viability/desirability.
|
||||
7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified.
|
||||
8. Feature requests that are not accompanied by a PR:
|
||||
* could be closed immediately (denied).
|
||||
* could be closed after some predetermined period of time (left as candidate for somebody to pick up).
|
||||
9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot.
|
||||
|
||||
## Pull requests
|
||||
|
||||
@@ -40,24 +42,24 @@ A Pull Request (PR) is the process where code modifications are managed in GitHu
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
|
||||
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
|
||||
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
|
||||
|
||||
1. All pull requests must be done against the dev branch.
|
||||
2. Make sure code is formatting per the `.clang-format`.
|
||||
3. Make sure any new code is clearly commented explaining what the function/logic does.
|
||||
4. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
|
||||
5. Only one feature/fix should be added per PR.
|
||||
6. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
|
||||
7. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
|
||||
8. All pull requests should consider updates to the documentation.
|
||||
9. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
|
||||
10. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
|
||||
11. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
|
||||
12. Pull requests that don't meet the above will be denied and closed.
|
||||
2. Make sure code is formatting per the `.clang-format`
|
||||
3. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
|
||||
4. Only one feature/fix should be added per PR.
|
||||
5. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
|
||||
6. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
|
||||
7. All pull requests should consider updates to the documentation.
|
||||
8. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
|
||||
9. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
|
||||
10. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
|
||||
11. Pull requests that don't meet the above will be denied and closed.
|
||||
|
||||
|
||||
## Semantic Commit Messages
|
||||
|
||||
@@ -69,7 +71,7 @@ Format: `<type>(<scope>): <subject>`
|
||||
|
||||
## Example
|
||||
|
||||
```text
|
||||
```
|
||||
feat: add hat wobble
|
||||
^--^ ^------------^
|
||||
| |
|
||||
@@ -90,13 +92,13 @@ More Examples:
|
||||
|
||||
References:
|
||||
|
||||
- <https://www.conventionalcommits.org/>
|
||||
- https://www.conventionalcommits.org/
|
||||
|
||||
---
|
||||
--------------------------------------
|
||||
|
||||
## Contributor License Agreement (CLA)
|
||||
|
||||
```text
|
||||
```
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
@@ -121,7 +123,7 @@ By making a contribution to this project, I certify that:
|
||||
|
||||
This Contributor License Agreement (CLA) was adopted on April 1st, 2019.
|
||||
|
||||
The text of this license is available under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-sa/3.0/). It is based on the Linux [Developer Certificate Of Origin](http://elinux.org/Developer_Certificate_Of_Origin), but is modified to explicitly use the GPL-3.0 license and not mention sign-off (due to GitHub.com keeps an historial, with your user name, of PRs' commits and all editions on PR's comments).
|
||||
The text of this license is available under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-sa/3.0/). It is based on the Linux [Developer Certificate Of Origin](http://elinux.org/Developer_Certificate_Of_Origin), but is modified to explicitly use the GPL-3.0 license and not mention sign-off (due to GitHub.com keeps an historial, with your user name, of PRs' commits and all editions on PR's comments).
|
||||
|
||||
**Why a CLA ?**
|
||||
|
||||
@@ -131,10 +133,10 @@ A CLA is a legal document in which you state _you are entitled to contribute the
|
||||
|
||||
CLA is a safety because it also ensures that once you have provided a contribution, you cannot try to withdraw permission for its use at a later date. People can therefore use that software, confident that they will not be asked to stop using pieces of the code at a later date.
|
||||
|
||||
A **license** grants "outbound" rights to the user of project.
|
||||
A __license__ grants "outbound" rights to the user of project.
|
||||
|
||||
A **CLA** enables a contributor to grant "inbound" rights to a project.
|
||||
A __CLA__ enables a contributor to grant "inbound" rights to a project.
|
||||
|
||||
<Other>
|
||||
<A table should be maintained for relating maintainers and components. When triaging, this is essential to figure out if someone in particular should be consulted about specific changes.>
|
||||
<A stable release cadence should be established, e.g.: every month.>
|
||||
<A stable release cadence should be established, e.g.: every month.>
|
||||
180
README.md
180
README.md
@@ -1,119 +1,107 @@
|
||||
<div align="center">
|
||||
<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>
|
||||
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
||||
|
||||
<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>
|
||||
This is the firmware for the ESP32. Compared to version 2 on the ESP8266, this version has
|
||||
- Ethernet Support
|
||||
- Pre-configured board layouts
|
||||
- Writing values directly from the Web UI
|
||||
- Mock API server for faster offline development
|
||||
- Expose to more commands, via MQTT
|
||||
- Improvements to Dallas sensors, Shower service
|
||||
|
||||
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||
[](LICENSE)
|
||||
[](https://sonarcloud.io/summary/new_code?id=emsesp_EMS-ESP32)
|
||||
[](https://app.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
[](https://www.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=github.com&utm_medium=referral&utm_content=emsesp/EMS-ESP32&utm_campaign=Badge_Grade)
|
||||
[](https://github.com/emsesp/EMS-ESP32/releases)
|
||||
[](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Average time to resolve an issue")
|
||||
[](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Percentage of issues still open")
|
||||
<br/>
|
||||
[](https://discord.gg/3J3GgnzpyT)
|
||||
|
||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
||||
|
||||
[](https://github.com/emsesp/EMS-ESP32/stargazers)
|
||||
[](https://github.com/emsesp/EMS-ESP32/network)
|
||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||
[](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.
|
||||
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at https://bbqkees-electronics.nl.
|
||||
|
||||
It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
||||
<img src="media/gateway-integration.jpg" width=40%>
|
||||
|
||||
## 📦 **Key Features**
|
||||
---
|
||||
|
||||
- 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
|
||||
- Easy to add external Temperature and Analog sensors that are attached to GPIO pins on the ESP32 board
|
||||
- A multi-user, multi-language web interface to change settings and monitor incoming data
|
||||
- A simple to use console, accessible via Serial/USB or Telnet for advanced operations and detailed monitoring
|
||||
- Native integration with Home Assistant, Domoticz, openHAB and Modbus
|
||||
- Easy setup and install with automatic updates
|
||||
- Simulation of remote thermostats
|
||||
- Localized in 11 languages, and customizable to rename any device or sensor
|
||||
- Extendable by adding own custom EMS entities
|
||||
- Expandable via adding user-built external modules
|
||||
- A powerful Scheduler to automate tasks and trigger events based data changes
|
||||
- A Notification service to alert you of important events
|
||||
|
||||
## 🚀 **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.
|
||||
|
||||
## 📋 **Documentation**
|
||||
|
||||
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.
|
||||
|
||||
## 💬 **Getting Support**
|
||||
|
||||
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 [how to request support](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
|
||||
|
||||
## 🎥 **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.
|
||||
|
||||
## 💖 **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).
|
||||
|
||||
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.
|
||||
|
||||
## 📦 **Building**
|
||||
|
||||
To build the web interface only, run `platformio run -e build_webUI`. This will install the necessary dependencies and build the web interface and also create the embedded code used need to build the firmware. You can run the web interface locally by going to the `interface` directory and running `pnpm standalone`.
|
||||
|
||||
To build the firmware, run `platformio run`. This will build the firmware for all ESP32 modules and place the binaries in the `build/firmware` folder. If you want to configure the build for a single platform create a local `pio_local.ni` file in the root directory (see example in `pio_local.ini_example`).
|
||||
|
||||
## 📢 **Libraries used**
|
||||
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) originally 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
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing
|
||||
- [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
|
||||
|
||||
## 📜 **License**
|
||||
|
||||
This program is licensed under GPL-3.0
|
||||
## **Features**
|
||||
|
||||
- Compatible with both ESP8266 and ESP32
|
||||
- A multi-user secure web interface to change settings and monitor the data
|
||||
- A console, accessible via Serial and Telnet for more monitoring
|
||||
- Native support for Home Assistant via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||
- Easy first-time configuration via a web Captive Portal
|
||||
- Support for more than [70 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
||||
|
||||
## **Screenshots**
|
||||
|
||||
### Web Interface
|
||||
### Web Interface:
|
||||
|
||||
| | |
|
||||
| ---------------------------------- | -------------------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| | |
|
||||
| --- | --- |
|
||||
| <img src="media/web_settings.PNG"> | <img src="media/web_status.PNG"> |
|
||||
| <img src="media/web_devices.PNG"> | <img src="media/web_mqtt.PNG"> |
|
||||
|
||||
### Telnet Console
|
||||
### Telnet Console:
|
||||
<img src="media/console.PNG" width=80% height=80%>
|
||||
|
||||

|
||||
### In Home Assistant:
|
||||
<img src="media/ha_lovelace.PNG" width=80% height=80%>
|
||||
|
||||
## **Installing**
|
||||
|
||||
### Home Assistant
|
||||
Refer to the [official documentation](https://emsesp.github.io/docs) to how to install the firmware and configure it. The documentation is being constantly updated as new features and settings are added.
|
||||
|
||||

|
||||
You can choose to use an pre-built firmware image or compile the code yourself:
|
||||
|
||||
* [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
|
||||
* [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
|
||||
|
||||
## **Support Information**
|
||||
|
||||
If you're looking for support on **EMS-ESP** there are some options available:
|
||||
|
||||
### Documentation
|
||||
|
||||
* [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
|
||||
* [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
|
||||
|
||||
### Support Community
|
||||
|
||||
* [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
|
||||
* [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
||||
|
||||
### Developer's Community
|
||||
|
||||
* [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
|
||||
* [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
|
||||
* [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new *Troubleshooting & Question* issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
|
||||
|
||||
## **Contributing**
|
||||
|
||||
You can contribute to EMS-ESP by
|
||||
- providing Pull Requests (Features, Fixes, suggestions)
|
||||
- testing new released features and report issues on your EMS equipment
|
||||
- contributing to missing [Documentation](https://emsesp.github.io/docs)
|
||||
|
||||
## **Credits**
|
||||
|
||||
A shout out to the people helping EMS-ESP get to where it is today...
|
||||
- **@MichaelDvP** for all his amazing contributions and patience. Specifically for the improved uart library, thermostat and mixer logic.
|
||||
- **@BBQKees** for his endless testing and building the awesome circuit boards
|
||||
- **@rjwats** for his [esp8266-react](https://github.com/rjwats/esp8266-react) framework that provides the new Web UI
|
||||
- **@nomis** for his core [console](https://github.com/nomis/mcu-uuid-console), telnet and syslog core libraries
|
||||
- plus everyone else providing suggestions, PRs and the odd donation that keeps this project open source. Thanks!
|
||||
|
||||
## **License**
|
||||
|
||||
This program is licensed under GPL-3.0
|
||||
|
||||
6
RELEASENOTES.md
Normal file
6
RELEASENOTES.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 
|
||||
|
||||
# Firmware Installation
|
||||
|
||||
Follow the instructions in the [documentation](https://emsesp.github.io/docs) on how to install the firmware binaries in the Assets below.
|
||||
|
||||
8
RELEASENOTES_DEV.md
Normal file
8
RELEASENOTES_DEV.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 
|
||||
|
||||
This is a snapshot of the current "beta" development code and firmware binaries for the ESP32. It has all the latest features and fixes but please be aware that this is still experimental firmware used for testing and thus may contain the odd bug. Use at your own risk and remember to report an issue if you find something unusual.
|
||||
|
||||
# Firmware Installation
|
||||
|
||||
Follow the instructions in the [documentation](https://emsesp.github.io/docs) on how to install the firmware binaries in the Assets below.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
39
cspell.json
39
cspell.json
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
||||
"version": "0.2",
|
||||
"dictionaryDefinitions": [
|
||||
{
|
||||
"name": "project-words",
|
||||
"path": "./project-words.txt",
|
||||
"addWords": true
|
||||
}
|
||||
],
|
||||
"dictionaries": ["project-words"],
|
||||
"ignorePaths": [
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -1,231 +0,0 @@
|
||||
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,HydrTemp,
|
||||
0x23,JunkersSetMixer,fetched
|
||||
0x27,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,
|
||||
0xC0,RCErrorMessage,
|
||||
0xC2,UBAErrorMessage3,
|
||||
0xC6,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,JunkersDisp,fetched
|
||||
0x012E,HPEnergy1,
|
||||
0x013B,HPEnergy2,
|
||||
0x0165,JunkersSet,
|
||||
0x0166,JunkersSet,
|
||||
0x0167,JunkersSet,
|
||||
0x0168,JunkersSet,
|
||||
0x016E,Absent,fetched
|
||||
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
|
||||
0x0241,RC300Settings,fetched
|
||||
0x0267,RC300Floordry,
|
||||
0x0269,RC300Holiday,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,HPPressure,fetched
|
||||
0x02CD,MMPLUSConfigMessage,fetched
|
||||
0x02CE,RC300Set2,
|
||||
0x02D0,RC300Set2,
|
||||
0x02D2,RC300Set2,
|
||||
0x02D6,HPPump2,fetched
|
||||
0x02D7,MMPLUSStatusMessage,
|
||||
0x02E0,UBASetPoints,
|
||||
0x02F5,RC300WWmode,fetched
|
||||
0x02F6,RC300WW2mode,fetched
|
||||
0x0313,MMPLUSConfigMessage_WWC,fetched
|
||||
0x031B,RC300WWtemp,fetched
|
||||
0x031D,RC300WWmode2,
|
||||
0x031E,RC300WWmode2,
|
||||
0x0331,MMPLUSStatusMessage_WWC,
|
||||
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
|
||||
0x043F,CRHolidays,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
|
||||
0x055C,VentilationSet,fetched
|
||||
0x056B,VentilationMode,fetched
|
||||
0x0583,VentilationMonitor,
|
||||
0x0585,Blowerspeed,
|
||||
0x0587,Bypass,
|
||||
0x05BA,HpPoolStatus,fetched
|
||||
0x05D9,Airquality,
|
||||
0x0772,HIUSettings,
|
||||
0x0779,HIUMonitor,
|
||||
0x07A5,SM100wwCirc,fetched
|
||||
0x07A6,SM100wwParam,fetched
|
||||
0x07AA,SM100wwStatus,
|
||||
0x07AB,SM100wwCommand,
|
||||
0x07AC,SM100wwParam1,
|
||||
0x07AD,SM100ValveStatus,
|
||||
0x07AE,SM100wwKeepWarm,fetched
|
||||
0x07D6,SM100wwTemperature,
|
||||
0x07E0,SM100wwStatus2,fetched
|
||||
0x0935,EM100SetMessage,fetched
|
||||
0x0936,EM100OutMessage,
|
||||
0x0937,EM100TempMessage,
|
||||
0x0938,EM100InputMessage,
|
||||
0x0939,EM100MonitorMessage,
|
||||
0x093A,EM100ConfigMessage,
|
||||
0x0998,HPSettings,fetched
|
||||
0x0999,HPFunctionTest,fetched
|
||||
0x099A,HPStarts,
|
||||
0x099B,HPFlowTemp,
|
||||
0x099C,HPComp,
|
||||
0x09A0,HPTemperature,
|
||||
|
6
esp32_partition_app1984k_spiffs64k.csv
Normal file
6
esp32_partition_app1984k_spiffs64k.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x1F0000,
|
||||
app1, app, ota_1, 0x200000, 0x1F0000,
|
||||
spiffs, data, spiffs, 0x3F0000,0x10000,
|
||||
|
@@ -7,8 +7,8 @@ build_flags =
|
||||
|
||||
; Access point settings
|
||||
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
|
||||
-D FACTORY_AP_SSID=\"ems-esp\"
|
||||
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\"
|
||||
-D FACTORY_AP_SSID=\"ems-esp\" ; 1-64 characters
|
||||
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\" ; 8-64 characters
|
||||
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
|
||||
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
|
||||
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
|
||||
@@ -25,14 +25,20 @@ build_flags =
|
||||
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"CET-1CEST,M3.5.0,M10.5.0/3\"
|
||||
-D FACTORY_NTP_SERVER=\"time.google.com\"
|
||||
|
||||
; OTA settings
|
||||
-D FACTORY_OTA_PORT=8266
|
||||
-D FACTORY_OTA_PASSWORD=\"ems-esp-neo\"
|
||||
-D FACTORY_OTA_ENABLED=true
|
||||
|
||||
; MQTT settings
|
||||
-D FACTORY_MQTT_ENABLED=false
|
||||
-D FACTORY_MQTT_HOST=\"\"
|
||||
-D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
|
||||
-D FACTORY_MQTT_PORT=1883
|
||||
-D FACTORY_MQTT_USERNAME=\"\"
|
||||
-D FACTORY_MQTT_PASSWORD=\"\"
|
||||
-D FACTORY_MQTT_CLIENT_ID=\"ems-esp\"
|
||||
-D FACTORY_MQTT_KEEP_ALIVE=60
|
||||
-D FACTORY_MQTT_CLEAN_SESSION=false
|
||||
-D FACTORY_MQTT_CLEAN_SESSION=true
|
||||
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
|
||||
|
||||
; JWT Secret
|
||||
|
||||
5
interface/.env
Normal file
5
interface/.env
Normal file
@@ -0,0 +1,5 @@
|
||||
# This is the name of your project. It appears on the sign-in page and in the menu bar.
|
||||
REACT_APP_PROJECT_NAME=EMS-ESP
|
||||
|
||||
# This is the url path your project will be exposed under.
|
||||
REACT_APP_PROJECT_PATH=ems-esp
|
||||
@@ -1,2 +1,5 @@
|
||||
VITE_ALOVA_TIPS=0
|
||||
REACT_APP_ALOVA_TIPS=0
|
||||
# Change the IP address to that of your ESP device to enable local development of the UI
|
||||
|
||||
# REACT_APP_HTTP_ROOT=http://localhost:3000
|
||||
# REACT_APP_WEB_SOCKET_ROOT=ws://localhost:3000
|
||||
|
||||
|
||||
1
interface/.env.production
Normal file
1
interface/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
GENERATE_SOURCEMAP=false
|
||||
@@ -1,7 +0,0 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
src/i18n/*
|
||||
|
||||
.prettierrc
|
||||
.typesafe-i18n.json
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"adapter": "react",
|
||||
"baseLocale": "pl",
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
|
||||
}
|
||||
37
interface/config-overrides.js
Normal file
37
interface/config-overrides.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const ProgmemGenerator = require('./progmem-generator.js');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = function override(config, env) {
|
||||
if (env === "production") {
|
||||
// rename the output file, we need it's path to be short for LittleFS
|
||||
config.output.filename = 'js/[id].[chunkhash:4].js';
|
||||
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
||||
|
||||
// take out the manifest and service worker plugins
|
||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin));
|
||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW));
|
||||
|
||||
// shorten css filenames
|
||||
const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin);
|
||||
miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css";
|
||||
miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css";
|
||||
|
||||
// build progmem data files
|
||||
config.plugins.push(new ProgmemGenerator({ outputPath: "../lib/framework/WWWData.h", bytesPerLine: 20 }));
|
||||
|
||||
// add compression plugin, compress javascript
|
||||
config.plugins.push(new CompressionPlugin({
|
||||
filename: "[path].gz[query]",
|
||||
algorithm: "gzip",
|
||||
test: /\.(js)$/,
|
||||
deleteOriginalAssets: true
|
||||
}));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
prettierConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'dist/*',
|
||||
'*.mjs',
|
||||
'build/*',
|
||||
'*.js',
|
||||
'**/*.cjs',
|
||||
'**/unpack.ts',
|
||||
'i18n*.*'
|
||||
]
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
'error',
|
||||
{
|
||||
checksVoidReturn: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="stylesheet" href="/css/roboto.css" />
|
||||
<link rel="manifest" href="/app/manifest.json" />
|
||||
<title>EMS-ESP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
39411
interface/package-lock.json
generated
Normal file
39411
interface/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,68 +1,63 @@
|
||||
{
|
||||
"name": "EMS-ESP",
|
||||
"version": "3.7.3",
|
||||
"description": "EMS-ESP WebUI",
|
||||
"homepage": "https://emsesp.org",
|
||||
"author": "proddy, emsesp.org",
|
||||
"license": "MIT",
|
||||
"name": "esp8266-react",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build-hosted": "typesafe-i18n && vite build --mode hosted",
|
||||
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"",
|
||||
"mock-rest": "bun --watch ../mock-api/restServer.ts",
|
||||
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite\"",
|
||||
"typesafe-i18n": "typesafe-i18n --no-watch",
|
||||
"webUI": "node progmem-generator.js",
|
||||
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
|
||||
"lint": "eslint . --fix",
|
||||
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/adapter-xhr": "2.2.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@table-library/react-table-library": "4.1.15",
|
||||
"alova": "3.3.4",
|
||||
"async-validator": "^4.2.5",
|
||||
"formidable": "^3.5.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"mime-types": "^3.0.1",
|
||||
"preact": "^10.27.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.5",
|
||||
"react-toastify": "^11.0.5",
|
||||
"typesafe-i18n": "^5.26.2",
|
||||
"typescript": "^5.9.3"
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^15.0.1",
|
||||
"@types/react": "^17.0.4",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@types/react-material-ui-form-validator": "^2.1.0",
|
||||
"@types/react-router": "^5.1.13",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"compression-webpack-plugin": "^5.0.2",
|
||||
"express": "^4.17.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.30",
|
||||
"notistack": "^1.0.6",
|
||||
"parse-ms": "^3.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
"react-form-validator-core": "^1.1.1",
|
||||
"react-material-ui-form-validator": "^2.1.4",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"sockette": "^2.0.6",
|
||||
"typescript": "4.2.4",
|
||||
"zlib": "^1.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"eject": "react-scripts eject",
|
||||
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
||||
"dev": "run-p start mock-api"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.5",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@preact/compat": "^18.3.1",
|
||||
"@preact/preset-vite": "^2.10.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.6.2",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"terser": "^5.44.0",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-imagemin": "^0.6.1",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.20.0"
|
||||
"concurrently": "^6.0.1",
|
||||
"http-proxy-middleware": "^1.1.1",
|
||||
"nodemon": "^2.0.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"react-app-rewired": "^2.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
6056
interface/pnpm-lock.yaml
generated
6056
interface/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- cwebp-bin
|
||||
- esbuild
|
||||
- gifsicle
|
||||
- jpegtran-bin
|
||||
- mozjpeg
|
||||
- optipng-bin
|
||||
- pngquant-bin
|
||||
@@ -1,148 +1,122 @@
|
||||
import crypto from 'crypto';
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
unlinkSync
|
||||
} from 'fs';
|
||||
import mime from 'mime-types';
|
||||
import { relative, resolve, sep } from 'path';
|
||||
import zlib from 'zlib';
|
||||
const { resolve, relative, sep } = require('path');
|
||||
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
|
||||
var zlib = require('zlib');
|
||||
var mime = require('mime-types');
|
||||
|
||||
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||
const INDENT = ' ';
|
||||
const outputPath = '../src/ESP32React/WWWData.h';
|
||||
const sourcePath = './dist';
|
||||
const bytesPerLine = 20;
|
||||
let 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 ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
|
||||
|
||||
const generateWWWClass =
|
||||
() => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
|
||||
// Bundle Statistics:
|
||||
// - 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 {
|
||||
${INDENT}public:
|
||||
${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')}
|
||||
${INDENT.repeat(2)}}
|
||||
};
|
||||
`;
|
||||
|
||||
const getFilesSync = (dir, files = []) => {
|
||||
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||
function getFilesSync(dir, files = []) {
|
||||
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
|
||||
const entryPath = resolve(dir, entry.name);
|
||||
entry.isDirectory() ? getFilesSync(entryPath, files) : files.push(entryPath);
|
||||
});
|
||||
return files;
|
||||
};
|
||||
|
||||
const cleanAndOpen = (path) => {
|
||||
existsSync(path) && unlinkSync(path);
|
||||
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 variable = `ESP_REACT_DATA_${fileInfo.length}`;
|
||||
const mimeType = mime.lookup(relativeFilePath);
|
||||
const fileType = getFileType(relativeFilePath);
|
||||
let size = 0;
|
||||
writeStream.write(`const uint8_t ${variable}[] = {`);
|
||||
|
||||
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
|
||||
const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
|
||||
|
||||
zipBuffer.forEach((b) => {
|
||||
if (!(size % bytesPerLine)) {
|
||||
writeStream.write('\n' + INDENT);
|
||||
if (entry.isDirectory()) {
|
||||
getFilesSync(entryPath, files);
|
||||
} else {
|
||||
files.push(entryPath);
|
||||
}
|
||||
writeStream.write('0x' + b.toString(16).toUpperCase().padStart(2, '0') + ',');
|
||||
size++;
|
||||
});
|
||||
|
||||
size % bytesPerLine && writeStream.write('\n');
|
||||
writeStream.write('};\n\n');
|
||||
|
||||
// Update bundle statistics
|
||||
bundleStats[fileType].count++;
|
||||
bundleStats[fileType].uncompressed += buffer.length;
|
||||
bundleStats[fileType].compressed += zipBuffer.length;
|
||||
|
||||
fileInfo.push({
|
||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||
mimeType,
|
||||
variable,
|
||||
size,
|
||||
hash
|
||||
});
|
||||
|
||||
totalSize += size;
|
||||
};
|
||||
|
||||
console.log(`Generating ${outputPath} from ${sourcePath}`);
|
||||
const fileInfo = [];
|
||||
const writeStream = cleanAndOpen(resolve(outputPath));
|
||||
|
||||
writeStream.write(ARDUINO_INCLUDES);
|
||||
|
||||
const buildPath = resolve(sourcePath);
|
||||
for (const filePath of getFilesSync(buildPath)) {
|
||||
writeFile(relative(buildPath, filePath), readFileSync(filePath));
|
||||
})
|
||||
return files;
|
||||
}
|
||||
|
||||
writeStream.write(generateWWWClass());
|
||||
writeStream.end();
|
||||
function coherseToBuffer(input) {
|
||||
return Buffer.isBuffer(input) ? input : Buffer.from(input);
|
||||
}
|
||||
|
||||
// Calculate and display bundle statistics
|
||||
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);
|
||||
function cleanAndOpen(path) {
|
||||
if (existsSync(path)) {
|
||||
unlinkSync(path);
|
||||
}
|
||||
return createWriteStream(path, { flags: "w+" });
|
||||
}
|
||||
|
||||
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)`
|
||||
class ProgmemGenerator {
|
||||
|
||||
constructor(options = {}) {
|
||||
const { outputPath, bytesPerLine = 20, indent = " ", includes = ARDUINO_INCLUDES } = options;
|
||||
this.options = { outputPath, bytesPerLine, indent, includes };
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tapAsync(
|
||||
{ name: 'ProgmemGenerator' },
|
||||
(compilation, callback) => {
|
||||
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
||||
const fileInfo = [];
|
||||
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
||||
try {
|
||||
const writeIncludes = () => {
|
||||
writeStream.write(includes);
|
||||
}
|
||||
|
||||
const writeFile = (relativeFilePath, buffer) => {
|
||||
const variable = "ESP_REACT_DATA_" + fileInfo.length;
|
||||
const mimeType = mime.lookup(relativeFilePath);
|
||||
var size = 0;
|
||||
writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
|
||||
const zipBuffer = zlib.gzipSync(buffer);
|
||||
zipBuffer.forEach((b) => {
|
||||
if (!(size % bytesPerLine)) {
|
||||
writeStream.write("\n");
|
||||
writeStream.write(indent);
|
||||
}
|
||||
writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
|
||||
size++;
|
||||
});
|
||||
if (size % bytesPerLine) {
|
||||
writeStream.write("\n");
|
||||
}
|
||||
writeStream.write("};\n\n");
|
||||
fileInfo.push({
|
||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||
mimeType,
|
||||
variable,
|
||||
size
|
||||
});
|
||||
};
|
||||
|
||||
const writeFiles = () => {
|
||||
// process static files
|
||||
const buildPath = compilation.options.output.path;
|
||||
for (const filePath of getFilesSync(buildPath)) {
|
||||
const readStream = readFileSync(filePath);
|
||||
const relativeFilePath = relative(buildPath, filePath);
|
||||
writeFile(relativeFilePath, readStream);
|
||||
}
|
||||
// process assets
|
||||
const { assets } = compilation;
|
||||
Object.keys(assets).forEach((relativeFilePath) => {
|
||||
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
||||
});
|
||||
}
|
||||
|
||||
const generateWWWClass = () => {
|
||||
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
||||
|
||||
class WWWData {
|
||||
${indent}public:
|
||||
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||
${fileInfo.map(file => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`).join('\n')}
|
||||
${indent.repeat(2)}}
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
const writeWWWClass = () => {
|
||||
writeStream.write(generateWWWClass());
|
||||
}
|
||||
|
||||
writeIncludes();
|
||||
writeFiles();
|
||||
writeWWWClass();
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
callback();
|
||||
});
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log('='.repeat(50));
|
||||
}
|
||||
|
||||
module.exports = ProgmemGenerator;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "EMS-ESP",
|
||||
"icons": [
|
||||
"name":"EMS-ESP",
|
||||
"icons":[
|
||||
{
|
||||
"src": "/app/icon.png",
|
||||
"sizes": "48x48 72x72 96x96 128x128 256x256"
|
||||
"src":"/app/icon.png",
|
||||
"sizes":"48x48 72x72 96x96 128x128 256x256"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "fullscreen",
|
||||
"orientation": "any"
|
||||
"start_url":"/",
|
||||
"display":"fullscreen",
|
||||
"orientation":"any"
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
/*
|
||||
* Uses font-weight 400 (normal) only, no bold, and Latin with a few extra unicode chars.
|
||||
* This is to keep flash memory to a minimum
|
||||
* View fonts on https://fonts.google.com/
|
||||
* Download woff2 using e.g. https://fonts.googleapis.com/css2?family=Lato or https://fonts.googleapis.com/css2?family=Roboto
|
||||
*/
|
||||
/* Just supporting latin due to size constrains on the esp chip */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/li.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
/* src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2'); */
|
||||
src:
|
||||
local('Roboto'),
|
||||
local('Roboto-Regular'),
|
||||
url(../fonts/re.woff2) format('woff2');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144,
|
||||
U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||
U+FEFF, U+FFFD;
|
||||
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/me.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
}
|
||||
BIN
interface/public/fonts/li.woff2
Normal file
BIN
interface/public/fonts/li.woff2
Normal file
Binary file not shown.
BIN
interface/public/fonts/me.woff2
Normal file
BIN
interface/public/fonts/me.woff2
Normal file
Binary file not shown.
Binary file not shown.
16
interface/public/index.html
Normal file
16
interface/public/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="%PUBLIC_URL%/css/roboto.css">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/app/manifest.json">
|
||||
<title>EMS-ESP</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,79 +1,50 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ToastContainer, Zoom } from 'react-toastify';
|
||||
import React, { Component, RefObject } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
|
||||
import AppRouting from 'AppRouting';
|
||||
import CustomTheme from 'CustomTheme';
|
||||
import TypesafeI18n from 'i18n/i18n-react';
|
||||
import type { Locales } from 'i18n/i18n-types';
|
||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
|
||||
// Memoize available locales to prevent recreation on every render
|
||||
const AVAILABLE_LOCALES = [
|
||||
'de',
|
||||
'en',
|
||||
'it',
|
||||
'fr',
|
||||
'nl',
|
||||
'no',
|
||||
'pl',
|
||||
'sk',
|
||||
'sv',
|
||||
'tr',
|
||||
'cz'
|
||||
] as Locales[];
|
||||
import AppRouting from './AppRouting';
|
||||
import CustomMuiTheme from './CustomMuiTheme';
|
||||
import { PROJECT_NAME } from './api';
|
||||
import FeaturesWrapper from './features/FeaturesWrapper';
|
||||
|
||||
const App = memo(() => {
|
||||
const [wasLoaded, setWasLoaded] = useState(false);
|
||||
const [locale, setLocale] = useState<Locales>('en');
|
||||
// this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid.
|
||||
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||
|
||||
// Memoize locale initialization to prevent unnecessary re-runs
|
||||
const initializeLocale = useCallback(async () => {
|
||||
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
|
||||
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
|
||||
localStorage.setItem('lang', newLocale);
|
||||
setLocale(newLocale);
|
||||
await loadLocaleAsync(newLocale);
|
||||
setWasLoaded(true);
|
||||
}, []);
|
||||
class App extends Component {
|
||||
|
||||
useEffect(() => {
|
||||
void initializeLocale();
|
||||
}, [initializeLocale]);
|
||||
notistackRef: RefObject<any> = React.createRef();
|
||||
|
||||
// Memoize toast container props to prevent recreation
|
||||
const toastContainerProps = useMemo(
|
||||
() => ({
|
||||
position: 'bottom-left' as const,
|
||||
autoClose: 3000,
|
||||
hideProgressBar: false,
|
||||
newestOnTop: false,
|
||||
closeOnClick: true,
|
||||
rtl: false,
|
||||
pauseOnFocusLoss: true,
|
||||
draggable: false,
|
||||
pauseOnHover: false,
|
||||
transition: Zoom,
|
||||
closeButton: false,
|
||||
theme: 'dark' as const,
|
||||
toastStyle: {
|
||||
border: '1px solid #177ac9',
|
||||
width: 'fit-content'
|
||||
}
|
||||
}),
|
||||
[]
|
||||
);
|
||||
componentDidMount() {
|
||||
document.title = PROJECT_NAME;
|
||||
}
|
||||
|
||||
if (!wasLoaded) return null;
|
||||
onClickDismiss = (key: string | number | undefined) => () => {
|
||||
this.notistackRef.current.closeSnackbar(key);
|
||||
}
|
||||
|
||||
return (
|
||||
<TypesafeI18n locale={locale}>
|
||||
<CustomTheme>
|
||||
<AppRouting />
|
||||
<ToastContainer {...toastContainerProps} />
|
||||
</CustomTheme>
|
||||
</TypesafeI18n>
|
||||
);
|
||||
});
|
||||
render() {
|
||||
return (
|
||||
<CustomMuiTheme>
|
||||
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
ref={this.notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}>
|
||||
<FeaturesWrapper>
|
||||
<Switch>
|
||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
||||
<Route component={AppRouting} />
|
||||
</Switch>
|
||||
</FeaturesWrapper>
|
||||
</SnackbarProvider>
|
||||
</CustomMuiTheme>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
||||
@@ -1,60 +1,59 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
import React, { Component } from 'react';
|
||||
import { Switch, Redirect } from 'react-router';
|
||||
|
||||
import AuthenticatedRouting from 'AuthenticatedRouting';
|
||||
import SignIn from 'SignIn';
|
||||
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
||||
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import * as Authentication from './authentication/Authentication';
|
||||
import AuthenticationWrapper from './authentication/AuthenticationWrapper';
|
||||
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
|
||||
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
|
||||
|
||||
interface SecurityRedirectProps {
|
||||
message: string;
|
||||
signOut?: boolean;
|
||||
import SignIn from './SignIn';
|
||||
import ProjectRouting from './project/ProjectRouting';
|
||||
import NetworkConnection from './network/NetworkConnection';
|
||||
import AccessPoint from './ap/AccessPoint';
|
||||
import NetworkTime from './ntp/NetworkTime';
|
||||
import Security from './security/Security';
|
||||
import System from './system/System';
|
||||
|
||||
import { PROJECT_PATH } from './api';
|
||||
import Mqtt from './mqtt/Mqtt';
|
||||
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
||||
import { Features } from './features/types';
|
||||
|
||||
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/network/";
|
||||
class AppRouting extends Component<WithFeaturesProps> {
|
||||
|
||||
componentDidMount() {
|
||||
Authentication.clearLoginRedirect();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { features } = this.props;
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
<Switch>
|
||||
{features.security && (
|
||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||
)}
|
||||
{features.project && (
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
||||
)}
|
||||
<AuthenticatedRoute exact path="/network/*" component={NetworkConnection} />
|
||||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||
{features.ntp && (
|
||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||
)}
|
||||
{features.mqtt && (
|
||||
<AuthenticatedRoute exact path="/mqtt/*" component={Mqtt} />
|
||||
)}
|
||||
{features.security && (
|
||||
<AuthenticatedRoute exact path="/security/*" component={Security} />
|
||||
)}
|
||||
<AuthenticatedRoute exact path="/system/*" component={System} />
|
||||
<Redirect to={getDefaultRoute(features)} />
|
||||
</Switch>
|
||||
</AuthenticationWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const RootRedirect = ({ message, signOut }: SecurityRedirectProps) => {
|
||||
const authenticationContext = useContext(AuthenticationContext);
|
||||
useEffect(() => {
|
||||
signOut && authenticationContext.signOut(false);
|
||||
toast.success(message);
|
||||
}, [message, signOut, authenticationContext]);
|
||||
return <Navigate to="/" />;
|
||||
};
|
||||
|
||||
const AppRouting = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<Authentication>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/unauthorized"
|
||||
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
||||
/>
|
||||
<Route
|
||||
path="/fileUpdated"
|
||||
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RequireUnauthenticated>
|
||||
<SignIn />
|
||||
</RequireUnauthenticated>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<RequireAuthenticated>
|
||||
<AuthenticatedRouting />
|
||||
</RequireAuthenticated>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Authentication>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRouting;
|
||||
export default withFeatures(AppRouting);
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router';
|
||||
|
||||
import CustomEntities from 'app/main/CustomEntities';
|
||||
import Customizations from 'app/main/Customizations';
|
||||
import Dashboard from 'app/main/Dashboard';
|
||||
import Devices from 'app/main/Devices';
|
||||
import Help from 'app/main/Help';
|
||||
import Modules from 'app/main/Modules';
|
||||
import Scheduler from 'app/main/Scheduler';
|
||||
import Sensors from 'app/main/Sensors';
|
||||
import APSettings from 'app/settings/APSettings';
|
||||
import ApplicationSettings from 'app/settings/ApplicationSettings';
|
||||
import DownloadUpload from 'app/settings/DownloadUpload';
|
||||
import MqttSettings from 'app/settings/MqttSettings';
|
||||
import NTPSettings from 'app/settings/NTPSettings';
|
||||
import Settings from 'app/settings/Settings';
|
||||
import Network from 'app/settings/network/Network';
|
||||
import Security from 'app/settings/security/Security';
|
||||
import APStatus from 'app/status/APStatus';
|
||||
import Activity from 'app/status/Activity';
|
||||
import HardwareStatus from 'app/status/HardwareStatus';
|
||||
import MqttStatus from 'app/status/MqttStatus';
|
||||
import NTPStatus from 'app/status/NTPStatus';
|
||||
import NetworkStatus from 'app/status/NetworkStatus';
|
||||
import Status from 'app/status/Status';
|
||||
import SystemLog from 'app/status/SystemLog';
|
||||
import Version from 'app/status/Version';
|
||||
import { Layout } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
const AuthenticatedRouting = () => {
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route path="/devices/*" element={<Devices />} />
|
||||
<Route path="/sensors/*" element={<Sensors />} />
|
||||
<Route path="/help/*" element={<Help />} />
|
||||
|
||||
<Route path="/status/*" element={<Status />} />
|
||||
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
|
||||
<Route path="/status/activity" element={<Activity />} />
|
||||
<Route path="/status/log" element={<SystemLog />} />
|
||||
<Route path="/status/mqtt" element={<MqttStatus />} />
|
||||
<Route path="/status/ntp" element={<NTPStatus />} />
|
||||
<Route path="/status/ap" element={<APStatus />} />
|
||||
<Route path="/status/network" element={<NetworkStatus />} />
|
||||
<Route path="/status/version" element={<Version />} />
|
||||
|
||||
{me.admin && (
|
||||
<>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/settings/application" element={<ApplicationSettings />} />
|
||||
<Route path="/settings/mqtt" element={<MqttSettings />} />
|
||||
<Route path="/settings/ntp" element={<NTPSettings />} />
|
||||
<Route path="/settings/ap" element={<APSettings />} />
|
||||
<Route path="/settings/modules" element={<Modules />} />
|
||||
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
|
||||
|
||||
<Route path="/settings/network/*" element={<Network />} />
|
||||
<Route path="/settings/security/*" element={<Security />} />
|
||||
|
||||
<Route path="/customizations" element={<Customizations />} />
|
||||
<Route path="/scheduler" element={<Scheduler />} />
|
||||
<Route path="/customentities" element={<CustomEntities />} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Route path="/*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticatedRouting;
|
||||
44
interface/src/CustomMuiTheme.tsx
Normal file
44
interface/src/CustomMuiTheme.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { CssBaseline } from '@material-ui/core';
|
||||
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
|
||||
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
type: "dark",
|
||||
primary: {
|
||||
main: '#33bfff',
|
||||
},
|
||||
secondary: {
|
||||
main: '#3d5afe',
|
||||
},
|
||||
info: {
|
||||
main: blueGrey[500]
|
||||
},
|
||||
warning: {
|
||||
main: orange[500]
|
||||
},
|
||||
error: {
|
||||
main: red[500]
|
||||
},
|
||||
success: {
|
||||
main: green[500]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default class CustomMuiTheme extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StylesProvider>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{this.props.children}
|
||||
</MuiThemeProvider>
|
||||
</StylesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import {
|
||||
CssBaseline,
|
||||
ThemeProvider,
|
||||
responsiveFontSizes,
|
||||
tooltipClasses
|
||||
} from '@mui/material';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
import type { RequiredChildrenProps } from 'utils';
|
||||
|
||||
// Memoize dialog style to prevent recreation
|
||||
export const dialogStyle = {
|
||||
'& .MuiDialog-paper': {
|
||||
borderRadius: '8px',
|
||||
borderColor: '#565656',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px'
|
||||
}
|
||||
} as const;
|
||||
|
||||
// Memoize theme creation to prevent recreation
|
||||
const theme = responsiveFontSizes(
|
||||
createTheme({
|
||||
typography: {
|
||||
fontSize: 13
|
||||
},
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
secondary: {
|
||||
main: '#2196f3' // blue[500]
|
||||
},
|
||||
info: {
|
||||
main: '#607d8b' // blueGrey[500]
|
||||
},
|
||||
text: {
|
||||
disabled: '#eee' // white
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MuiListItemText: {
|
||||
styleOverrides: {
|
||||
primary: {
|
||||
fontSize: 14
|
||||
},
|
||||
secondary: {
|
||||
color: '#9e9e9e' // grey[500]
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiTooltip: {
|
||||
defaultProps: {
|
||||
placement: 'top',
|
||||
arrow: true
|
||||
},
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
padding: '4px 8px',
|
||||
fontSize: 10,
|
||||
color: 'rgba(0, 0, 0, 0.87)',
|
||||
backgroundColor: '#4caf50', // MUI success.main default color
|
||||
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
[`& .${tooltipClasses.arrow}`]: {
|
||||
color: '#4caf50'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const CustomTheme: FC<RequiredChildrenProps> = memo(({ children }) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
));
|
||||
|
||||
export default CustomTheme;
|
||||
@@ -1,149 +1,147 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import React, { Component } from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import ForwardIcon from '@mui/icons-material/Forward';
|
||||
import { Box, Button, Paper, Typography } from '@mui/material';
|
||||
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
|
||||
import { Paper, Typography, Fab } from '@material-ui/core';
|
||||
import ForwardIcon from '@material-ui/icons/Forward';
|
||||
|
||||
import * as AuthenticationApi from 'components/routing/authentication';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
LanguageSelector,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
} from 'components';
|
||||
import { AuthenticationContext } from 'contexts/authentication';
|
||||
import { PROJECT_NAME } from 'env';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SignInRequest } from 'types';
|
||||
import { onEnterCallback, updateValue } from 'utils';
|
||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
||||
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
||||
import {PasswordValidator} from './components';
|
||||
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||
|
||||
const SignIn = () => {
|
||||
const authenticationContext = useContext(AuthenticationContext);
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
signInPage: {
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
margin: "auto",
|
||||
padding: theme.spacing(2),
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
maxWidth: theme.breakpoints.values.sm
|
||||
},
|
||||
signInPanel: {
|
||||
textAlign: "center",
|
||||
padding: theme.spacing(2),
|
||||
paddingTop: "200px",
|
||||
backgroundImage: 'url("/app/icon.png")',
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
||||
backgroundSize: "auto 150px",
|
||||
width: "100%"
|
||||
},
|
||||
extendedIcon: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
}
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
||||
|
||||
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
interface SignInState {
|
||||
username: string,
|
||||
password: string,
|
||||
processing: boolean
|
||||
}
|
||||
|
||||
const { send: callSignIn } = useRequest(
|
||||
(request: SignInRequest) => AuthenticationApi.signIn(request),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
).onSuccess((response) => {
|
||||
if (response.data) {
|
||||
authenticationContext.signIn(response.data.access_token);
|
||||
}
|
||||
});
|
||||
class SignIn extends Component<SignInProps, SignInState> {
|
||||
|
||||
const updateLoginRequestValue = updateValue(setSignInRequest);
|
||||
constructor(props: SignInProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
processing: false
|
||||
};
|
||||
}
|
||||
|
||||
const signIn = async () => {
|
||||
await callSignIn(signInRequest).catch((event: Error) => {
|
||||
if (event.message === 'Unauthorized') {
|
||||
toast.warning(LL.INVALID_LOGIN());
|
||||
} else {
|
||||
toast.error(LL.ERROR() + ' ' + event.message);
|
||||
}
|
||||
setProcessing(false);
|
||||
});
|
||||
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const { name, value } = event.currentTarget;
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
[name]: value,
|
||||
}))
|
||||
};
|
||||
|
||||
const validateAndSignIn = async () => {
|
||||
setProcessing(true);
|
||||
SIGN_IN_REQUEST_VALIDATOR.messages({
|
||||
required: LL.IS_REQUIRED('%s')
|
||||
});
|
||||
try {
|
||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
||||
await signIn();
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
setProcessing(false);
|
||||
}
|
||||
onSubmit = () => {
|
||||
const { username, password } = this.state;
|
||||
const { authenticationContext } = this.props;
|
||||
this.setState({ processing: true });
|
||||
fetch(SIGN_IN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else if (response.status === 401) {
|
||||
throw Error("Invalid credentials.");
|
||||
} else {
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}
|
||||
}).then(json => {
|
||||
authenticationContext.signIn(json.access_token);
|
||||
})
|
||||
.catch(error => {
|
||||
this.props.enqueueSnackbar(error.message, {
|
||||
variant: 'warning',
|
||||
});
|
||||
this.setState({ processing: false });
|
||||
});
|
||||
};
|
||||
|
||||
const submitOnEnter = onEnterCallback(signIn);
|
||||
render() {
|
||||
const { username, password, processing } = this.state;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.signInPage}>
|
||||
<Paper className={classes.signInPanel}>
|
||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
<TextValidator
|
||||
disabled={processing}
|
||||
validators={['required']}
|
||||
errorMessages={['Username is required']}
|
||||
name="username"
|
||||
label="Username"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={username}
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
autoCapitalize: "none",
|
||||
autoCorrect: "off",
|
||||
}}
|
||||
/>
|
||||
<PasswordValidator
|
||||
disabled={processing}
|
||||
validators={['required']}
|
||||
errorMessages={['Password is required']}
|
||||
name="password"
|
||||
label="Password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={password}
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
/>
|
||||
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
||||
<ForwardIcon className={classes.extendedIcon} />
|
||||
Sign In
|
||||
</Fab>
|
||||
</ValidatorForm>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
height="100vh"
|
||||
margin="auto"
|
||||
padding={2}
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
maxWidth={(theme) => theme.breakpoints.values.sm}
|
||||
>
|
||||
<Paper
|
||||
sx={(theme) => ({
|
||||
textAlign: 'center',
|
||||
padding: theme.spacing(2),
|
||||
paddingTop: '172px',
|
||||
backgroundImage: 'url("/app/icon.png")',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50% ' + theme.spacing(2),
|
||||
width: '100%'
|
||||
})}
|
||||
>
|
||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||
}
|
||||
|
||||
<LanguageSelector />
|
||||
|
||||
<Box display="flex" flexDirection="column" alignItems="center">
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors || {}}
|
||||
disabled={processing}
|
||||
sx={{
|
||||
width: 240
|
||||
}}
|
||||
name="username"
|
||||
label={LL.USERNAME(0)}
|
||||
value={signInRequest.username}
|
||||
onChange={updateLoginRequestValue}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
slotProps={{
|
||||
input: {
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors || {}}
|
||||
disabled={processing}
|
||||
sx={{
|
||||
width: 240
|
||||
}}
|
||||
name="password"
|
||||
label={LL.PASSWORD()}
|
||||
value={signInRequest.password}
|
||||
onChange={updateLoginRequestValue}
|
||||
onKeyDown={submitOnEnter}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={validateAndSignIn}
|
||||
disabled={processing}
|
||||
>
|
||||
<ForwardIcon sx={{ mr: 1 }} />
|
||||
{LL.SIGN_IN()}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
|
||||
|
||||
5
interface/src/ap/APModes.ts
Normal file
5
interface/src/ap/APModes.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { APSettings, APProvisionMode } from "./types";
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
}
|
||||
30
interface/src/ap/APSettingsController.tsx
Normal file
30
interface/src/ap/APSettingsController.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { AP_SETTINGS_ENDPOINT } from '../api';
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
|
||||
import APSettingsForm from './APSettingsForm';
|
||||
import { APSettings } from './types';
|
||||
|
||||
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
||||
|
||||
class APSettingsController extends Component<APSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="Access Point Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <APSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
||||
106
interface/src/ap/APSettingsForm.tsx
Normal file
106
interface/src/ap/APSettingsForm.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
||||
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { PasswordValidator, RestFormProps, FormActions, FormButton } from '../components';
|
||||
|
||||
import { isAPEnabled } from './APModes';
|
||||
import { APSettings, APProvisionMode } from './types';
|
||||
import { isIP } from '../validators';
|
||||
|
||||
type APSettingsFormProps = RestFormProps<APSettings>;
|
||||
|
||||
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIP', isIP);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, handleValueChange, saveData } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
||||
<SelectValidator name="provision_mode"
|
||||
label="Provide Access Point…"
|
||||
value={data.provision_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('provision_mode')}
|
||||
margin="normal">
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When Network Disconnected</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||
</SelectValidator>
|
||||
{
|
||||
isAPEnabled(data) &&
|
||||
<Fragment>
|
||||
<TextValidator
|
||||
validators={['required', 'matchRegexp:^.{1,32}$']}
|
||||
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
|
||||
name="ssid"
|
||||
label="Access Point SSID"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.ssid}
|
||||
onChange={handleValueChange('ssid')}
|
||||
margin="normal"
|
||||
/>
|
||||
<PasswordValidator
|
||||
validators={['required', 'matchRegexp:^.{8,64}$']}
|
||||
errorMessages={['Access Point Password is required', 'Access Point Password must be 8-64 characters']}
|
||||
name="password"
|
||||
label="Access Point Password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={handleValueChange('password')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
errorMessages={['Local IP is required', 'Must be an IP address']}
|
||||
name="local_ip"
|
||||
label="Local IP"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.local_ip}
|
||||
onChange={handleValueChange('local_ip')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
errorMessages={['Gateway IP is required', 'Must be an IP address']}
|
||||
name="gateway_ip"
|
||||
label="Gateway"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.gateway_ip}
|
||||
onChange={handleValueChange('gateway_ip')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
||||
name="subnet_mask"
|
||||
label="Subnet"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.subnet_mask}
|
||||
onChange={handleValueChange('subnet_mask')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default APSettingsForm;
|
||||
28
interface/src/ap/APStatus.ts
Normal file
28
interface/src/ap/APStatus.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { APStatus, APNetworkStatus } from "./types";
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return theme.palette.info.main;
|
||||
case APNetworkStatus.LINGERING:
|
||||
return theme.palette.warning.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
}
|
||||
|
||||
export const apStatus = ({ status }: APStatus) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return "Active";
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return "Inactive";
|
||||
case APNetworkStatus.LINGERING:
|
||||
return "Lingering until idle";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
29
interface/src/ap/APStatusController.tsx
Normal file
29
interface/src/ap/APStatusController.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import { AP_STATUS_ENDPOINT } from '../api';
|
||||
|
||||
import APStatusForm from './APStatusForm';
|
||||
import { APStatus } from './types';
|
||||
|
||||
type APStatusControllerProps = RestControllerProps<APStatus>;
|
||||
|
||||
class APStatusController extends Component<APStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="Access Point Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <APStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default restController(AP_STATUS_ENDPOINT, APStatusController);
|
||||
78
interface/src/ap/APStatusForm.tsx
Normal file
78
interface/src/ap/APStatusForm.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||
|
||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
import ComputerIcon from '@material-ui/icons/Computer';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
||||
import { apStatusHighlight, apStatus } from './APStatus';
|
||||
import { APStatus } from './types';
|
||||
|
||||
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
||||
|
||||
class APStatusForm extends Component<APStatusFormProps> {
|
||||
|
||||
createListItems() {
|
||||
const { data, theme } = this.props
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<HighlightAvatar color={apStatusHighlight(data, theme)}>
|
||||
<SettingsInputAntennaIcon />
|
||||
</HighlightAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={apStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="IP Address" secondary={data.ip_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ComputerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="AP Clients" secondary={data.station_num} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withTheme(APStatusForm);
|
||||
38
interface/src/ap/AccessPoint.tsx
Normal file
38
interface/src/ap/AccessPoint.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import APSettingsController from './APSettingsController';
|
||||
import APStatusController from './APStatusController';
|
||||
|
||||
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class AccessPoint extends Component<AccessPointProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Access Point">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value="/ap/status" label="Access Point Status" />
|
||||
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
|
||||
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
|
||||
<Redirect to="/ap/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(AccessPoint);
|
||||
@@ -10,20 +10,17 @@ export enum APNetworkStatus {
|
||||
LINGERING = 2
|
||||
}
|
||||
|
||||
export interface APStatusType {
|
||||
export interface APStatus {
|
||||
status: APNetworkStatus;
|
||||
ip_address: string;
|
||||
mac_address: string;
|
||||
station_num: number;
|
||||
}
|
||||
|
||||
export interface APSettingsType {
|
||||
export interface APSettings {
|
||||
provision_mode: APProvisionMode;
|
||||
ssid: string;
|
||||
password: string;
|
||||
channel: number;
|
||||
ssid_hidden: boolean;
|
||||
max_clients: number;
|
||||
local_ip: string;
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
23
interface/src/api/Endpoints.ts
Normal file
23
interface/src/api/Endpoints.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ENDPOINT_ROOT } from './Env';
|
||||
|
||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
|
||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
|
||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
|
||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
|
||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "networkSettings";
|
||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + "networkStatus";
|
||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
|
||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
|
||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
|
||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
|
||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
|
||||
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
|
||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
||||
24
interface/src/api/Env.ts
Normal file
24
interface/src/api/Env.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||
|
||||
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
|
||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");
|
||||
|
||||
function calculateEndpointRoot(endpointPath: string) {
|
||||
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
||||
if (httpRoot) {
|
||||
return httpRoot + endpointPath;
|
||||
}
|
||||
const location = window.location;
|
||||
return location.protocol + "//" + location.host + endpointPath;
|
||||
}
|
||||
|
||||
function calculateWebSocketRoot(webSocketPath: string) {
|
||||
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
|
||||
if (webSocketRoot) {
|
||||
return webSocketRoot + webSocketPath;
|
||||
}
|
||||
const location = window.location;
|
||||
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return webProtocol + "//" + location.host + webSocketPath;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { APSettingsType, APStatusType } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
|
||||
export const readAPSettings = () =>
|
||||
alovaInstance.Get<APSettingsType>('/rest/apSettings');
|
||||
export const updateAPSettings = (data: APSettingsType) =>
|
||||
alovaInstance.Post<APSettingsType>('/rest/apSettings', data);
|
||||
@@ -1,155 +0,0 @@
|
||||
import { alovaInstance } from 'api/endpoints';
|
||||
|
||||
import type {
|
||||
APIcall,
|
||||
Action,
|
||||
Activity,
|
||||
CoreData,
|
||||
DashboardData,
|
||||
DeviceData,
|
||||
DeviceEntity,
|
||||
Entities,
|
||||
EntityItem,
|
||||
ModuleItem,
|
||||
Modules,
|
||||
Schedule,
|
||||
ScheduleItem,
|
||||
SensorData,
|
||||
Settings,
|
||||
WriteAnalogSensor,
|
||||
WriteTemperatureSensor
|
||||
} from '../app/main/types';
|
||||
|
||||
// Dashboard
|
||||
export const readDashboard = () =>
|
||||
alovaInstance.Get<DashboardData>('/rest/dashboardData', {
|
||||
responseType: 'arraybuffer' // uses msgpack
|
||||
});
|
||||
|
||||
// Devices
|
||||
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
|
||||
export const readDeviceData = (id: number) =>
|
||||
alovaInstance.Get<DeviceData>('/rest/deviceData', {
|
||||
// alovaInstance.Get<DeviceData>(`/rest/deviceData/${id}`, {
|
||||
params: { id },
|
||||
responseType: 'arraybuffer' // uses msgpack
|
||||
});
|
||||
export const writeDeviceValue = (data: { id: number; c: string; v: unknown }) =>
|
||||
alovaInstance.Post('/rest/writeDeviceValue', data);
|
||||
|
||||
// Application Settings
|
||||
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
|
||||
export const writeSettings = (data: Settings) =>
|
||||
alovaInstance.Post('/rest/settings', data);
|
||||
export const getBoardProfile = (boardProfile: string) =>
|
||||
alovaInstance.Get('/rest/boardProfile', {
|
||||
params: { boardProfile }
|
||||
});
|
||||
|
||||
// Sensors
|
||||
export const readSensorData = () =>
|
||||
alovaInstance.Get<SensorData>('/rest/sensorData');
|
||||
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
|
||||
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
|
||||
export const writeAnalogSensor = (as: WriteAnalogSensor) =>
|
||||
alovaInstance.Post('/rest/writeAnalogSensor', as);
|
||||
|
||||
// Activity
|
||||
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
|
||||
|
||||
// API
|
||||
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
|
||||
|
||||
// Generic action
|
||||
export const callAction = (action: Action) =>
|
||||
alovaInstance.Post('/rest/action', action);
|
||||
|
||||
// SettingsCustomization
|
||||
export const readDeviceEntities = (id: number) =>
|
||||
// alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities/${id}`, {
|
||||
alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities`, {
|
||||
params: { id },
|
||||
responseType: 'arraybuffer',
|
||||
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
|
||||
transform(data) {
|
||||
return (data as DeviceEntity[]).map((de: DeviceEntity) => ({
|
||||
...de,
|
||||
o_m: de.m,
|
||||
o_cn: de.cn,
|
||||
o_mi: de.mi,
|
||||
o_ma: de.ma
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const resetCustomizations = () =>
|
||||
alovaInstance.Post('/rest/resetCustomizations');
|
||||
export const writeCustomizationEntities = (data: {
|
||||
id: number;
|
||||
entity_ids: string[];
|
||||
}) => alovaInstance.Post('/rest/customizationEntities', data);
|
||||
export const writeDeviceName = (data: { id: number; name: string }) =>
|
||||
alovaInstance.Post('/rest/writeDeviceName', data);
|
||||
|
||||
// SettingsScheduler
|
||||
export const readSchedule = () =>
|
||||
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
|
||||
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
|
||||
transform(data) {
|
||||
return (data as Schedule).schedule.map((si: ScheduleItem) => ({
|
||||
...si,
|
||||
o_id: si.id,
|
||||
o_active: si.active,
|
||||
o_deleted: si.deleted,
|
||||
o_flags: si.flags,
|
||||
o_time: si.time,
|
||||
o_cmd: si.cmd,
|
||||
o_value: si.value,
|
||||
o_name: si.name
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeSchedule = (data: Schedule) =>
|
||||
alovaInstance.Post('/rest/schedule', data);
|
||||
|
||||
// Modules
|
||||
export const readModules = () =>
|
||||
alovaInstance.Get<ModuleItem[]>('/rest/modules', {
|
||||
transform(data) {
|
||||
return (data as Modules).modules.map((mi: ModuleItem) => ({
|
||||
...mi,
|
||||
o_enabled: mi.enabled,
|
||||
o_license: mi.license
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeModules = (data: {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
license: string;
|
||||
}) => alovaInstance.Post('/rest/modules', data);
|
||||
|
||||
// CustomEntities
|
||||
export const readCustomEntities = () =>
|
||||
alovaInstance.Get<EntityItem[]>('/rest/customEntities', {
|
||||
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
|
||||
transform(data) {
|
||||
return (data as Entities).entities.map((ei: EntityItem) => ({
|
||||
...ei,
|
||||
o_id: ei.id,
|
||||
o_ram: ei.ram,
|
||||
o_device_id: ei.device_id,
|
||||
o_type_id: ei.type_id,
|
||||
o_offset: ei.offset,
|
||||
o_factor: ei.factor,
|
||||
o_uom: ei.uom,
|
||||
o_value_type: ei.value_type,
|
||||
o_name: ei.name,
|
||||
o_writeable: ei.writeable,
|
||||
o_value: ei.value,
|
||||
o_deleted: ei.deleted,
|
||||
o_hide: ei.hide
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeCustomEntities = (data: Entities) =>
|
||||
alovaInstance.Post('/rest/customEntities', data);
|
||||
@@ -1,66 +0,0 @@
|
||||
import { type AlovaXHRResponse, xhrRequestAdapter } from '@alova/adapter-xhr';
|
||||
import { createAlova } from 'alova';
|
||||
import ReactHook from 'alova/react';
|
||||
|
||||
import { unpack } from './unpack';
|
||||
|
||||
export const ACCESS_TOKEN = 'access_token';
|
||||
|
||||
export const alovaInstance = createAlova({
|
||||
statesHook: ReactHook,
|
||||
// timeout: 3000, // 3 seconds before throwing a timeout error, default is 0 = none
|
||||
cacheFor: null, // disable cache
|
||||
// cacheFor: {
|
||||
// GET: {
|
||||
// mode: 'memory',
|
||||
// expire: 60 * 10 * 1000 // 60 seconds in cache
|
||||
// }
|
||||
// },
|
||||
requestAdapter: xhrRequestAdapter(),
|
||||
beforeRequest(method) {
|
||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||
method.config.headers.Authorization =
|
||||
'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||
}
|
||||
// for simulating very slow networks
|
||||
// return new Promise((resolve) => {
|
||||
// const random = 3000 + Math.random() * 2000;
|
||||
// setTimeout(resolve, Math.floor(random));
|
||||
// });
|
||||
},
|
||||
|
||||
responded: {
|
||||
onSuccess: async (response: AlovaXHRResponse) => {
|
||||
// if (response.status === 202) {
|
||||
// throw new Error('Wait'); // wifi scan in progress
|
||||
// } else
|
||||
if (response.status === 205) {
|
||||
throw new Error('Reboot required');
|
||||
} else if (response.status === 400) {
|
||||
throw new Error('Request Failed');
|
||||
} else if (response.status >= 400) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const data: ArrayBuffer = (await response.data) as ArrayBuffer;
|
||||
if (response.data instanceof ArrayBuffer) {
|
||||
return unpack(data) as ArrayBuffer;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Interceptor for request failure. This interceptor will be entered when the request is wrong.
|
||||
// http errors like 401 (unauthorized) are handled either in the methods or AuthenticatedRouting()
|
||||
// onError: (error, method) => {
|
||||
// alert(error.message);
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
export const alovaInstanceGH = createAlova({
|
||||
baseURL:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '/gh'
|
||||
: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
|
||||
statesHook: ReactHook,
|
||||
requestAdapter: xhrRequestAdapter()
|
||||
});
|
||||
2
interface/src/api/index.ts
Normal file
2
interface/src/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './Env'
|
||||
export * from './Endpoints'
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { MqttSettingsType, MqttStatusType } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
export const readMqttStatus = () =>
|
||||
alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
|
||||
export const readMqttSettings = () =>
|
||||
alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
|
||||
export const updateMqttSettings = (data: MqttSettingsType) =>
|
||||
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { NetworkSettingsType, NetworkStatusType, WiFiNetworkList } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
export const readNetworkStatus = () =>
|
||||
alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
|
||||
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
||||
export const listNetworks = () =>
|
||||
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
||||
timeout: 20000 // 20 seconds
|
||||
});
|
||||
export const readNetworkSettings = () =>
|
||||
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings');
|
||||
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
|
||||
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { NTPSettingsType, NTPStatusType, Time } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
export const readNTPStatus = () =>
|
||||
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
|
||||
|
||||
export const readNTPSettings = () =>
|
||||
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {});
|
||||
export const updateNTPSettings = (data: NTPSettingsType) =>
|
||||
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);
|
||||
|
||||
export const updateTime = (data: Time) =>
|
||||
alovaInstance.Post<Time>('/rest/time', data);
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { SecuritySettingsType, Token } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
export const readSecuritySettings = () =>
|
||||
alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
|
||||
|
||||
export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
|
||||
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
||||
|
||||
export const generateToken = (username?: string) =>
|
||||
alovaInstance.Get<Token>('/rest/generateToken', {
|
||||
params: { username }
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { LogSettings, SystemStatus } from 'types';
|
||||
|
||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
||||
|
||||
// systemStatus - also used to ping in System Monitor for pinging
|
||||
export const readSystemStatus = () =>
|
||||
alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
||||
|
||||
// SystemLog
|
||||
export const readLogSettings = () =>
|
||||
alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
||||
export const updateLogSettings = (data: LogSettings) =>
|
||||
alovaInstance.Post('/rest/logSettings', data);
|
||||
export const fetchLogES = () => alovaInstance.Get('/es/log');
|
||||
|
||||
// Get versions from GitHub
|
||||
// cache for 10 minutes to stop getting the IP blocked by GitHub
|
||||
export const getStableVersion = () =>
|
||||
alovaInstanceGH.Get('latest', {
|
||||
cacheFor: 60 * 10 * 1000,
|
||||
transform(response: { data: { name: string; published_at: string } }) {
|
||||
return {
|
||||
name: response.data.name.substring(1),
|
||||
published_at: response.data.published_at
|
||||
};
|
||||
}
|
||||
});
|
||||
export const getDevVersion = () =>
|
||||
alovaInstanceGH.Get('tags/latest', {
|
||||
cacheFor: 60 * 10 * 1000,
|
||||
transform(response: { data: { name: string; published_at: string } }) {
|
||||
return {
|
||||
name: response.data.name.split(/\s+/).splice(-1)[0]?.substring(1) || '',
|
||||
published_at: response.data.published_at
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const uploadFile = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return alovaInstance.Post('/rest/uploadFile', formData, {
|
||||
timeout: 60000 // override timeout for uploading firmware - 1 minute
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,368 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useBlocker } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { updateState, useRequest } from 'alova/client';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
import { readCustomEntities, writeCustomEntities } from '../../api/app';
|
||||
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
|
||||
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { Entities, EntityItem } from './types';
|
||||
import { entityItemValidation } from './validators';
|
||||
|
||||
const CustomEntities = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useLayoutTitle(LL.CUSTOM_ENTITIES(0));
|
||||
|
||||
const {
|
||||
data: entities,
|
||||
send: fetchEntities,
|
||||
error
|
||||
} = useRequest(readCustomEntities, {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
useInterval(() => {
|
||||
if (!dialogOpen && !numChanges) {
|
||||
void fetchEntities();
|
||||
}
|
||||
});
|
||||
|
||||
const { send: writeEntities } = useRequest(
|
||||
(data: Entities) => writeCustomEntities(data),
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
function hasEntityChanged(ei: EntityItem) {
|
||||
return (
|
||||
ei.id !== ei.o_id ||
|
||||
ei.ram !== ei.o_ram ||
|
||||
(ei?.name || '') !== (ei?.o_name || '') ||
|
||||
ei.device_id !== ei.o_device_id ||
|
||||
ei.type_id !== ei.o_type_id ||
|
||||
ei.offset !== ei.o_offset ||
|
||||
ei.uom !== ei.o_uom ||
|
||||
ei.factor !== ei.o_factor ||
|
||||
ei.value_type !== ei.o_value_type ||
|
||||
ei.writeable !== ei.o_writeable ||
|
||||
ei.hide !== ei.o_hide ||
|
||||
ei.deleted !== ei.o_deleted ||
|
||||
(ei.value || '') !== (ei.o_value || '')
|
||||
);
|
||||
}
|
||||
|
||||
const entity_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
.td {
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(1) {
|
||||
padding: 8px;
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-of-type(5) {
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-of-type(6) {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
.th {
|
||||
border-bottom: 1px solid #565656;
|
||||
height: 36px;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
background-color: #1e1e1e;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const saveEntities = async () => {
|
||||
await writeEntities({
|
||||
entities: entities
|
||||
.filter((ei: EntityItem) => !ei.deleted)
|
||||
.map((condensed_ei: EntityItem) => ({
|
||||
id: condensed_ei.id,
|
||||
ram: condensed_ei.ram,
|
||||
name: condensed_ei.name,
|
||||
device_id: condensed_ei.device_id,
|
||||
type_id: condensed_ei.type_id,
|
||||
offset: condensed_ei.offset,
|
||||
factor: condensed_ei.factor,
|
||||
uom: condensed_ei.uom,
|
||||
writeable: condensed_ei.writeable,
|
||||
hide: condensed_ei.hide,
|
||||
value_type: condensed_ei.value_type,
|
||||
value: condensed_ei.value
|
||||
}))
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.ENTITIES_UPDATED());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
await fetchEntities();
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
const editEntityItem = useCallback((ei: EntityItem) => {
|
||||
setCreating(false);
|
||||
setSelectedEntityItem(ei);
|
||||
setDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const onDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const onDialogCancel = async () => {
|
||||
await fetchEntities().then(() => {
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
const onDialogSave = (updatedItem: EntityItem) => {
|
||||
setDialogOpen(false);
|
||||
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
||||
const new_data = creating
|
||||
? [
|
||||
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
||||
updatedItem
|
||||
]
|
||||
: data.map((ei) =>
|
||||
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
|
||||
);
|
||||
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
||||
return new_data;
|
||||
});
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
setCreating(true);
|
||||
setSelectedEntityItem({
|
||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||
name: '',
|
||||
ram: 0,
|
||||
device_id: '0',
|
||||
type_id: '0',
|
||||
offset: 0,
|
||||
factor: 1,
|
||||
uom: 0,
|
||||
value_type: 0,
|
||||
writeable: false,
|
||||
deleted: false,
|
||||
hide: false,
|
||||
value: ''
|
||||
});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
function formatValue(value: unknown, uom: number) {
|
||||
return value === undefined
|
||||
? ''
|
||||
: typeof value === 'number'
|
||||
? new Intl.NumberFormat().format(value) +
|
||||
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
||||
: (value as string) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]);
|
||||
}
|
||||
|
||||
function showHex(value: number, digit: number) {
|
||||
return '0x' + value.toString(16).toUpperCase().padStart(digit, '0');
|
||||
}
|
||||
|
||||
const renderEntity = () => {
|
||||
if (!entities) {
|
||||
return (
|
||||
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={{
|
||||
nodes: entities
|
||||
.filter((ei: EntityItem) => !ei.deleted)
|
||||
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name))
|
||||
}}
|
||||
theme={entity_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: EntityItem[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell>{LL.NAME(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.ID_OF(LL.DEVICE())}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.ID_OF(LL.TYPE(1))}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.OFFSET()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((ei: EntityItem) => (
|
||||
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
||||
<Cell>
|
||||
{ei.name}
|
||||
{ei.writeable && (
|
||||
<EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}
|
||||
</Cell>
|
||||
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
||||
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
|
||||
<Cell>
|
||||
{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}
|
||||
</Cell>
|
||||
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body1">{LL.ENTITIES_HELP_1()}.</Typography>
|
||||
</Box>
|
||||
|
||||
{renderEntity()}
|
||||
|
||||
{selectedEntityItem && (
|
||||
<SettingsCustomEntitiesDialog
|
||||
open={dialogOpen}
|
||||
creating={creating}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
onDup={onDialogDup}
|
||||
selectedItem={selectedEntityItem}
|
||||
validator={entityItemValidation(entities, selectedEntityItem)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box mt={2} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges > 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onDialogCancel}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
color="info"
|
||||
onClick={saveEntities}
|
||||
>
|
||||
{LL.APPLY_CHANGES(numChanges)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={addEntityItem}
|
||||
>
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEntities;
|
||||
@@ -1,434 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||
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 {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { EntityItem } from './types';
|
||||
|
||||
interface CustomEntitiesDialogProps {
|
||||
open: boolean;
|
||||
creating: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ei: EntityItem) => void;
|
||||
onDup: (ei: EntityItem) => void;
|
||||
selectedItem: EntityItem;
|
||||
validator: Schema;
|
||||
}
|
||||
|
||||
const CustomEntitiesDialog = ({
|
||||
open,
|
||||
creating,
|
||||
onClose,
|
||||
onSave,
|
||||
onDup,
|
||||
selectedItem,
|
||||
validator
|
||||
}: CustomEntitiesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
setEditItem(selectedItem);
|
||||
// convert to hex strings straight away
|
||||
setEditItem({
|
||||
...selectedItem,
|
||||
device_id: selectedItem.device_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]);
|
||||
|
||||
const handleClose = (
|
||||
_event: React.SyntheticEvent,
|
||||
reason: 'backdropClick' | 'escapeKeyDown'
|
||||
) => {
|
||||
if (reason !== 'backdropClick') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
if (typeof editItem.device_id === 'string') {
|
||||
editItem.device_id = parseInt(editItem.device_id, 16);
|
||||
}
|
||||
if (typeof editItem.type_id === 'string') {
|
||||
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);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
editItem.deleted = true;
|
||||
onSave(editItem);
|
||||
};
|
||||
|
||||
const dup = () => {
|
||||
onDup(editItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<DialogTitle>
|
||||
{creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.ENTITY()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box display="flex" flexWrap="wrap" mb={1}>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap" />
|
||||
</Box>
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid size={12}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors || {}}
|
||||
name="name"
|
||||
label={LL.NAME(0)}
|
||||
value={editItem.name}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</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>
|
||||
<TextField
|
||||
name="ram"
|
||||
label={LL.VALUE(0) + ' ' + LL.TYPE(1)}
|
||||
value={editItem.ram}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>EMS-{LL.VALUE(1)}</MenuItem>
|
||||
<MenuItem value={1}>RAM-{LL.VALUE(1)}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{editItem.ram === 1 && (
|
||||
<>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.DEFAULT(0) + ' ' + LL.VALUE(0)}
|
||||
type="string"
|
||||
value={editItem.value as string}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
</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 && (
|
||||
<>
|
||||
<Grid mt={3}>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
icon={<EditOffOutlinedIcon color="primary" />}
|
||||
checkedIcon={<EditOutlinedIcon htmlColor="white" />}
|
||||
checked={editItem.writeable}
|
||||
onChange={updateFormValue}
|
||||
name="writeable"
|
||||
/>
|
||||
}
|
||||
label={LL.WRITEABLE()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors || {}}
|
||||
name="device_id"
|
||||
label={LL.ID_OF(LL.DEVICE())}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
type="string"
|
||||
value={editItem.device_id as string}
|
||||
onChange={updateFormValue}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">0x</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { style: { textTransform: 'uppercase' } }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors || {}}
|
||||
name="type_id"
|
||||
label={LL.ID_OF(LL.TYPE(1))}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
type="string"
|
||||
value={editItem.type_id as string}
|
||||
onChange={updateFormValue}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">0x</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { style: { textTransform: 'uppercase' } }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors || {}}
|
||||
name="offset"
|
||||
label={LL.OFFSET()}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
value={numberValue(editItem.offset)}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="value_type"
|
||||
label={LL.VALUE(0) + ' ' + LL.TYPE(1)}
|
||||
value={editItem.value_type}
|
||||
variant="outlined"
|
||||
sx={{ width: '11ch' }}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={DeviceValueType.BOOL}>
|
||||
{DeviceValueTypeNames[DeviceValueType.BOOL]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.INT8}>
|
||||
{DeviceValueTypeNames[DeviceValueType.INT8]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT8}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT8]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.INT16}>
|
||||
{DeviceValueTypeNames[DeviceValueType.INT16]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT16}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT16]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT24}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT24]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.TIME}>
|
||||
{DeviceValueTypeNames[DeviceValueType.TIME]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT32}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT32]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.STRING}>
|
||||
{DeviceValueTypeNames[DeviceValueType.STRING]}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
{editItem.value_type !== DeviceValueType.BOOL &&
|
||||
editItem.value_type !== DeviceValueType.STRING && (
|
||||
<>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="factor"
|
||||
label={LL.FACTOR()}
|
||||
value={numberValue(editItem.factor as number)}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
sx={{ width: '11ch' }}
|
||||
margin="normal"
|
||||
type="number"
|
||||
slotProps={{
|
||||
htmlInput: { step: '0.001' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="uom"
|
||||
label={LL.UNIT()}
|
||||
value={editItem.uom}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
onChange={updateFormValue}
|
||||
select
|
||||
>
|
||||
{DeviceValueUOM_s.map((val, i) => (
|
||||
<MenuItem key={val} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{editItem.value_type === DeviceValueType.STRING &&
|
||||
editItem.device_id !== '0' && (
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors || {}}
|
||||
name="factor"
|
||||
label={LL.BYTES()}
|
||||
value={numberValue(editItem.factor as number)}
|
||||
sx={{ width: '11ch' }}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
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>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!creating && (
|
||||
<Box flexGrow={1}>
|
||||
<Button
|
||||
startIcon={<RemoveIcon />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={remove}
|
||||
>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 1 }}
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={dup}
|
||||
>
|
||||
{LL.DUPLICATE()}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={creating ? <AddIcon /> : <DoneIcon />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEntitiesDialog;
|
||||
@@ -1,771 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useBlocker, useLocation } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
Link,
|
||||
MenuItem,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import SystemMonitor from 'app/status/SystemMonitor';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import {
|
||||
API,
|
||||
readCoreData,
|
||||
readDeviceEntities,
|
||||
resetCustomizations,
|
||||
writeCustomizationEntities,
|
||||
writeDeviceName
|
||||
} from '../../api/app';
|
||||
import SettingsCustomizationsDialog from './CustomizationsDialog';
|
||||
import EntityMaskToggle from './EntityMaskToggle';
|
||||
import OptionIcon from './OptionIcon';
|
||||
import { DeviceEntityMask } from './types';
|
||||
import type { APIcall, Device, DeviceEntity } from './types';
|
||||
|
||||
export const APIURL = window.location.origin + '/api/';
|
||||
|
||||
const Customizations = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
|
||||
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
||||
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [rename, setRename] = useState<boolean>(false);
|
||||
|
||||
useLayoutTitle(LL.CUSTOMIZATIONS());
|
||||
|
||||
// fetch devices first from coreData
|
||||
const { data: devices, send: fetchCoreData } = useRequest(readCoreData);
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>(
|
||||
Number(useLocation().state) || -1
|
||||
);
|
||||
const [selectedDeviceTypeNameURL, setSelectedDeviceTypeNameURL] =
|
||||
useState<string>(''); // needed for API URL
|
||||
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
|
||||
|
||||
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: sendDeviceName } = useRequest(
|
||||
(data: { id: number; name: string }) => writeDeviceName(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const { send: sendCustomizationEntities } = useRequest(
|
||||
(data: { id: number; entity_ids: string[] }) => writeCustomizationEntities(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const { send: sendDeviceEntities } = useRequest(
|
||||
(data: number) => readDeviceEntities(data),
|
||||
{
|
||||
initialData: [],
|
||||
immediate: false
|
||||
}
|
||||
).onSuccess((event) => {
|
||||
setOriginalSettings(event.data);
|
||||
});
|
||||
|
||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||
setDeviceEntities(
|
||||
data.map((de) => {
|
||||
const result: DeviceEntity = {
|
||||
...de,
|
||||
o_m: de.m
|
||||
};
|
||||
if (de.cn !== undefined) {
|
||||
result.o_cn = de.cn;
|
||||
}
|
||||
if (de.mi !== undefined) {
|
||||
result.o_mi = de.mi;
|
||||
}
|
||||
if (de.ma !== undefined) {
|
||||
result.o_ma = de.ma;
|
||||
}
|
||||
return result;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const doRestart = async () => {
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const entities_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
.td {
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(3) {
|
||||
text-align: right;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
text-align: right;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
.th {
|
||||
border-bottom: 1px solid #565656;
|
||||
height: 36px;
|
||||
}
|
||||
&:nth-of-type(1) .th {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
background-color: #1e1e1e;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&.tr.tr-body.row-select.row-select-single-selected {
|
||||
background-color: #3d4752;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`,
|
||||
Cell: `
|
||||
&:nth-of-type(2) {
|
||||
padding: 8px;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
&:last-of-type {
|
||||
padding-right: 8px;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
function hasEntityChanged(de: DeviceEntity) {
|
||||
return (
|
||||
(de?.cn || '') !== (de?.o_cn || '') ||
|
||||
de.m !== de.o_m ||
|
||||
de.ma !== de.o_ma ||
|
||||
de.mi !== de.o_mi
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (deviceEntities.length) {
|
||||
setNumChanges(
|
||||
deviceEntities
|
||||
.filter((de) => hasEntityChanged(de))
|
||||
.map(
|
||||
(new_de) =>
|
||||
new_de.m.toString(16).padStart(2, '0') +
|
||||
new_de.id +
|
||||
(new_de.cn || new_de.mi || new_de.ma ? '|' : '') +
|
||||
(new_de.cn ? new_de.cn : '') +
|
||||
(new_de.mi ? '>' + new_de.mi : '') +
|
||||
(new_de.ma ? '<' + new_de.ma : '')
|
||||
).length
|
||||
);
|
||||
}
|
||||
}, [deviceEntities]);
|
||||
|
||||
useEffect(() => {
|
||||
if (devices && selectedDevice !== -1) {
|
||||
void sendDeviceEntities(selectedDevice);
|
||||
const index = devices.devices.findIndex((d) => d.id === selectedDevice);
|
||||
if (index === -1) {
|
||||
setSelectedDevice(-1);
|
||||
setSelectedDeviceTypeNameURL('');
|
||||
} else {
|
||||
const device = devices.devices[index];
|
||||
if (device) {
|
||||
setSelectedDeviceTypeNameURL(device.url || '');
|
||||
setSelectedDeviceName(device.n);
|
||||
}
|
||||
setNumChanges(0);
|
||||
setRestartNeeded(false);
|
||||
}
|
||||
}
|
||||
}, [devices, selectedDevice]);
|
||||
|
||||
function formatValue(value: unknown) {
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat().format(value);
|
||||
} else if (value === undefined) {
|
||||
return '';
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value ? 'true' : 'false';
|
||||
}
|
||||
return value as string;
|
||||
}
|
||||
|
||||
const formatName = (de: DeviceEntity, withShortname: boolean) =>
|
||||
(de.n && de.n[0] === '!'
|
||||
? de.t
|
||||
? LL.COMMAND(1) + ': ' + de.t + ' ' + de.n.slice(1)
|
||||
: LL.COMMAND(1) + ': ' + de.n.slice(1)
|
||||
: de.cn && de.cn !== ''
|
||||
? de.t
|
||||
? de.t + ' ' + de.cn
|
||||
: de.cn
|
||||
: de.t
|
||||
? de.t + ' ' + de.n
|
||||
: de.n) + (withShortname ? ' ' + de.id : '');
|
||||
|
||||
const getMaskNumber = (newMask: string[]) => {
|
||||
let new_mask = 0;
|
||||
for (const entry of newMask) {
|
||||
new_mask |= Number(entry);
|
||||
}
|
||||
return new_mask;
|
||||
};
|
||||
|
||||
const getMaskString = (m: number) => {
|
||||
const new_masks: string[] = [];
|
||||
if ((m & 1) === 1) {
|
||||
new_masks.push('1');
|
||||
}
|
||||
if ((m & 2) === 2) {
|
||||
new_masks.push('2');
|
||||
}
|
||||
if ((m & 4) === 4) {
|
||||
new_masks.push('4');
|
||||
}
|
||||
if ((m & 8) === 8) {
|
||||
new_masks.push('8');
|
||||
}
|
||||
if ((m & 128) === 128) {
|
||||
new_masks.push('128');
|
||||
}
|
||||
return new_masks;
|
||||
};
|
||||
|
||||
const filter_entity = (de: DeviceEntity) =>
|
||||
(de.m & selectedFilters || !selectedFilters) &&
|
||||
formatName(de, true).toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
const maskDisabled = (set: boolean) => {
|
||||
setDeviceEntities(
|
||||
deviceEntities.map(function (de) {
|
||||
if (filter_entity(de)) {
|
||||
return {
|
||||
...de,
|
||||
m: set
|
||||
? de.m |
|
||||
(DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||
DeviceEntityMask.DV_WEB_EXCLUDE)
|
||||
: de.m &
|
||||
~(
|
||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||
DeviceEntityMask.DV_WEB_EXCLUDE
|
||||
)
|
||||
};
|
||||
} else {
|
||||
return de;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const resetCustomization = async () => {
|
||||
try {
|
||||
await sendResetCustomizations();
|
||||
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message);
|
||||
} finally {
|
||||
setConfirmReset(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||
setDeviceEntities(
|
||||
deviceEntities?.map((de) =>
|
||||
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onDialogSave = (updatedItem: DeviceEntity) => {
|
||||
setDialogOpen(false);
|
||||
updateDeviceEntity(updatedItem);
|
||||
};
|
||||
|
||||
const editDeviceEntity = useCallback((de: DeviceEntity) => {
|
||||
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (de.cn === undefined) {
|
||||
de.cn = '';
|
||||
}
|
||||
|
||||
setSelectedDeviceEntity(de);
|
||||
setDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const saveCustomization = async () => {
|
||||
if (devices && deviceEntities && selectedDevice !== -1) {
|
||||
const masked_entities = deviceEntities
|
||||
.filter((de: DeviceEntity) => hasEntityChanged(de))
|
||||
.map(
|
||||
(new_de) =>
|
||||
new_de.m.toString(16).padStart(2, '0') +
|
||||
new_de.id +
|
||||
(new_de.cn || new_de.mi || new_de.ma ? '|' : '') +
|
||||
(new_de.cn ? new_de.cn : '') +
|
||||
(new_de.mi ? '>' + new_de.mi : '') +
|
||||
(new_de.ma ? '<' + new_de.ma : '')
|
||||
);
|
||||
|
||||
// check size in bytes to match buffer in CPP, which is 2048
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
|
||||
if (bytes > 2000) {
|
||||
toast.warning(LL.CUSTOMIZATIONS_FULL());
|
||||
return;
|
||||
}
|
||||
|
||||
await sendCustomizationEntities({
|
||||
id: selectedDevice,
|
||||
entity_ids: masked_entities
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.CUSTOMIZATIONS_SAVED());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (error.message === 'Reboot required') {
|
||||
setRestartNeeded(true);
|
||||
} else {
|
||||
toast.error(error.message);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setOriginalSettings(deviceEntities);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renameDevice = async () => {
|
||||
await sendDeviceName({ id: selectedDevice, name: selectedDeviceName })
|
||||
.then(() => {
|
||||
toast.success(LL.UPDATED_OF(LL.NAME(1)));
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(LL.UPDATE_OF(LL.NAME(1)) + ' ' + LL.FAILED(1));
|
||||
})
|
||||
.finally(async () => {
|
||||
setRename(false);
|
||||
await fetchCoreData();
|
||||
});
|
||||
};
|
||||
|
||||
const renderDeviceList = () => (
|
||||
<>
|
||||
<Box mb={1} color="warning.main">
|
||||
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||||
{rename ? (
|
||||
<TextField
|
||||
name="device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedDeviceName}
|
||||
onChange={(e) => setSelectedDeviceName(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
name="device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
variant="outlined"
|
||||
value={selectedDevice}
|
||||
disabled={numChanges !== 0}
|
||||
onChange={(e) => setSelectedDevice(parseInt(e.target.value))}
|
||||
margin="normal"
|
||||
style={{ minWidth: '50%' }}
|
||||
select
|
||||
>
|
||||
<MenuItem disabled key={-1} value={-1}>
|
||||
{LL.SELECT_DEVICE()}...
|
||||
</MenuItem>
|
||||
{devices.devices.map(
|
||||
(device: Device) =>
|
||||
device.id < 90 && (
|
||||
<MenuItem key={device.id} value={device.id}>
|
||||
{device.n} ({device.tn})
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</TextField>
|
||||
)}
|
||||
{selectedDevice !== -1 &&
|
||||
(rename ? (
|
||||
<>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => setRename(false)}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => renameDevice()}
|
||||
>
|
||||
{LL.RENAME()}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
startIcon={<EditIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setRename(true)}
|
||||
>
|
||||
{LL.RENAME()}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderDeviceData = () => {
|
||||
const shown_data = deviceEntities.filter((de) => filter_entity(de));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box color="warning.main">
|
||||
<Typography variant="body2" mt={1}>
|
||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
|
||||
{LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />=
|
||||
{LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid
|
||||
container
|
||||
mb={1}
|
||||
mt={0}
|
||||
spacing={2}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid>
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder={LL.SEARCH()}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={getMaskString(selectedFilters)}
|
||||
onChange={(_, mask: string[]) => {
|
||||
setSelectedFilters(getMaskNumber(mask));
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="8">
|
||||
<OptionIcon type="favorite" isSet={true} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="4">
|
||||
<OptionIcon type="readonly" isSet={true} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="2">
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="1">
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="128">
|
||||
<OptionIcon type="deleted" isSet={true} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(false)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
||||
<OptionIcon type="web_exclude" isSet={false} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(true)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography variant="subtitle2" color="grey">
|
||||
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length}
|
||||
{LL.ENTITIES(deviceEntities.length)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Table
|
||||
data={{ nodes: shown_data }}
|
||||
theme={entities_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: DeviceEntity[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
|
||||
<HeaderCell resize>{LL.NAME(1)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.MIN()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.MAX()}</HeaderCell>
|
||||
<HeaderCell resize>{LL.VALUE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((de: DeviceEntity) => (
|
||||
<Row key={de.id} item={de} onClick={() => editDeviceEntity(de)}>
|
||||
<Cell stiff>
|
||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={de} />
|
||||
</Cell>
|
||||
<Cell>
|
||||
{formatName(de, false)} (
|
||||
<Link
|
||||
target="_blank"
|
||||
href={APIURL + selectedDeviceTypeNameURL + '/' + de.id}
|
||||
>
|
||||
{de.id}
|
||||
</Link>
|
||||
)
|
||||
</Cell>
|
||||
<Cell>
|
||||
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}
|
||||
</Cell>
|
||||
<Cell>{formatValue(de.v)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderResetDialog = () => (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmReset}
|
||||
onClose={() => setConfirmReset(false)}
|
||||
>
|
||||
<DialogTitle>{LL.RESET(1)}</DialogTitle>
|
||||
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmReset(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={resetCustomization}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESET(0)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
{devices && renderDeviceList()}
|
||||
{selectedDevice !== -1 && !rename && renderDeviceData()}
|
||||
{restartNeeded ? (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={doRestart}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
) : (
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => devices && sendDeviceEntities(selectedDevice)}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
color="info"
|
||||
onClick={saveCustomization}
|
||||
>
|
||||
{LL.APPLY_CHANGES(numChanges)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
{!rename && (
|
||||
<ButtonRow mt={1}>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setConfirmReset(true)}
|
||||
>
|
||||
{LL.RESET(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{renderResetDialog()}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <SystemMonitor /> : renderContent()}
|
||||
{selectedDeviceEntity && (
|
||||
<SettingsCustomizationsDialog
|
||||
open={dialogOpen}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedItem={selectedDeviceEntity}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Customizations;
|
||||
@@ -1,180 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
|
||||
import EntityMaskToggle from './EntityMaskToggle';
|
||||
import { DeviceEntityMask } from './types';
|
||||
import type { DeviceEntity } from './types';
|
||||
|
||||
interface SettingsCustomizationsDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (di: DeviceEntity) => void;
|
||||
selectedItem: DeviceEntity;
|
||||
}
|
||||
|
||||
const CustomizationsDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem
|
||||
}: SettingsCustomizationsDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
const isWriteableNumber =
|
||||
typeof editItem.v === 'number' &&
|
||||
editItem.w &&
|
||||
!(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setError(false);
|
||||
setEditItem(selectedItem);
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const handleClose = (
|
||||
_event: React.SyntheticEvent,
|
||||
reason: 'backdropClick' | 'escapeKeyDown'
|
||||
) => {
|
||||
if (reason !== 'backdropClick') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (
|
||||
isWriteableNumber &&
|
||||
editItem.mi &&
|
||||
editItem.ma &&
|
||||
editItem.mi > editItem?.ma
|
||||
) {
|
||||
setError(true);
|
||||
} else {
|
||||
onSave(editItem);
|
||||
}
|
||||
};
|
||||
|
||||
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||
setEditItem({ ...editItem, m: updatedItem.m });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container>
|
||||
<Typography variant="body2" color="warning.main">
|
||||
{LL.ID_OF(LL.ENTITY())}:
|
||||
</Typography>
|
||||
<Typography variant="body2">{editItem.id}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid container direction="row">
|
||||
<Typography variant="body2" color="warning.main">
|
||||
{LL.DEFAULT(1) + ' ' + LL.ENTITY_NAME(1)}:
|
||||
</Typography>
|
||||
<Typography variant="body2">{editItem.n}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid container direction="row">
|
||||
<Typography variant="body2" color="warning.main">
|
||||
{LL.WRITEABLE()}:
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{editItem.w ? (
|
||||
<DoneIcon color="success" sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<CloseIcon color="error" sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Box mt={1} mb={2}>
|
||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="cn"
|
||||
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
||||
value={editItem.cn}
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
{isWriteableNumber && (
|
||||
<>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="mi"
|
||||
label={LL.MIN()}
|
||||
value={numberValue(editItem.mi)}
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="ma"
|
||||
label={LL.MAX()}
|
||||
value={numberValue(editItem.ma)}
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
{error && (
|
||||
<Typography variant="body2" color="error" mt={2}>
|
||||
Error: Check min and max values
|
||||
</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<DoneIcon />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomizationsDialog;
|
||||
@@ -1,415 +0,0 @@
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { IconContext } from 'react-icons/lib';
|
||||
import { Link } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import { Body, Cell, Row, Table } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { CellTree, useTree } from '@table-library/react-table-library/tree';
|
||||
import { useRequest } from 'alova/client';
|
||||
import {
|
||||
ButtonTooltip,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval, usePersistState } from 'utils';
|
||||
|
||||
import { readDashboard, writeDeviceValue } from '../../api/app';
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
import DevicesDialog from './DevicesDialog';
|
||||
import { formatValue } from './deviceValue';
|
||||
import {
|
||||
type DashboardItem,
|
||||
DeviceEntityMask,
|
||||
DeviceType,
|
||||
type DeviceValue
|
||||
} from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
|
||||
const Dashboard = memo(() => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
useLayoutTitle(LL.DASHBOARD());
|
||||
|
||||
const [showAll, setShowAll] = usePersistState(true, 'showAll');
|
||||
|
||||
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState<boolean>(false);
|
||||
const [parentNodes, setParentNodes] = useState<number>(0);
|
||||
const [selectedDashboardItem, setSelectedDashboardItem] =
|
||||
useState<DashboardItem>();
|
||||
|
||||
const {
|
||||
data,
|
||||
send: fetchDashboard,
|
||||
error
|
||||
} = useRequest(readDashboard, {
|
||||
initialData: { connected: true, nodes: [] }
|
||||
}).onSuccess((event) => {
|
||||
if (event.data.nodes.length !== parentNodes) {
|
||||
setParentNodes(event.data.nodes.length); // count number of parents/devices
|
||||
}
|
||||
});
|
||||
|
||||
const { loading: submitting, send: sendDeviceValue } = useRequest(
|
||||
(data: { id: number; c: string; v: unknown }) => writeDeviceValue(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const deviceValueDialogSave = useCallback(
|
||||
async (devicevalue: DeviceValue) => {
|
||||
if (!selectedDashboardItem) {
|
||||
return;
|
||||
}
|
||||
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
|
||||
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
|
||||
.then(() => {
|
||||
toast.success(LL.WRITE_CMD_SENT());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
setSelectedDashboardItem(undefined);
|
||||
});
|
||||
},
|
||||
[selectedDashboardItem, sendDeviceValue, LL]
|
||||
);
|
||||
|
||||
const dashboard_theme = useMemo(
|
||||
() =>
|
||||
useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
.td {
|
||||
height: 28px;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
cursor: pointer;
|
||||
background-color: #1e1e1e;
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
},
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
},
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(2) {
|
||||
text-align: right;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
text-align: right;
|
||||
}
|
||||
`
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const tree = useTree(
|
||||
{ nodes: data.nodes },
|
||||
{
|
||||
onChange: () => {} // not used but needed
|
||||
},
|
||||
{
|
||||
treeIcon: {
|
||||
margin: '4px',
|
||||
iconDefault: null,
|
||||
iconRight: (
|
||||
<ChevronRightIcon
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
color="info"
|
||||
/>
|
||||
),
|
||||
iconDown: (
|
||||
<ExpandMoreIcon
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
color="info"
|
||||
/>
|
||||
)
|
||||
},
|
||||
indentation: 45
|
||||
}
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
if (!deviceValueDialogOpen) {
|
||||
void fetchDashboard();
|
||||
}
|
||||
});
|
||||
|
||||
const nodeIds = useMemo(
|
||||
() => data.nodes.map((item: DashboardItem) => item.id),
|
||||
[data.nodes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
showAll
|
||||
? tree.fns.onAddAll(nodeIds) // expand tree
|
||||
: tree.fns.onRemoveAll(); // collapse tree
|
||||
}, [parentNodes]);
|
||||
|
||||
const showType = useCallback(
|
||||
(n?: string, t?: number) => {
|
||||
// if we have a name show it
|
||||
if (n) {
|
||||
return n;
|
||||
}
|
||||
if (t) {
|
||||
// otherwise pick translation based on type
|
||||
switch (t) {
|
||||
case DeviceType.CUSTOM:
|
||||
return LL.CUSTOM_ENTITIES(0);
|
||||
case DeviceType.ANALOGSENSOR:
|
||||
return LL.ANALOG_SENSORS();
|
||||
case DeviceType.TEMPERATURESENSOR:
|
||||
return LL.TEMP_SENSORS();
|
||||
case DeviceType.SCHEDULER:
|
||||
return LL.SCHEDULER();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[LL]
|
||||
);
|
||||
|
||||
const showName = useCallback(
|
||||
(di: DashboardItem) => {
|
||||
if (di.id < 100) {
|
||||
// if its a device (parent node) and has entities
|
||||
if (di.nodes?.length) {
|
||||
return (
|
||||
<span style={{ fontSize: '15px' }}>
|
||||
<DeviceIcon type_id={di.t ?? 0} />
|
||||
{showType(di.n, di.t)}
|
||||
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (di.dv) {
|
||||
return <span>{di.dv.id.slice(2)}</span>;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[showType]
|
||||
);
|
||||
|
||||
const hasMask = useCallback(
|
||||
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
||||
[]
|
||||
);
|
||||
|
||||
const editDashboardValue = useCallback(
|
||||
(di: DashboardItem) => {
|
||||
if (me.admin && di.dv?.c) {
|
||||
setSelectedDashboardItem(di);
|
||||
setDeviceValueDialogOpen(true);
|
||||
}
|
||||
},
|
||||
[me.admin]
|
||||
);
|
||||
|
||||
const handleShowAll = (
|
||||
_event: React.MouseEvent<HTMLElement>,
|
||||
toggle: boolean | null
|
||||
) => {
|
||||
if (toggle !== null) {
|
||||
tree.fns.onToggleAll({});
|
||||
setShowAll(toggle);
|
||||
}
|
||||
};
|
||||
|
||||
const hasFavEntities = useMemo(
|
||||
() => data.nodes.filter((item: DashboardItem) => item.id <= 90).length,
|
||||
[data.nodes]
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (!data) {
|
||||
return (
|
||||
<FormLoader onRetry={fetchDashboard} errorMessage={error?.message || ''} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!data.connected && (
|
||||
<MessageBox mb={2} level="error" message={LL.EMS_BUS_WARNING()} />
|
||||
)}
|
||||
|
||||
{data.connected && data.nodes.length > 0 && !hasFavEntities && (
|
||||
<MessageBox mb={2} level="warning">
|
||||
<Typography>
|
||||
{LL.NO_DATA_1()}
|
||||
<Link to="/customizations" style={{ color: 'white' }}>
|
||||
{LL.CUSTOMIZATIONS()}
|
||||
</Link>
|
||||
{LL.NO_DATA_2()}
|
||||
{LL.NO_DATA_3()}
|
||||
<Link to="/devices" style={{ color: 'white' }}>
|
||||
{LL.DEVICES()}
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</MessageBox>
|
||||
)}
|
||||
|
||||
{data.nodes.length > 0 && (
|
||||
<>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="flex-end"
|
||||
flexWrap="nowrap"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="primary"
|
||||
value={showAll}
|
||||
exclusive
|
||||
onChange={handleShowAll}
|
||||
>
|
||||
<ButtonTooltip title={LL.ALLVALUES()}>
|
||||
<ToggleButton value={true}>
|
||||
<UnfoldMoreIcon sx={{ fontSize: 18 }} />
|
||||
</ToggleButton>
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip title={LL.COMPACT()}>
|
||||
<ToggleButton value={false}>
|
||||
<UnfoldLessIcon sx={{ fontSize: 18 }} />
|
||||
</ToggleButton>
|
||||
</ButtonTooltip>
|
||||
</ToggleButtonGroup>
|
||||
<Tooltip title={LL.DASHBOARD_1()}>
|
||||
<HelpOutlineIcon
|
||||
sx={{
|
||||
ml: 1,
|
||||
mt: 1,
|
||||
fontSize: 20,
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box mt={1} justifyContent="center" flexDirection="column">
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
color: 'lightblue',
|
||||
size: '18',
|
||||
style: { verticalAlign: 'middle' }
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
data={{ nodes: data.nodes }}
|
||||
theme={dashboard_theme}
|
||||
layout={{ custom: true }}
|
||||
tree={tree}
|
||||
>
|
||||
{(tableList: DashboardItem[]) => (
|
||||
<Body>
|
||||
{tableList.map((di: DashboardItem) => (
|
||||
<Row
|
||||
key={di.id}
|
||||
item={di}
|
||||
onClick={() => editDashboardValue(di)}
|
||||
>
|
||||
{di.id > 99 ? (
|
||||
<>
|
||||
<Cell>{showName(di)}</Cell>
|
||||
<Cell>
|
||||
<ButtonTooltip
|
||||
title={formatValue(LL, di.dv?.v, di.dv?.u)}
|
||||
>
|
||||
<span>{formatValue(LL, di.dv?.v, di.dv?.u)}</span>
|
||||
</ButtonTooltip>
|
||||
</Cell>
|
||||
|
||||
<Cell>
|
||||
{me.admin &&
|
||||
di.dv?.c &&
|
||||
!hasMask(
|
||||
di.dv.id,
|
||||
DeviceEntityMask.DV_READONLY
|
||||
) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => editDashboardValue(di)}
|
||||
>
|
||||
<EditIcon
|
||||
color="primary"
|
||||
sx={{ fontSize: 16 }}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Cell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CellTree item={di}>{showName(di)}</CellTree>
|
||||
<Cell />
|
||||
<Cell />
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
)}
|
||||
</Table>
|
||||
</IconContext.Provider>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{renderContent()}
|
||||
{selectedDashboardItem && selectedDashboardItem.dv && (
|
||||
<DevicesDialog
|
||||
open={deviceValueDialogOpen}
|
||||
onClose={() => setDeviceValueDialogOpen(false)}
|
||||
onSave={deviceValueDialogSave}
|
||||
selectedItem={selectedDashboardItem.dv}
|
||||
writeable={true}
|
||||
validator={deviceValueItemValidation(selectedDashboardItem.dv)}
|
||||
progress={submitting}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
});
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,53 +0,0 @@
|
||||
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
|
||||
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
||||
import { FaSolarPanel } from 'react-icons/fa';
|
||||
import { GiHeatHaze, GiTap } from 'react-icons/gi';
|
||||
import { MdPlaylistAdd } from 'react-icons/md';
|
||||
import { MdMoreTime } from 'react-icons/md';
|
||||
import {
|
||||
MdOutlineDevices,
|
||||
MdOutlinePool,
|
||||
MdOutlineSensors,
|
||||
MdThermostatAuto
|
||||
} from 'react-icons/md';
|
||||
import { PiFan, PiGauge } from 'react-icons/pi';
|
||||
import { TiFlowSwitch, TiThermometer } from 'react-icons/ti';
|
||||
import { VscVmConnect } from 'react-icons/vsc';
|
||||
|
||||
import type { SvgIconProps } from '@mui/material';
|
||||
|
||||
import { DeviceType } from './types';
|
||||
|
||||
const deviceIconLookup: {
|
||||
[key in DeviceType]: React.ComponentType<SvgIconProps> | undefined;
|
||||
} = {
|
||||
[DeviceType.TEMPERATURESENSOR]: TiThermometer,
|
||||
[DeviceType.ANALOGSENSOR]: PiGauge,
|
||||
[DeviceType.BOILER]: CgSmartHomeBoiler,
|
||||
[DeviceType.HEATSOURCE]: CgSmartHomeBoiler,
|
||||
[DeviceType.THERMOSTAT]: MdThermostatAuto,
|
||||
[DeviceType.MIXER]: AiOutlineControl,
|
||||
[DeviceType.SOLAR]: FaSolarPanel,
|
||||
[DeviceType.HEATPUMP]: GiHeatHaze,
|
||||
[DeviceType.GATEWAY]: AiOutlineGateway,
|
||||
[DeviceType.SWITCH]: TiFlowSwitch,
|
||||
[DeviceType.CONTROLLER]: VscVmConnect,
|
||||
[DeviceType.CONNECT]: VscVmConnect,
|
||||
[DeviceType.ALERT]: AiOutlineAlert,
|
||||
[DeviceType.EXTENSION]: MdOutlineDevices,
|
||||
[DeviceType.WATER]: GiTap,
|
||||
[DeviceType.POOL]: MdOutlinePool,
|
||||
[DeviceType.CUSTOM]: MdPlaylistAdd,
|
||||
[DeviceType.UNKNOWN]: MdOutlineSensors,
|
||||
[DeviceType.SYSTEM]: undefined,
|
||||
[DeviceType.SCHEDULER]: MdMoreTime,
|
||||
[DeviceType.GENERIC]: MdOutlineSensors,
|
||||
[DeviceType.VENTILATION]: PiFan
|
||||
};
|
||||
|
||||
const DeviceIcon = ({ type_id }: { type_id: DeviceType }) => {
|
||||
const Icon = deviceIconLookup[type_id];
|
||||
return Icon ? <Icon /> : null;
|
||||
};
|
||||
|
||||
export default DeviceIcon;
|
||||
@@ -1,833 +0,0 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||
import ConstructionIcon from '@mui/icons-material/Construction';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
|
||||
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined';
|
||||
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import { useRowSelect } from '@table-library/react-table-library/select';
|
||||
import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import type { Action, State } from '@table-library/react-table-library/types/common';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import {
|
||||
ButtonTooltip,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
import { readCoreData, readDeviceData, writeDeviceValue } from '../../api/app';
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
import DevicesDialog from './DevicesDialog';
|
||||
import { formatValue } from './deviceValue';
|
||||
import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
|
||||
import type { Device, DeviceValue } from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
|
||||
const Devices = memo(() => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [size, setSize] = useState([0, 0]);
|
||||
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
|
||||
const [onlyFav, setOnlyFav] = useState(false);
|
||||
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
|
||||
const [showDeviceInfo, setShowDeviceInfo] = useState(false);
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useLayoutTitle(LL.DEVICES());
|
||||
|
||||
const { data: coreData, send: sendCoreData } = useRequest(() => readCoreData(), {
|
||||
initialData: {
|
||||
connected: true,
|
||||
devices: []
|
||||
}
|
||||
});
|
||||
|
||||
const { data: deviceData, send: sendDeviceData } = useRequest(
|
||||
(id: number) => readDeviceData(id),
|
||||
{
|
||||
initialData: {
|
||||
nodes: []
|
||||
},
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const { loading: submitting, send: sendDeviceValue } = useRequest(
|
||||
(data: { id: number; c: string; v: unknown }) => writeDeviceValue(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
function updateSize() {
|
||||
setSize([window.innerWidth, window.innerHeight]);
|
||||
}
|
||||
window.addEventListener('resize', updateSize);
|
||||
updateSize();
|
||||
return () => window.removeEventListener('resize', updateSize);
|
||||
}, []);
|
||||
|
||||
const leftOffset = () => {
|
||||
const devicesWindow = document.getElementById('devices-window');
|
||||
if (!devicesWindow) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const clientRect = devicesWindow.getBoundingClientRect();
|
||||
const left = clientRect.left;
|
||||
const right = clientRect.right;
|
||||
|
||||
if (!left || !right) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return left + (right - left < 400 ? 0 : 200);
|
||||
};
|
||||
|
||||
const common_theme = useMemo(
|
||||
() =>
|
||||
useTheme({
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
.th {
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
cursor: pointer;
|
||||
background-color: #1E1E1E;
|
||||
.td {
|
||||
padding: 8px;
|
||||
}
|
||||
&.tr.tr-body.row-select.row-select-single-selected {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const device_theme = useMemo(
|
||||
() =>
|
||||
useTheme([
|
||||
common_theme,
|
||||
{
|
||||
BaseRow: `
|
||||
font-size: 15px;
|
||||
.td {
|
||||
height: 28px;
|
||||
}
|
||||
`,
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
.th {
|
||||
padding: 8px;
|
||||
`,
|
||||
Row: `
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
},
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
},
|
||||
`
|
||||
}
|
||||
]),
|
||||
[common_theme]
|
||||
);
|
||||
|
||||
const data_theme = useMemo(
|
||||
() =>
|
||||
useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
::-webkit-scrollbar {
|
||||
display:none;
|
||||
}
|
||||
`,
|
||||
BaseRow: `
|
||||
.td {
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(1) {
|
||||
border-left: 1px solid #177ac9;
|
||||
},
|
||||
&:nth-of-type(2) {
|
||||
text-align: right;
|
||||
},
|
||||
&:nth-of-type(3) {
|
||||
border-right: 1px solid #177ac9;
|
||||
}
|
||||
`,
|
||||
HeaderRow: `
|
||||
.th {
|
||||
border-top: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
},
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
}
|
||||
]),
|
||||
[common_theme]
|
||||
);
|
||||
|
||||
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
return <KeyboardArrowDownOutlinedIcon />;
|
||||
}
|
||||
if (state.sortKey === sortKey && !state.reverse) {
|
||||
return <KeyboardArrowUpOutlinedIcon />;
|
||||
}
|
||||
return <UnfoldMoreOutlinedIcon />;
|
||||
};
|
||||
|
||||
const dv_sort = useSort(
|
||||
{ nodes: deviceData.nodes },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
NAME: (array) =>
|
||||
array.sort((a, b) =>
|
||||
a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))
|
||||
),
|
||||
|
||||
VALUE: (array) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function onSelectChange(action: Action, state: State) {
|
||||
setSelectedDevice(state.id as number);
|
||||
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
|
||||
await sendDeviceData(state.id as number);
|
||||
}
|
||||
}
|
||||
|
||||
const device_select = useRowSelect(
|
||||
{ nodes: coreData.devices },
|
||||
{
|
||||
onChange: onSelectChange
|
||||
}
|
||||
);
|
||||
|
||||
const resetDeviceSelect = () => {
|
||||
device_select.fns.onRemoveAll();
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const escFunction = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (device_select) {
|
||||
device_select.fns.onRemoveAll();
|
||||
}
|
||||
}
|
||||
},
|
||||
[device_select]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', escFunction);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escFunction);
|
||||
};
|
||||
}, [escFunction]);
|
||||
|
||||
const customize = () => {
|
||||
if (selectedDevice === 99) {
|
||||
void navigate('/customentities');
|
||||
} else {
|
||||
void navigate('/customizations', { state: selectedDevice });
|
||||
}
|
||||
};
|
||||
|
||||
const escapeCsvCell = (cell: string) => {
|
||||
if (cell == null) {
|
||||
return '';
|
||||
}
|
||||
const sc = cell.toString().trim();
|
||||
if (sc === '' || sc === '""') {
|
||||
return sc;
|
||||
}
|
||||
if (
|
||||
sc.includes('"') ||
|
||||
sc.includes(';') ||
|
||||
sc.includes('\n') ||
|
||||
sc.includes('\r')
|
||||
) {
|
||||
return '"' + sc.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return sc;
|
||||
};
|
||||
|
||||
const hasMask = useCallback(
|
||||
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDownloadCsv = () => {
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d: Device) => d.id === device_select.state.id
|
||||
);
|
||||
if (deviceIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const selectedDevice = coreData.devices[deviceIndex];
|
||||
if (!selectedDevice) {
|
||||
return;
|
||||
}
|
||||
const filename = selectedDevice.tn + '_' + selectedDevice.n;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
accessor: (dv: DeviceValue) => dv.id.slice(2),
|
||||
name: LL.ENTITY_NAME(0)
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v,
|
||||
name: LL.VALUE(0)
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
dv.u !== undefined && DeviceValueUOM_s[dv.u]
|
||||
? DeviceValueUOM_s[dv.u]?.replace(/[^a-zA-Z0-9]/g, '')
|
||||
: '',
|
||||
name: 'UoM'
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) ? 'yes' : 'no',
|
||||
name: LL.WRITEABLE()
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
dv.h
|
||||
? dv.h
|
||||
: dv.l
|
||||
? dv.l.join(' | ')
|
||||
: dv.m !== undefined && dv.x !== undefined
|
||||
? dv.m + ', ' + dv.x
|
||||
: '',
|
||||
name: 'Range'
|
||||
}
|
||||
];
|
||||
|
||||
const data = onlyFav
|
||||
? deviceData.nodes.filter((dv: DeviceValue) =>
|
||||
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)
|
||||
)
|
||||
: deviceData.nodes;
|
||||
|
||||
const csvData = data.reduce(
|
||||
(csvString: string, rowItem: DeviceValue) =>
|
||||
csvString +
|
||||
columns
|
||||
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) =>
|
||||
escapeCsvCell(accessor(rowItem) as string)
|
||||
)
|
||||
.join(';') +
|
||||
'\r\n',
|
||||
columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') +
|
||||
'\r\n'
|
||||
);
|
||||
|
||||
const downloadBlob = (blob: Blob) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.download = filename;
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const device = { ...{ device: coreData.devices[deviceIndex] }, ...deviceData };
|
||||
downloadBlob(
|
||||
new Blob([JSON.stringify(device, null, 2)], {
|
||||
type: 'text;charset:utf-8'
|
||||
})
|
||||
);
|
||||
|
||||
downloadBlob(new Blob([csvData], { type: 'text/csv;charset:utf-8' }));
|
||||
};
|
||||
|
||||
useInterval(() => {
|
||||
if (!deviceValueDialogOpen) {
|
||||
selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData();
|
||||
}
|
||||
});
|
||||
|
||||
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||
const id = Number(device_select.state.id);
|
||||
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
|
||||
.then(() => {
|
||||
toast.success(LL.WRITE_CMD_SENT());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
await sendDeviceData(id);
|
||||
setSelectedDeviceValue(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
const renderDeviceDetails = () => {
|
||||
if (showDeviceInfo) {
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d: Device) => d.id === device_select.state.id
|
||||
);
|
||||
if (deviceIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
const deviceDetails = coreData.devices[deviceIndex];
|
||||
if (!deviceDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={showDeviceInfo}
|
||||
onClose={() => setShowDeviceInfo(false)}
|
||||
>
|
||||
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<List dense={true}>
|
||||
<ListItem>
|
||||
<ListItemText primary={LL.TYPE(0)} secondary={deviceDetails.tn} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary={LL.NAME(0)} secondary={deviceDetails.n} />
|
||||
</ListItem>
|
||||
{deviceDetails.t !== DeviceType.CUSTOM && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemText primary={LL.BRAND()} secondary={deviceDetails.b} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.ID_OF(LL.DEVICE())}
|
||||
secondary={
|
||||
'0x' +
|
||||
('00' + deviceDetails.d.toString(16).toUpperCase()).slice(-2)
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.ID_OF(LL.PRODUCT())}
|
||||
secondary={deviceDetails.p}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.VERSION()}
|
||||
secondary={deviceDetails.v}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setShowDeviceInfo(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderCoreData = () => (
|
||||
<>
|
||||
<Box justifyContent="center" flexDirection="column">
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
color: 'lightblue',
|
||||
size: '18',
|
||||
style: { verticalAlign: 'middle' }
|
||||
}}
|
||||
>
|
||||
{!coreData.connected && (
|
||||
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
|
||||
)}
|
||||
|
||||
{coreData.connected && (
|
||||
<Table
|
||||
data={{ nodes: coreData.devices }}
|
||||
select={device_select}
|
||||
theme={device_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: Device[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.length === 0 && (
|
||||
<CircularProgress sx={{ margin: 1 }} size={18} />
|
||||
)}
|
||||
{tableList.map((device: Device) => (
|
||||
<Row key={device.id} item={device}>
|
||||
<Cell>
|
||||
<DeviceIcon type_id={device.t} />
|
||||
|
||||
{device.n}
|
||||
<span style={{ color: 'lightblue' }}>
|
||||
({device.e})
|
||||
</span>
|
||||
</Cell>
|
||||
<Cell stiff>{device.tn}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
const deviceValueDialogClose = () => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
if (selectedDevice !== undefined) {
|
||||
void sendDeviceData(selectedDevice);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDeviceData = () => {
|
||||
if (!selectedDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showDeviceValue = useCallback((dv: DeviceValue) => {
|
||||
setSelectedDeviceValue(dv);
|
||||
setDeviceValueDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const renderNameCell = useCallback(
|
||||
(dv: DeviceValue) => (
|
||||
<>
|
||||
{dv.id.slice(2)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
||||
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[hasMask]
|
||||
);
|
||||
|
||||
const shown_data = useMemo(() => {
|
||||
if (onlyFav) {
|
||||
return deviceData.nodes.filter(
|
||||
(dv: DeviceValue) =>
|
||||
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
|
||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
return deviceData.nodes.filter((dv: DeviceValue) =>
|
||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}, [deviceData.nodes, onlyFav, search]);
|
||||
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d: Device) => d.id === device_select.state.id
|
||||
);
|
||||
if (deviceIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const deviceInfo = coreData.devices[deviceIndex];
|
||||
if (!deviceInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, height] = size;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'black',
|
||||
position: 'absolute',
|
||||
left: () => leftOffset(),
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 64,
|
||||
zIndex: 'modal',
|
||||
maxHeight: () => (height || 0) - 126,
|
||||
border: '1px solid #177ac9'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Grid container justifyContent="space-between">
|
||||
<Typography noWrap variant="subtitle1" color="warning.main">
|
||||
{deviceInfo.n} (
|
||||
{deviceInfo.tn})
|
||||
</Typography>
|
||||
<Grid justifyContent="flex-end">
|
||||
<ButtonTooltip title={LL.CLOSE()}>
|
||||
<IconButton onClick={resetDeviceSelect}>
|
||||
<HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ width: '22ch' }}
|
||||
placeholder={LL.SEARCH()}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ButtonTooltip title={LL.DEVICE_DETAILS()}>
|
||||
<IconButton onClick={() => setShowDeviceInfo(true)}>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
{me.admin && (
|
||||
<ButtonTooltip title={LL.CUSTOMIZATIONS()}>
|
||||
<IconButton onClick={customize}>
|
||||
<ConstructionIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
)}
|
||||
<ButtonTooltip title={LL.EXPORT()}>
|
||||
<IconButton onClick={handleDownloadCsv}>
|
||||
<DownloadIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
|
||||
<ButtonTooltip title={LL.FAVORITES()}>
|
||||
<ToggleButton
|
||||
value="1"
|
||||
size="small"
|
||||
selected={onlyFav}
|
||||
onChange={() => {
|
||||
setOnlyFav(!onlyFav);
|
||||
}}
|
||||
>
|
||||
{onlyFav ? (
|
||||
<StarIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
) : (
|
||||
<StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
)}{' '}
|
||||
</ToggleButton>
|
||||
</ButtonTooltip>
|
||||
|
||||
<span style={{ color: 'grey', fontSize: '12px' }}>
|
||||
|
||||
{LL.SHOWING() +
|
||||
' ' +
|
||||
shown_data.length +
|
||||
'/' +
|
||||
deviceInfo.e +
|
||||
' ' +
|
||||
LL.ENTITIES(shown_data.length)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
<Table
|
||||
data={{ nodes: shown_data }}
|
||||
theme={data_theme}
|
||||
sort={dv_sort}
|
||||
layout={{ custom: true, fixedHeader: true }}
|
||||
>
|
||||
{(tableList: DeviceValue[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(dv_sort.state, 'NAME')}
|
||||
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
{LL.ENTITY_NAME(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
|
||||
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||
>
|
||||
{LL.VALUE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((dv: DeviceValue) => (
|
||||
<Row key={dv.id} item={dv} onClick={() => showDeviceValue(dv)}>
|
||||
<Cell>{renderNameCell(dv)}</Cell>
|
||||
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
|
||||
<Cell stiff>
|
||||
{me.admin &&
|
||||
dv.c &&
|
||||
!hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => showDeviceValue(dv)}
|
||||
>
|
||||
{dv.v === '' ? (
|
||||
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent id="devices-window">
|
||||
{renderCoreData()}
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDetails()}
|
||||
{selectedDeviceValue && (
|
||||
<DevicesDialog
|
||||
open={deviceValueDialogOpen}
|
||||
onClose={deviceValueDialogClose}
|
||||
onSave={deviceValueDialogSave}
|
||||
selectedItem={selectedDeviceValue}
|
||||
writeable={
|
||||
selectedDeviceValue.c !== undefined &&
|
||||
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
||||
}
|
||||
validator={deviceValueItemValidation(selectedDeviceValue)}
|
||||
progress={submitting}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
});
|
||||
|
||||
export default Devices;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user