mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
Compare commits
14 Commits
| 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
|
BasedOnStyle: LLVM
|
||||||
UseTab: Never
|
UseTab: Never
|
||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
ColumnLimit: 160
|
ColumnLimit: 220
|
||||||
TabWidth: 4
|
TabWidth: 4
|
||||||
#BreakBeforeBraces: Custom
|
#BreakBeforeBraces: Custom
|
||||||
BraceWrapping:
|
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: ''
|
|
||||||
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,50 +1,35 @@
|
|||||||
---
|
---
|
||||||
name: Problem Report
|
name: Bug report
|
||||||
about: Create a Report to help us improve
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Thanks for reporting a problem 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 problem 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.*
|
||||||
|
|
||||||
### PROBLEM 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._
|
**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 problem in [issues](https://github.com/emsesp/EMS-ESP32/issues)
|
**Device information**
|
||||||
- [ ] Searched the problem in [discussions](https://github.com/emsesp/EMS-ESP32/discussions)
|
*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*
|
||||||
- [ ] Searched the problem in the [docs](https://emsesp.github.io/docs/Troubleshooting/)
|
|
||||||
- [ ] Searched the problem in the [chat](https://discord.gg/3J3GgnzpyT)
|
|
||||||
- [ ] Provide the output of http://ems-esp.local/api/system :
|
|
||||||
|
|
||||||
```lua
|
**Additional context**
|
||||||
System information output here:
|
*Add any other context about the problem 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 problem._
|
|
||||||
|
|
||||||
### ADDITIONAL CONTEXT
|
|
||||||
|
|
||||||
_Add any other context about the problem here._
|
|
||||||
|
|
||||||
**(Please, remember to close the issue when the problem has been addressed)**
|
|
||||||
|
|||||||
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://emsesp.github.io/docs/
|
|
||||||
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.*
|
||||||
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
|
||||||
23
.github/workflows/github-releases-to-discord.yml
vendored
23
.github/workflows/github-releases-to-discord.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
name: 'github-releases-to-discord'
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
github-releases-to-discord:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- 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
|
|
||||||
60
.github/workflows/pre_release.yml
vendored
60
.github/workflows/pre_release.yml
vendored
@@ -1,61 +1,61 @@
|
|||||||
name: 'pre-release'
|
name: "pre-release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'dev'
|
- "dev"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-release:
|
pre-release:
|
||||||
name: 'Automatic pre-release build'
|
|
||||||
|
name: "Automatic pre-release build"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Get EMS-ESP source code and version
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Get build variables
|
||||||
id: build_info
|
id: build_info
|
||||||
run: |
|
run: |
|
||||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
echo "::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: Install PlatformIO
|
- name: Compile locally
|
||||||
|
run: make
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
|
||||||
|
- name: Install pio
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
|
platformio upgrade
|
||||||
|
platformio update
|
||||||
|
|
||||||
- name: Build WebUI
|
- name: Build web
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
yarn install
|
npm install
|
||||||
yarn typesafe-i18n --no-watch
|
npm run build
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
|
||||||
yarn build
|
|
||||||
yarn webUI
|
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run -e ci
|
||||||
|
|
||||||
- name: Build S3 firmware
|
- name: Release
|
||||||
run: |
|
id: "automatic_releases"
|
||||||
platformio run -e ci_s3
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
|
|
||||||
- name: Create a GH Release
|
|
||||||
id: 'automatic_releases'
|
|
||||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
|
||||||
with:
|
with:
|
||||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
title: ${{steps.build_info.outputs.platform}} Development Build v${{steps.build_info.outputs.version}}
|
||||||
automatic_release_tag: 'latest'
|
automatic_release_tag: "latest"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: |
|
files: |
|
||||||
CHANGELOG_LATEST.md
|
CHANGELOG_LATEST.md
|
||||||
./build/firmware/*.*
|
./build/firmware/*.*
|
||||||
|
|
||||||
|
|||||||
30
.github/workflows/sonar_check.yml
vendored
30
.github/workflows/sonar_check.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Sonar Check
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build and analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# if: github.repository_owner == 'emsesp'
|
|
||||||
# if: github.repository == 'emsesp/EMS-ESP32'
|
|
||||||
env:
|
|
||||||
BUILD_WRAPPER_OUT_DIR: bw-output
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
|
||||||
- name: Install sonar-scanner and build-wrapper
|
|
||||||
uses: SonarSource/sonarcloud-github-c-cpp@v2
|
|
||||||
- name: Run build-wrapper
|
|
||||||
run: |
|
|
||||||
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
|
|
||||||
- name: Run sonar-scanner
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
run: |
|
|
||||||
sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
|
|
||||||
51
.github/workflows/tagged_release.yml
vendored
51
.github/workflows/tagged_release.yml
vendored
@@ -1,52 +1,55 @@
|
|||||||
name: 'tagged-release'
|
name: "tagged-release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tagged-release:
|
tagged-release:
|
||||||
name: 'Tagged Release'
|
|
||||||
|
name: "Tagged Release"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout source code
|
||||||
- uses: actions/setup-python@v4
|
uses: actions/checkout@v2
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Install PlatformIO
|
- 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: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
platformio upgrade
|
platformio upgrade
|
||||||
pio pkg update
|
platformio update
|
||||||
|
|
||||||
- name: Build WebUI
|
- name: Build web
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
yarn install
|
npm install
|
||||||
yarn typesafe-i18n --no-watch
|
npm run build
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
|
||||||
yarn build
|
|
||||||
yarn webUI
|
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run -e ci
|
||||||
|
|
||||||
- name: Build S3 firmware
|
|
||||||
run: |
|
|
||||||
platformio run -e ci_s3
|
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: |
|
files: |
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
|||||||
61
.github/workflows/test_release.yml
vendored
61
.github/workflows/test_release.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
name: 'test-release'
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev2'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-release:
|
|
||||||
name: 'Automatic test-release build'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Get EMS-ESP source code and version
|
|
||||||
id: build_info
|
|
||||||
run: |
|
|
||||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
|
||||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Install PlatformIO
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -U platformio
|
|
||||||
|
|
||||||
- name: Build WebUI
|
|
||||||
run: |
|
|
||||||
cd interface
|
|
||||||
yarn install
|
|
||||||
yarn typesafe-i18n --no-watch
|
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
|
||||||
yarn build
|
|
||||||
yarn webUI
|
|
||||||
|
|
||||||
- name: Build firmware
|
|
||||||
run: |
|
|
||||||
platformio run -e ci
|
|
||||||
|
|
||||||
- name: Build S3 firmware
|
|
||||||
run: |
|
|
||||||
platformio run -e ci_s3
|
|
||||||
|
|
||||||
- name: Create a GH Release
|
|
||||||
id: 'automatic_releases'
|
|
||||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
|
||||||
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/*.*
|
|
||||||
54
.gitignore
vendored
54
.gitignore
vendored
@@ -1,64 +1,30 @@
|
|||||||
# vscode
|
# vscode
|
||||||
.vscode/c_cpp_properties.json
|
.vscode
|
||||||
.vscode/extensions.json
|
.directory
|
||||||
.vscode/launch.json
|
workspace.code-workspace
|
||||||
# .vscode/settings.json
|
|
||||||
|
|
||||||
# c++ compiling
|
# build
|
||||||
|
build/
|
||||||
.clang_complete
|
.clang_complete
|
||||||
.gcc-flags.json
|
.gcc-flags.json
|
||||||
cppcheck.out.xml
|
cppcheck.out.xml
|
||||||
|
debug.log
|
||||||
|
|
||||||
# platformio
|
# platformio
|
||||||
.pio
|
.pio
|
||||||
pio_local.ini
|
pio_local.ini
|
||||||
|
/.VSCodeCounter
|
||||||
|
|
||||||
# OS specific
|
# OS specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*Thumbs.db
|
*Thumbs.db
|
||||||
emsesp
|
|
||||||
|
|
||||||
# web specfic
|
# project specfic
|
||||||
build/
|
/scripts/stackdmp.txt
|
||||||
dist/
|
emsesp
|
||||||
/data/www
|
/data/www
|
||||||
/lib/framework/WWWData.h
|
/lib/framework/WWWData.h
|
||||||
/interface/build
|
/interface/build
|
||||||
node_modules
|
node_modules
|
||||||
/interface/.eslintcache
|
/interface/.eslintcache
|
||||||
stats.html
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
yarn.lock
|
|
||||||
interface/analyse.html
|
|
||||||
|
|
||||||
# scripts
|
|
||||||
test.sh
|
|
||||||
scripts/run.sh
|
|
||||||
scripts/__pycache__
|
|
||||||
/scripts/stackdmp.txt
|
|
||||||
|
|
||||||
# i18n generated files
|
|
||||||
interface/src/i18n/i18n-react.tsx
|
|
||||||
interface/src/i18n/i18n-types.ts
|
|
||||||
interface/src/i18n/i18n-util.ts
|
|
||||||
interface/src/i18n/i18n-util.sync.ts
|
|
||||||
interface/src/i18n/i18n-util.async.ts
|
|
||||||
|
|
||||||
# sonar
|
|
||||||
.scannerwork/
|
|
||||||
sonar/
|
|
||||||
bw-output/
|
|
||||||
|
|
||||||
# entity dump results
|
|
||||||
# dump_entities.csv
|
|
||||||
# dump_entities.xls*
|
|
||||||
|
|
||||||
benchmark/*.log
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "none",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 120,
|
|
||||||
"bracketSpacing": true
|
|
||||||
}
|
|
||||||
12
.vscode/extensions.json
vendored
12
.vscode/extensions.json
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
|
||||||
// for the documentation about the extensions.json format
|
|
||||||
"recommendations": [
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"platformio.platformio-ide"
|
|
||||||
],
|
|
||||||
"unwantedRecommendations": [
|
|
||||||
"ms-vscode.cpptools-extension-pack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
88
.vscode/settings.json
vendored
88
.vscode/settings.json
vendored
@@ -1,88 +0,0 @@
|
|||||||
{
|
|
||||||
"search.exclude": {
|
|
||||||
"**/.yarn": true,
|
|
||||||
"**/.pnp.*": true
|
|
||||||
},
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll": "explicit"
|
|
||||||
},
|
|
||||||
"eslint.nodePath": "interface/.yarn/sdks",
|
|
||||||
"eslint.workingDirectories": ["interface"],
|
|
||||||
"prettier.prettierPath": "",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
|
||||||
"files.associations": {
|
|
||||||
"*.tsx": "typescriptreact",
|
|
||||||
"*.tcc": "cpp",
|
|
||||||
"optional": "cpp",
|
|
||||||
"istream": "cpp",
|
|
||||||
"ostream": "cpp",
|
|
||||||
"ratio": "cpp",
|
|
||||||
"system_error": "cpp",
|
|
||||||
"array": "cpp",
|
|
||||||
"functional": "cpp",
|
|
||||||
"regex": "cpp",
|
|
||||||
"tuple": "cpp",
|
|
||||||
"type_traits": "cpp",
|
|
||||||
"utility": "cpp",
|
|
||||||
"string": "cpp",
|
|
||||||
"string_view": "cpp",
|
|
||||||
"atomic": "cpp",
|
|
||||||
"bitset": "cpp",
|
|
||||||
"cctype": "cpp",
|
|
||||||
"chrono": "cpp",
|
|
||||||
"clocale": "cpp",
|
|
||||||
"cmath": "cpp",
|
|
||||||
"condition_variable": "cpp",
|
|
||||||
"cstdarg": "cpp",
|
|
||||||
"cstddef": "cpp",
|
|
||||||
"cstdint": "cpp",
|
|
||||||
"cstdio": "cpp",
|
|
||||||
"cstdlib": "cpp",
|
|
||||||
"cstring": "cpp",
|
|
||||||
"ctime": "cpp",
|
|
||||||
"cwchar": "cpp",
|
|
||||||
"cwctype": "cpp",
|
|
||||||
"deque": "cpp",
|
|
||||||
"list": "cpp",
|
|
||||||
"unordered_map": "cpp",
|
|
||||||
"unordered_set": "cpp",
|
|
||||||
"vector": "cpp",
|
|
||||||
"exception": "cpp",
|
|
||||||
"algorithm": "cpp",
|
|
||||||
"iterator": "cpp",
|
|
||||||
"map": "cpp",
|
|
||||||
"memory": "cpp",
|
|
||||||
"memory_resource": "cpp",
|
|
||||||
"numeric": "cpp",
|
|
||||||
"random": "cpp",
|
|
||||||
"set": "cpp",
|
|
||||||
"fstream": "cpp",
|
|
||||||
"initializer_list": "cpp",
|
|
||||||
"iomanip": "cpp",
|
|
||||||
"iosfwd": "cpp",
|
|
||||||
"iostream": "cpp",
|
|
||||||
"limits": "cpp",
|
|
||||||
"mutex": "cpp",
|
|
||||||
"new": "cpp",
|
|
||||||
"sstream": "cpp",
|
|
||||||
"stdexcept": "cpp",
|
|
||||||
"streambuf": "cpp",
|
|
||||||
"thread": "cpp",
|
|
||||||
"cinttypes": "cpp",
|
|
||||||
"typeinfo": "cpp"
|
|
||||||
},
|
|
||||||
"todo-tree.filtering.excludeGlobs": [
|
|
||||||
"**/vendor/**",
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/dist/**",
|
|
||||||
"**/bower_components/**",
|
|
||||||
"**/build/**",
|
|
||||||
"**/.vscode/**",
|
|
||||||
"**/.github/**",
|
|
||||||
"**/_output/**",
|
|
||||||
"**/*.min.*",
|
|
||||||
"**/*.map",
|
|
||||||
"**/ArduinoJson/**"
|
|
||||||
],
|
|
||||||
"cSpell.enableFiletypes": ["!cpp"]
|
|
||||||
}
|
|
||||||
18
.vscode/tasks.json
vendored
18
.vscode/tasks.json
vendored
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"type": "shell",
|
|
||||||
"label": "build standalone emsesp",
|
|
||||||
"command": "make",
|
|
||||||
"args": [],
|
|
||||||
"problemMatcher": ["$gcc"],
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
543
CHANGELOG.md
543
CHANGELOG.md
@@ -5,498 +5,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [3.6.4] November 24 2023
|
## [3.1.0] May 4 2021
|
||||||
|
|
||||||
## **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 entitiy 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
|
|
||||||
|
|
||||||
- Mock API to simulate an ESP, for testing web
|
- Mock API to simulate an ESP, for testing web
|
||||||
- Able to write values from the Web UI
|
- Able to write values from the Web UI
|
||||||
@@ -518,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
|
- lowercased Flow temp in commands
|
||||||
- system console commands to main
|
- system console commands to main
|
||||||
|
|
||||||
# [3.0.1] March 30 2021
|
## Removed
|
||||||
|
|
||||||
|
## [3.0.1] March 30 2021
|
||||||
|
|
||||||
## Added
|
## 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)
|
- individual mqtt commands (#31)
|
||||||
- board Profiles (#11)
|
- board Profiles (#11)
|
||||||
|
|
||||||
## Fixed
|
## 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
|
- Rx and Tx quality % would sometimes show > 100
|
||||||
|
|
||||||
## Changed
|
## 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.
|
- invert LED changed to Hide LED. Default is off.
|
||||||
- renamed Scan Network to Scan WiFi Network
|
- renamed Scan Network to Scan WiFi Network
|
||||||
- added version to cmd=settings
|
- added version to cmd=settings
|
||||||
@@ -540,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)
|
- 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
|
- Power settings, disabling BLE and turning off Wifi sleep
|
||||||
- Rx and Tx counts to Heartbeat MQTT payload
|
- Rx and Tx counts to Heartbeat MQTT payload
|
||||||
@@ -559,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
|
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
||||||
- Syslog port selectable (#744)
|
- Syslog port selectable (#744)
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
|
|
||||||
- telegrams matched to masterthermostat 0x18
|
- telegrams matched to masterthermostat 0x18
|
||||||
- multiple roomcontrollers
|
- multiple roomcontrollers
|
||||||
@@ -570,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
|
- wrong position of values #723, #732
|
||||||
- OTA Upload via Web on OSX
|
- OTA Upload via Web on OSX
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
|
|
||||||
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
||||||
- split `show values` in smaller packages (edited)
|
- split `show values` in smaller packages (edited)
|
||||||
@@ -585,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
|
- some names of mqtt-tags like in v2.2.1
|
||||||
- new ESP32 partition side to allow for smoother OTA and fallback
|
- new ESP32 partition side to allow for smoother OTA and fallback
|
||||||
- Network Gateway IP is optional (#682)emsesp/EMS-ESP
|
- 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,24 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [3.6.5]
|
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
|
||||||
|
|
||||||
## Added
|
## 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 (Slovencina) language. Thanks @misa1515
|
|
||||||
- CPU info [#1497](https://github.com/emsesp/EMS-ESP32/pull/1497)
|
|
||||||
|
|
||||||
## Fixed
|
## 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
|
## Changed
|
||||||
|
|
||||||
- HA don't set entity_category to Diagnostic/Configuration for EMS entities [#1459](https://github.com/emsesp/EMS-ESP32/discussions/1459)
|
## Removed
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<img src="/media/EMS-ESP_logo_dark.png" alt="Logo" align="right" height="76"/>
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
**Any contribution helps EMS-ESP get better for the entire community!**
|
**Any contribution helps EMS-ESP get better for the entire community!**
|
||||||
@@ -22,17 +24,17 @@ This document describes rules that are in effect for this repository, meant for
|
|||||||
|
|
||||||
## Triaging of Issues/PR's
|
## Triaging of Issues/PR's
|
||||||
|
|
||||||
1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so.
|
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.
|
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.
|
3. Issues that are accepted should be marked with appropriate labels.
|
||||||
4. Issues that could impact functionality for many users should be considered severe.
|
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.
|
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.
|
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.
|
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:
|
8. Feature requests that are not accompanied by a PR:
|
||||||
- could be closed immediately (denied).
|
* could be closed immediately (denied).
|
||||||
- could be closed after some predetermined period of time (left as candidate for somebody to pick up).
|
* could be closed 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.
|
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
|
## 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.
|
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)
|
- 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).
|
- 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.
|
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
|
||||||
- Ensure tests work.
|
- Ensure tests work.
|
||||||
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
|
- 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.
|
1. All pull requests must be done against the dev branch.
|
||||||
2. Make sure code is formatting per the `.clang-format`.
|
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.
|
3. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
|
||||||
4. 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. 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. 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 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. 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. 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. 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. 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.
|
||||||
12. Pull requests that don't meet the above will be denied and closed.
|
|
||||||
|
|
||||||
## Semantic Commit Messages
|
## Semantic Commit Messages
|
||||||
|
|
||||||
@@ -90,9 +92,9 @@ More Examples:
|
|||||||
|
|
||||||
References:
|
References:
|
||||||
|
|
||||||
- <https://www.conventionalcommits.org/>
|
- https://www.conventionalcommits.org/
|
||||||
|
|
||||||
---
|
--------------------------------------
|
||||||
|
|
||||||
## Contributor License Agreement (CLA)
|
## Contributor License Agreement (CLA)
|
||||||
|
|
||||||
@@ -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.
|
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 ?**
|
**Why a CLA ?**
|
||||||
|
|
||||||
@@ -131,9 +133,9 @@ 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.
|
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>
|
<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 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.>
|
||||||
|
|||||||
114
README.md
114
README.md
@@ -1,71 +1,107 @@
|
|||||||
# 
|
# 
|
||||||
|
|
||||||
|
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
||||||
|
|
||||||
|
This is the firmware for the ESP32. Compared to version 2 on the ESP8266, this version has
|
||||||
|
- Ethernet Support
|
||||||
|
- Pre-configured board layouts
|
||||||
|
- Writing values directly from the Web UI
|
||||||
|
- Mock API server for faster offline development
|
||||||
|
- Expose to more commands, via MQTT
|
||||||
|
- Improvements to Dallas sensors, Shower service
|
||||||
|
|
||||||
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
||||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=emsesp_EMS-ESP32)
|
[](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://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)
|
[](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)
|
[](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/stargazers)
|
||||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||||
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
||||||
|
|
||||||
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger. It requires a small gateway circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
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.
|
||||||
|
|
||||||
|
<img src="media/gateway-integration.jpg" width=40%>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## **Features**
|
## **Features**
|
||||||
|
|
||||||
- A multi-user, multi-language secure web interface to change settings and monitor incoming data
|
- Compatible with both ESP8266 and ESP32
|
||||||
- A console, accessible via Serial and Telnet for more advanced monitoring
|
- A multi-user secure web interface to change settings and monitor the data
|
||||||
- Native support for Home Assistant, Domoticz and openHAB via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
- 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
|
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||||
- Easy first-time configuration via a web Captive Portal
|
- Easy first-time configuration via a web Captive Portal
|
||||||
- Support for more than [110+ EMS devices](https://emsesp.github.io/docs/All-Devices/) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways, switches, heat sources)
|
- Support for more than [70 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
||||||
|
|
||||||
## **Documentation**
|
## **Screenshots**
|
||||||
|
|
||||||
For the complete documentation on how to install, configure and get support visit the [EMS-ESP Wiki](https://emsesp.github.io/docs).
|
### Web Interface:
|
||||||
|
|
||||||
## **Support**
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| <img src="media/web_settings.PNG"> | <img src="media/web_status.PNG"> |
|
||||||
|
| <img src="media/web_devices.PNG"> | <img src="media/web_mqtt.PNG"> |
|
||||||
|
|
||||||
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
|
### Telnet Console:
|
||||||
|
<img src="media/console.PNG" width=80% height=80%>
|
||||||
|
|
||||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute or offer a small donation!
|
### In Home Assistant:
|
||||||
|
<img src="media/ha_lovelace.PNG" width=80% height=80%>
|
||||||
|
|
||||||
## **Demo**
|
## **Installing**
|
||||||
|
|
||||||
For a live demo of the Web UI click [here](https://ems-esp.derbyshire.nl) and log in with any username/password.
|
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.
|
||||||
|
|
||||||
## **Contributors ✨**
|
You can choose to use an pre-built firmware image or compile the code yourself:
|
||||||
|
|
||||||
EMS-ESP is a project owned and maintained by [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP).
|
* [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)
|
||||||
|
|
||||||
## **Libraries used**
|
## **Support Information**
|
||||||
|
|
||||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
If you're looking for support on **EMS-ESP** there are some options available:
|
||||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
|
||||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON
|
### Documentation
|
||||||
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client, with custom modifications from @MichaelDvP and @proddy
|
|
||||||
|
* [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**
|
## **License**
|
||||||
|
|
||||||
This program is licensed under GPL-3.0
|
This program is licensed under GPL-3.0
|
||||||
|
|
||||||
## **Screenshots**
|
|
||||||
|
|
||||||
### Web Interface
|
|
||||||
|
|
||||||
| | |
|
|
||||||
| ---------------------------------- | -------------------------------- |
|
|
||||||
| <img src="media/web_settings.png"> | <img src="media/web_status.png"> |
|
|
||||||
| <img src="media/web_devices.png"> | <img src="media/web_mqtt.png"> |
|
|
||||||
| <img src="media/web_edit.png"> | <img src="media/web_log.png"> |
|
|
||||||
|
|
||||||
### Telnet Console
|
|
||||||
|
|
||||||
<img src="media/console0.png" width=80% height=80%>
|
|
||||||
|
|
||||||
### In Home Assistant
|
|
||||||
|
|
||||||
<img src="media/ha_lovelace.png" width=80% height=80%>
|
|
||||||
|
|||||||
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,39 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
const url = 'http://10.10.10.135/api/system/commands';
|
|
||||||
const queryParams = {
|
|
||||||
entity: 'commands',
|
|
||||||
id: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalRequests = 1000000;
|
|
||||||
const requestsPerCount = 100;
|
|
||||||
|
|
||||||
let requestCount = 0;
|
|
||||||
|
|
||||||
function fetchData() {
|
|
||||||
axios
|
|
||||||
.get(url, { params: queryParams })
|
|
||||||
.then((response) => {
|
|
||||||
requestCount++;
|
|
||||||
|
|
||||||
if (requestCount % requestsPerCount === 0) {
|
|
||||||
console.log(`Requests completed: ${requestCount}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestCount < totalRequests) {
|
|
||||||
fetchData();
|
|
||||||
} else {
|
|
||||||
console.log('All requests completed.');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error making request:', error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start making requests
|
|
||||||
console.log(`Starting test`);
|
|
||||||
fetchData();
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Install:
|
|
||||||
# npm install -g autocannon
|
|
||||||
# yarn global add autocannon
|
|
||||||
#
|
|
||||||
# or run https://github.com/nearform/autocannon-ui
|
|
||||||
|
|
||||||
TEST_IP="10.10.10.135"
|
|
||||||
TEST_TIME=60
|
|
||||||
LOG_FILE=http-loadtest.log
|
|
||||||
TIMEOUT=10000
|
|
||||||
PROTOCOL=http
|
|
||||||
#PROTOCOL=https
|
|
||||||
|
|
||||||
if test -f "$LOG_FILE"; then
|
|
||||||
rm $LOG_FILE
|
|
||||||
fi
|
|
||||||
|
|
||||||
# for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20
|
|
||||||
for CONCURRENCY in 1
|
|
||||||
#for CONCURRENCY in 20
|
|
||||||
do
|
|
||||||
printf "\n\nCLIENTS: *** $CONCURRENCY ***\n\n" >> $LOG_FILE
|
|
||||||
echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/"
|
|
||||||
autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/" >> $LOG_FILE 2>&1
|
|
||||||
printf "\n\n----------------\n\n" >> $LOG_FILE
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/api/system/commands"
|
|
||||||
autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/api/system/commands" >> $LOG_FILE 2>&1
|
|
||||||
printf "\n\n----------------\n\n" >> $LOG_FILE
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/app/icon.png"
|
|
||||||
autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/app/icon.png" >> $LOG_FILE 2>&1
|
|
||||||
printf "\n\n----------------\n\n" >> $LOG_FILE
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"autocannon": "^7.14.0",
|
|
||||||
"axios": "^1.6.2",
|
|
||||||
"eventsource": "^2.0.2",
|
|
||||||
"ws": "^8.14.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4341
dump_entities.csv
4341
dump_entities.csv
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
|
||||||
otadata, data, ota, , 0x2000,
|
|
||||||
app0, app, ota_0, , 0x2A0000,
|
|
||||||
app1, app, ota_1, , 0x140000,
|
|
||||||
spiffs, data, spiffs, , 64K,
|
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
|
||||||
otadata, data, ota, , 0x2000,
|
|
||||||
app0, app, ota_0, , 0x7F0000,
|
|
||||||
app1, app, ota_1, , 0x7F0000,
|
|
||||||
spiffs, data, spiffs, , 64K,
|
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
|
||||||
otadata, data, ota, , 0x2000,
|
|
||||||
app0, app, ota_0, , 0x1F0000,
|
|
||||||
app1, app, ota_1, , 0x1F0000,
|
|
||||||
spiffs, data, spiffs, , 64K,
|
|
||||||
|
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,
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
|
||||||
otadata, data, ota, 0xE000, 0x2000,
|
|
||||||
app0, app, ota_0, 0x10000, 0x210000,
|
|
||||||
spiffs, data, spiffs, 0x220000, 0x10000,
|
|
||||||
|
@@ -7,8 +7,8 @@ build_flags =
|
|||||||
|
|
||||||
; Access point settings
|
; Access point settings
|
||||||
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
|
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
|
||||||
-D FACTORY_AP_SSID=\"ems-esp\"
|
-D FACTORY_AP_SSID=\"ems-esp\" ; 1-64 characters
|
||||||
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\"
|
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\" ; 8-64 characters
|
||||||
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
|
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
|
||||||
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
|
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
|
||||||
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
|
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
|
||||||
@@ -28,17 +28,17 @@ build_flags =
|
|||||||
; OTA settings
|
; OTA settings
|
||||||
-D FACTORY_OTA_PORT=8266
|
-D FACTORY_OTA_PORT=8266
|
||||||
-D FACTORY_OTA_PASSWORD=\"ems-esp-neo\"
|
-D FACTORY_OTA_PASSWORD=\"ems-esp-neo\"
|
||||||
-D FACTORY_OTA_ENABLED=false
|
-D FACTORY_OTA_ENABLED=true
|
||||||
|
|
||||||
; MQTT settings
|
; MQTT settings
|
||||||
-D FACTORY_MQTT_ENABLED=false
|
-D FACTORY_MQTT_ENABLED=false
|
||||||
-D FACTORY_MQTT_HOST=\"\"
|
-D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
|
||||||
-D FACTORY_MQTT_PORT=1883
|
-D FACTORY_MQTT_PORT=1883
|
||||||
-D FACTORY_MQTT_USERNAME=\"\"
|
-D FACTORY_MQTT_USERNAME=\"\"
|
||||||
-D FACTORY_MQTT_PASSWORD=\"\"
|
-D FACTORY_MQTT_PASSWORD=\"\"
|
||||||
-D FACTORY_MQTT_CLIENT_ID=\"ems-esp\"
|
-D FACTORY_MQTT_CLIENT_ID=\"ems-esp\"
|
||||||
-D FACTORY_MQTT_KEEP_ALIVE=60
|
-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
|
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
|
||||||
|
|
||||||
; JWT Secret
|
; 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
|
# Change the IP address to that of your ESP device to enable local development of the UI
|
||||||
REACT_APP_ALOVA_TIPS=0
|
|
||||||
|
# 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,12 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.yarn/
|
|
||||||
|
|
||||||
.prettierrc
|
|
||||||
.eslintrc*
|
|
||||||
env.d.ts
|
|
||||||
progmem-generator.js
|
|
||||||
unpack.ts
|
|
||||||
vite.config.ts
|
|
||||||
package.json
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"airbnb/hooks",
|
|
||||||
"airbnb-typescript",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react/jsx-runtime",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"plugin:import/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module",
|
|
||||||
"tsconfigRootDir": ".",
|
|
||||||
"project": ["tsconfig.json"]
|
|
||||||
},
|
|
||||||
"plugins": ["react", "@typescript-eslint", "autofix", "react-hooks"],
|
|
||||||
"settings": {
|
|
||||||
"import/resolver": {
|
|
||||||
"typescript": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"version": "18.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"object-shorthand": "error",
|
|
||||||
"no-console": "warn",
|
|
||||||
"@typescript-eslint/consistent-type-definitions": ["off", "type"],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
||||||
"@typescript-eslint/naming-convention": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
|
||||||
"@typescript-eslint/restrict-plus-operands": "off",
|
|
||||||
"@typescript-eslint/no-unused-expressions": "off",
|
|
||||||
"@typescript-eslint/no-implied-eval": "off",
|
|
||||||
"@typescript-eslint/no-misused-promises": "off",
|
|
||||||
"arrow-body-style": ["error", "as-needed"],
|
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"prefer": "type-imports"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"import/order": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"groups": ["builtin", "external", "parent", "sibling", "index", "object", "type"],
|
|
||||||
"pathGroups": [
|
|
||||||
{
|
|
||||||
"pattern": "@/**/**",
|
|
||||||
"group": "parent",
|
|
||||||
"position": "before"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"alphabetize": { "order": "asc" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// "autofix/no-unused-vars": [
|
|
||||||
// "error",
|
|
||||||
// {
|
|
||||||
// "argsIgnorePattern": "^_",
|
|
||||||
// "ignoreRestSiblings": true,
|
|
||||||
// "destructuredArrayIgnorePattern": "^_"
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
"react/self-closing-comp": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"component": true,
|
|
||||||
"html": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/ban-types": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"extendDefaults": true,
|
|
||||||
"types": {
|
|
||||||
"{}": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prettier/prettier": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"endOfLine": "auto"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
interface/.gitignore
vendored
7
interface/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.prettierrc
|
|
||||||
.yarn/
|
|
||||||
.typesafe-i18n.json
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"adapter": "react",
|
|
||||||
"baseLocale": "pl",
|
|
||||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
|
|
||||||
}
|
|
||||||
893
interface/.yarn/releases/yarn-4.0.2.cjs
vendored
893
interface/.yarn/releases/yarn-4.0.2.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
|||||||
compressionLevel: mixed
|
|
||||||
|
|
||||||
enableGlobalCache: false
|
|
||||||
|
|
||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
|
||||||
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,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,78 +1,63 @@
|
|||||||
{
|
{
|
||||||
"name": "EMS-ESP",
|
"name": "esp8266-react",
|
||||||
"version": "3.6.5",
|
"version": "0.1.0",
|
||||||
"description": "build EMS-ESP WebUI",
|
|
||||||
"homepage": "https://emsesp.github.io/docs",
|
|
||||||
"author": "proddy",
|
|
||||||
"license": "MIT",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"build-hosted": "typesafe-i18n --no-watch && vite build --mode hosted",
|
|
||||||
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"npm:mock-api\" \"vite preview\"",
|
|
||||||
"mock-api": "bun --watch ../mock-api/server.ts",
|
|
||||||
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-api\" \"vite\"",
|
|
||||||
"typesafe-i18n": "typesafe-i18n --no-watch",
|
|
||||||
"webUI": "node progmem-generator.js",
|
|
||||||
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
|
||||||
"lint": "eslint . --cache --fix"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alova/adapter-xhr": "^1.0.2",
|
"@material-ui/core": "^4.11.4",
|
||||||
"@babel/core": "^7.23.7",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@emotion/react": "^11.11.3",
|
"@types/lodash": "^4.14.168",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@types/node": "^15.0.1",
|
||||||
"@mui/icons-material": "^5.15.2",
|
"@types/react": "^17.0.4",
|
||||||
"@mui/material": "^5.15.2",
|
"@types/react-dom": "^17.0.3",
|
||||||
"@table-library/react-table-library": "4.1.7",
|
"@types/react-material-ui-form-validator": "^2.1.0",
|
||||||
"@types/imagemin": "^8.0.5",
|
"@types/react-router": "^5.1.13",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/node": "^20.10.6",
|
"compression-webpack-plugin": "^5.0.2",
|
||||||
"@types/react": "^18.2.46",
|
"express": "^4.17.1",
|
||||||
"@types/react-dom": "^18.2.18",
|
"jwt-decode": "^3.1.2",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"lodash": "^4.17.21",
|
||||||
"alova": "^2.16.2",
|
"mime-types": "^2.1.30",
|
||||||
"async-validator": "^4.2.5",
|
"notistack": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"parse-ms": "^3.0.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"react": "^17.0.2",
|
||||||
"lodash-es": "^4.17.21",
|
"react-dom": "^17.0.2",
|
||||||
"mime-types": "^2.1.35",
|
"react-dropzone": "^11.3.2",
|
||||||
"react": "latest",
|
"react-form-validator-core": "^1.1.1",
|
||||||
"react-dom": "latest",
|
"react-material-ui-form-validator": "^2.1.4",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-router": "^5.2.0",
|
||||||
"react-icons": "^4.12.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-scripts": "4.0.3",
|
||||||
"react-toastify": "^9.1.3",
|
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"typesafe-i18n": "^5.26.2",
|
"typescript": "4.2.4",
|
||||||
"typescript": "^5.3.3"
|
"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": {
|
"devDependencies": {
|
||||||
"@preact/compat": "^17.1.2",
|
"concurrently": "^6.0.1",
|
||||||
"@preact/preset-vite": "^2.7.0",
|
"http-proxy-middleware": "^1.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
"nodemon": "^2.0.7",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"npm-run-all": "^4.1.5",
|
||||||
"concurrently": "^8.2.2",
|
"react-app-rewired": "^2.1.8"
|
||||||
"eslint": "^8.56.0",
|
}
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
|
||||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
|
||||||
"eslint-plugin-autofix": "^1.1.0",
|
|
||||||
"eslint-plugin-import": "^2.29.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
|
||||||
"eslint-plugin-prettier": "alpha",
|
|
||||||
"eslint-plugin-react": "^7.33.2",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"preact": "^10.19.3",
|
|
||||||
"prettier": "^3.1.1",
|
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
|
||||||
"terser": "^5.26.0",
|
|
||||||
"vite": "^5.0.10",
|
|
||||||
"vite-plugin-imagemin": "^0.6.1",
|
|
||||||
"vite-tsconfig-paths": "^4.2.3"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@4.0.2"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,122 @@
|
|||||||
import { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } from 'fs';
|
const { resolve, relative, sep } = require('path');
|
||||||
import { resolve, relative, sep } from 'path';
|
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
|
||||||
import zlib from 'zlib';
|
var zlib = require('zlib');
|
||||||
import mime from 'mime-types';
|
var mime = require('mime-types');
|
||||||
|
|
||||||
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
const ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
|
||||||
const INDENT = ' ';
|
|
||||||
const outputPath = '../lib/framework/WWWData.h';
|
|
||||||
const sourcePath = './dist';
|
|
||||||
const bytesPerLine = 20;
|
|
||||||
var totalSize = 0;
|
|
||||||
|
|
||||||
const generateWWWClass = () =>
|
|
||||||
`typedef std::function<void(const String &, const String & contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
|
||||||
// Total size is ${totalSize} bytes
|
|
||||||
|
|
||||||
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)}}
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
|
|
||||||
function getFilesSync(dir, files = []) {
|
function getFilesSync(dir, files = []) {
|
||||||
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
|
||||||
const entryPath = resolve(dir, entry.name);
|
const entryPath = resolve(dir, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
getFilesSync(entryPath, files);
|
getFilesSync(entryPath, files);
|
||||||
} else {
|
} else {
|
||||||
files.push(entryPath);
|
files.push(entryPath);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coherseToBuffer(input) {
|
||||||
|
return Buffer.isBuffer(input) ? input : Buffer.from(input);
|
||||||
|
}
|
||||||
|
|
||||||
function cleanAndOpen(path) {
|
function cleanAndOpen(path) {
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
unlinkSync(path);
|
unlinkSync(path);
|
||||||
}
|
}
|
||||||
return createWriteStream(path, { flags: 'w+' });
|
return createWriteStream(path, { flags: "w+" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeFile = (relativeFilePath, buffer) => {
|
class ProgmemGenerator {
|
||||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
|
||||||
const mimeType = mime.lookup(relativeFilePath);
|
constructor(options = {}) {
|
||||||
var size = 0;
|
const { outputPath, bytesPerLine = 20, indent = " ", includes = ARDUINO_INCLUDES } = options;
|
||||||
writeStream.write('const uint8_t ' + variable + '[] = {');
|
this.options = { outputPath, bytesPerLine, indent, includes };
|
||||||
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
|
|
||||||
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
|
|
||||||
zipBuffer.forEach((b) => {
|
|
||||||
if (!(size % bytesPerLine)) {
|
|
||||||
writeStream.write('\n');
|
|
||||||
writeStream.write(indent);
|
|
||||||
}
|
|
||||||
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ',');
|
|
||||||
size++;
|
|
||||||
});
|
|
||||||
if (size % bytesPerLine) {
|
|
||||||
writeStream.write('\n');
|
|
||||||
}
|
}
|
||||||
writeStream.write('};\n\n');
|
|
||||||
fileInfo.push({
|
|
||||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
|
||||||
mimeType,
|
|
||||||
variable,
|
|
||||||
size
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
|
apply(compiler) {
|
||||||
totalSize += size;
|
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)}}
|
||||||
};
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// start
|
const writeWWWClass = () => {
|
||||||
console.log('Generating ' + outputPath + ' from ' + sourcePath);
|
writeStream.write(generateWWWClass());
|
||||||
const includes = ARDUINO_INCLUDES;
|
}
|
||||||
const indent = INDENT;
|
|
||||||
const fileInfo = [];
|
|
||||||
const writeStream = cleanAndOpen(resolve(outputPath));
|
|
||||||
|
|
||||||
// includes
|
writeIncludes();
|
||||||
writeStream.write(includes);
|
writeFiles();
|
||||||
|
writeWWWClass();
|
||||||
|
|
||||||
// process static files
|
writeStream.on('finish', () => {
|
||||||
const buildPath = resolve(sourcePath);
|
callback();
|
||||||
for (const filePath of getFilesSync(buildPath)) {
|
});
|
||||||
const readStream = readFileSync(filePath);
|
} finally {
|
||||||
const relativeFilePath = relative(buildPath, filePath);
|
writeStream.end();
|
||||||
writeFile(relativeFilePath, readStream);
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add class
|
module.exports = ProgmemGenerator;
|
||||||
writeStream.write(generateWWWClass());
|
|
||||||
|
|
||||||
// end
|
|
||||||
writeStream.end();
|
|
||||||
|
|
||||||
console.log('Total size: ' + totalSize / 1000 + ' KB');
|
|
||||||
|
|||||||
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",
|
"name":"EMS-ESP",
|
||||||
"icons": [
|
"icons":[
|
||||||
{
|
{
|
||||||
"src": "/app/icon.png",
|
"src":"/app/icon.png",
|
||||||
"sizes": "48x48 72x72 96x96 128x128 256x256"
|
"sizes":"48x48 72x72 96x96 128x128 256x256"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/",
|
"start_url":"/",
|
||||||
"display": "fullscreen",
|
"display":"fullscreen",
|
||||||
"orientation": "any"
|
"orientation":"any"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
/*
|
/* Just supporting latin due to size constrains on the esp chip */
|
||||||
* Uses font-size 400 (normal) only and Latin (plus extra unicode chars) to keep flash memory to a minimum
|
@font-face {
|
||||||
* View fonts on https://fonts.google.com/
|
font-family: 'Roboto';
|
||||||
* Download woff2 using e.g. https://fonts.googleapis.com/css2?family=Lato or https://fonts.googleapis.com/css2?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-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
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');
|
||||||
src:
|
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;
|
||||||
local('Roboto'),
|
}
|
||||||
local('Roboto-Regular'),
|
@font-face {
|
||||||
url(../fonts/re.woff2) format('woff2');
|
font-family: 'Roboto';
|
||||||
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,
|
font-style: normal;
|
||||||
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,
|
font-weight: 500;
|
||||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
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,52 +1,50 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import React, { Component, RefObject } from 'react';
|
||||||
import { ToastContainer, Slide } from 'react-toastify';
|
import { Redirect, Route, Switch } from 'react-router';
|
||||||
|
import { SnackbarProvider } from 'notistack';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.min.css';
|
import { IconButton } from '@material-ui/core';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
|
||||||
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
import AppRouting from './AppRouting';
|
||||||
import { FeaturesLoader } from './contexts/features';
|
import CustomMuiTheme from './CustomMuiTheme';
|
||||||
import type { FC } from 'react';
|
import { PROJECT_NAME } from './api';
|
||||||
import AppRouting from 'AppRouting';
|
import FeaturesWrapper from './features/FeaturesWrapper';
|
||||||
import CustomTheme from 'CustomTheme';
|
|
||||||
|
|
||||||
import TypesafeI18n from 'i18n/i18n-react';
|
// this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid.
|
||||||
import { detectLocale } from 'i18n/i18n-util';
|
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
|
||||||
|
|
||||||
const detectedLocale = detectLocale(localStorageDetector);
|
class App extends Component {
|
||||||
|
|
||||||
const App: FC = () => {
|
notistackRef: RefObject<any> = React.createRef();
|
||||||
const [wasLoaded, setWasLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
componentDidMount() {
|
||||||
void loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
|
document.title = PROJECT_NAME;
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
if (!wasLoaded) return null;
|
onClickDismiss = (key: string | number | undefined) => () => {
|
||||||
|
this.notistackRef.current.closeSnackbar(key);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
render() {
|
||||||
<TypesafeI18n locale={detectedLocale}>
|
return (
|
||||||
<CustomTheme>
|
<CustomMuiTheme>
|
||||||
<FeaturesLoader>
|
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
<AppRouting />
|
ref={this.notistackRef}
|
||||||
</FeaturesLoader>
|
action={(key) => (
|
||||||
<ToastContainer
|
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||||
position="bottom-left"
|
<CloseIcon />
|
||||||
autoClose={3000}
|
</IconButton>
|
||||||
hideProgressBar={false}
|
)}>
|
||||||
newestOnTop={false}
|
<FeaturesWrapper>
|
||||||
closeOnClick={true}
|
<Switch>
|
||||||
rtl={false}
|
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
||||||
pauseOnFocusLoss={false}
|
<Route component={AppRouting} />
|
||||||
draggable={false}
|
</Switch>
|
||||||
pauseOnHover={false}
|
</FeaturesWrapper>
|
||||||
transition={Slide}
|
</SnackbarProvider>
|
||||||
closeButton={false}
|
</CustomMuiTheme>
|
||||||
theme="light"
|
);
|
||||||
/>
|
}
|
||||||
</CustomTheme>
|
}
|
||||||
</TypesafeI18n>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App
|
||||||
|
|||||||
@@ -1,73 +1,59 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { Switch, Redirect } from 'react-router';
|
||||||
|
|
||||||
import { Route, Routes, Navigate, useLocation } from 'react-router-dom';
|
import * as Authentication from './authentication/Authentication';
|
||||||
|
import AuthenticationWrapper from './authentication/AuthenticationWrapper';
|
||||||
|
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
|
||||||
|
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
|
||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import SignIn from './SignIn';
|
||||||
import type { FC } from 'react';
|
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 AuthenticatedRouting from 'AuthenticatedRouting';
|
import { PROJECT_PATH } from './api';
|
||||||
import SignIn from 'SignIn';
|
import Mqtt from './mqtt/Mqtt';
|
||||||
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
||||||
|
import { Features } from './features/types';
|
||||||
|
|
||||||
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/network/";
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
class AppRouting extends Component<WithFeaturesProps> {
|
||||||
|
|
||||||
interface SecurityRedirectProps {
|
componentDidMount() {
|
||||||
message: string;
|
Authentication.clearLoginRedirect();
|
||||||
signOut?: boolean;
|
}
|
||||||
|
|
||||||
|
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: FC<SecurityRedirectProps> = ({ message, signOut }) => {
|
export default withFeatures(AppRouting);
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
|
||||||
useEffect(() => {
|
|
||||||
signOut && authenticationContext.signOut(false);
|
|
||||||
toast.success(message);
|
|
||||||
}, [message, signOut, authenticationContext]);
|
|
||||||
return <Navigate to="/" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RemoveTrailingSlashes = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
return (
|
|
||||||
location.pathname.match('/.*/$') && (
|
|
||||||
<Navigate
|
|
||||||
to={{
|
|
||||||
pathname: location.pathname.replace(/\/+$/, ''),
|
|
||||||
search: location.search
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppRouting: FC = () => {
|
|
||||||
const { LL } = useI18nContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Authentication>
|
|
||||||
<RemoveTrailingSlashes />
|
|
||||||
<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;
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
|
||||||
import Dashboard from './project/Dashboard';
|
|
||||||
import Help from './project/Help';
|
|
||||||
import Settings from './project/Settings';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import { Layout, RequireAdmin } from 'components';
|
|
||||||
import AccessPoint from 'framework/ap/AccessPoint';
|
|
||||||
import Mqtt from 'framework/mqtt/Mqtt';
|
|
||||||
import NetworkConnection from 'framework/network/NetworkConnection';
|
|
||||||
import NetworkTime from 'framework/ntp/NetworkTime';
|
|
||||||
import Security from 'framework/security/Security';
|
|
||||||
import System from 'framework/system/System';
|
|
||||||
|
|
||||||
const AuthenticatedRouting: FC = () => (
|
|
||||||
// const location = useLocation();
|
|
||||||
// const navigate = useNavigate();
|
|
||||||
// const handleApiResponseError = useCallback(
|
|
||||||
// (error: AxiosError) => {
|
|
||||||
// if (error.response && error.response.status === 401) {
|
|
||||||
// AuthenticationApi.storeLoginRedirect(location);
|
|
||||||
// navigate('/unauthorized');
|
|
||||||
// }
|
|
||||||
// return Promise.reject(error);
|
|
||||||
// },
|
|
||||||
// [location, navigate]
|
|
||||||
// );
|
|
||||||
// useEffect(() => {
|
|
||||||
// const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
|
|
||||||
// return () => AXIOS.interceptors.response.eject(axiosHandlerId);
|
|
||||||
// }, [handleApiResponseError]);
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
|
||||||
<Route
|
|
||||||
path="/settings/*"
|
|
||||||
element={
|
|
||||||
<RequireAdmin>
|
|
||||||
<Settings />
|
|
||||||
</RequireAdmin>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/help/*" element={<Help />} />
|
|
||||||
|
|
||||||
<Route path="/network/*" element={<NetworkConnection />} />
|
|
||||||
<Route path="/ap/*" element={<AccessPoint />} />
|
|
||||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
|
||||||
<Route path="/mqtt/*" element={<Mqtt />} />
|
|
||||||
<Route
|
|
||||||
path="/security/*"
|
|
||||||
element={
|
|
||||||
<RequireAdmin>
|
|
||||||
<Security />
|
|
||||||
</RequireAdmin>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/system/*" element={<System />} />
|
|
||||||
<Route path="/*" element={<Navigate to="/" />} />
|
|
||||||
</Routes>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default AuthenticatedRouting;
|
|
||||||
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,44 +0,0 @@
|
|||||||
import { CssBaseline } from '@mui/material';
|
|
||||||
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
|
||||||
|
|
||||||
export const dialogStyle = {
|
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
borderRadius: '8px',
|
|
||||||
borderColor: '#565656',
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderWidth: '1px'
|
|
||||||
},
|
|
||||||
backdropFilter: 'blur(1px)'
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = responsiveFontSizes(
|
|
||||||
createTheme({
|
|
||||||
typography: {
|
|
||||||
fontSize: 13
|
|
||||||
},
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
secondary: {
|
|
||||||
main: '#2196f3' // blue[500]
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
main: '#607d8b' // blueGrey[500]
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
disabled: '#eee' // white
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const CustomTheme: FC<RequiredChildrenProps> = ({ children }) => (
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<CssBaseline />
|
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default CustomTheme;
|
|
||||||
@@ -1,198 +1,147 @@
|
|||||||
import ForwardIcon from '@mui/icons-material/Forward';
|
import React, { Component } from 'react';
|
||||||
import { Box, Paper, Typography, MenuItem, TextField, Button } from '@mui/material';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import { useRequest } from 'alova';
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
import { useContext, useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { FeaturesContext } from './contexts/features';
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
|
|
||||||
import type { Locales } from 'i18n/i18n-types';
|
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
|
||||||
import type { ChangeEventHandler, FC } from 'react';
|
import { Paper, Typography, Fab } from '@material-ui/core';
|
||||||
import type { SignInRequest } from 'types';
|
import ForwardIcon from '@material-ui/icons/Forward';
|
||||||
import * as AuthenticationApi from 'api/authentication';
|
|
||||||
import { PROJECT_NAME } from 'api/env';
|
|
||||||
|
|
||||||
import { ValidatedPasswordField, ValidatedTextField } from 'components';
|
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
||||||
import { AuthenticationContext } from 'contexts/authentication';
|
import {PasswordValidator} from './components';
|
||||||
|
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||||
|
|
||||||
import DEflag from 'i18n/DE.svg';
|
const styles = (theme: Theme) => createStyles({
|
||||||
import FRflag from 'i18n/FR.svg';
|
signInPage: {
|
||||||
import GBflag from 'i18n/GB.svg';
|
display: "flex",
|
||||||
import ITflag from 'i18n/IT.svg';
|
height: "100vh",
|
||||||
import NLflag from 'i18n/NL.svg';
|
margin: "auto",
|
||||||
import NOflag from 'i18n/NO.svg';
|
padding: theme.spacing(2),
|
||||||
import PLflag from 'i18n/PL.svg';
|
justifyContent: "center",
|
||||||
import SKflag from 'i18n/SK.svg';
|
flexDirection: "column",
|
||||||
import SVflag from 'i18n/SV.svg';
|
maxWidth: theme.breakpoints.values.sm
|
||||||
import TRflag from 'i18n/TR.svg';
|
},
|
||||||
import { I18nContext } from 'i18n/i18n-react';
|
signInPanel: {
|
||||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
textAlign: "center",
|
||||||
import { onEnterCallback, updateValue } from 'utils';
|
padding: theme.spacing(2),
|
||||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
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 SignIn: FC = () => {
|
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
|
||||||
|
|
||||||
const { LL, setLocale, locale } = useContext(I18nContext);
|
interface SignInState {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
processing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const { features } = useContext(FeaturesContext);
|
class SignIn extends Component<SignInProps, SignInState> {
|
||||||
|
|
||||||
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
constructor(props: SignInProps) {
|
||||||
username: '',
|
super(props);
|
||||||
password: ''
|
this.state = {
|
||||||
});
|
username: '',
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
password: '',
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
processing: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), {
|
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
immediate: false
|
const { name, value } = event.currentTarget;
|
||||||
});
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
onSuccess((response) => {
|
[name]: value,
|
||||||
if (response.data) {
|
}))
|
||||||
authenticationContext.signIn(response.data.access_token);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateLoginRequestValue = updateValue(setSignInRequest);
|
|
||||||
|
|
||||||
const signIn = async () => {
|
|
||||||
await callSignIn(signInRequest).catch((event) => {
|
|
||||||
if (event.message === 'Unauthorized') {
|
|
||||||
toast.warning(LL.INVALID_LOGIN());
|
|
||||||
} else {
|
|
||||||
toast.error(LL.ERROR() + ' ' + event.message);
|
|
||||||
}
|
|
||||||
setProcessing(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateAndSignIn = async () => {
|
onSubmit = () => {
|
||||||
setProcessing(true);
|
const { username, password } = this.state;
|
||||||
SIGN_IN_REQUEST_VALIDATOR.messages({
|
const { authenticationContext } = this.props;
|
||||||
required: LL.IS_REQUIRED('%s')
|
this.setState({ processing: true });
|
||||||
});
|
fetch(SIGN_IN_ENDPOINT, {
|
||||||
try {
|
method: 'POST',
|
||||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
body: JSON.stringify({ username, password }),
|
||||||
await signIn();
|
headers: new Headers({
|
||||||
} catch (errors: any) {
|
'Content-Type': 'application/json'
|
||||||
setFieldErrors(errors);
|
})
|
||||||
setProcessing(false);
|
})
|
||||||
}
|
.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
|
}
|
||||||
const loc = target.value as Locales;
|
|
||||||
localStorage.setItem('lang', loc);
|
|
||||||
await loadLocaleAsync(loc);
|
|
||||||
setLocale(loc);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
|
||||||
<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>
|
|
||||||
<Typography variant="subtitle2">{features.version}</Typography>
|
|
||||||
|
|
||||||
<TextField name="locale" variant="outlined" value={locale} onChange={onLocaleSelected} size="small" select>
|
|
||||||
<MenuItem key="de" value="de">
|
|
||||||
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
DE
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="en" value="en">
|
|
||||||
<img src={GBflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
EN
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="fr" value="fr">
|
|
||||||
<img src={FRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
FR
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="it" value="it">
|
|
||||||
<img src={ITflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
IT
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="nl" value="nl">
|
|
||||||
<img src={NLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
NL
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="no" value="no">
|
|
||||||
<img src={NOflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
NO
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="pl" value="pl">
|
|
||||||
<img src={PLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
PL
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="sk" value="sk">
|
|
||||||
<img src={SKflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
SK
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="sv" value="sv">
|
|
||||||
<img src={SVflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
SV
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="tr" value="tr">
|
|
||||||
<img src={TRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
TR
|
|
||||||
</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
<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;
|
|
||||||
|
|||||||
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);
|
||||||
@@ -21,9 +21,6 @@ export interface APSettings {
|
|||||||
provision_mode: APProvisionMode;
|
provision_mode: APProvisionMode;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
password: string;
|
password: string;
|
||||||
channel: number;
|
|
||||||
ssid_hidden: boolean;
|
|
||||||
max_clients: number;
|
|
||||||
local_ip: string;
|
local_ip: string;
|
||||||
gateway_ip: string;
|
gateway_ip: string;
|
||||||
subnet_mask: 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,7 +0,0 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
|
||||||
|
|
||||||
import type { APSettings, APStatus } from 'types';
|
|
||||||
|
|
||||||
export const readAPStatus = () => alovaInstance.Get<APStatus>('/rest/apStatus');
|
|
||||||
export const readAPSettings = () => alovaInstance.Get<APSettings>('/rest/apSettings');
|
|
||||||
export const updateAPSettings = (data: APSettings) => alovaInstance.Post<APSettings>('/rest/apSettings', data);
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { jwtDecode } from 'jwt-decode';
|
|
||||||
import { ACCESS_TOKEN, alovaInstance } from './endpoints';
|
|
||||||
import type * as H from 'history';
|
|
||||||
import type { Path } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { Me, SignInRequest, SignInResponse } from 'types';
|
|
||||||
|
|
||||||
export const SIGN_IN_PATHNAME = 'loginPathname';
|
|
||||||
export const SIGN_IN_SEARCH = 'loginSearch';
|
|
||||||
|
|
||||||
export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization');
|
|
||||||
export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request);
|
|
||||||
|
|
||||||
export function getStorage() {
|
|
||||||
return localStorage || sessionStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function storeLoginRedirect(location?: H.Location) {
|
|
||||||
if (location) {
|
|
||||||
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
|
|
||||||
getStorage().setItem(SIGN_IN_SEARCH, location.search);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearLoginRedirect() {
|
|
||||||
getStorage().removeItem(SIGN_IN_PATHNAME);
|
|
||||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchLoginRedirect(): Partial<Path> {
|
|
||||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
|
||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
|
||||||
clearLoginRedirect();
|
|
||||||
return {
|
|
||||||
pathname: signInPathname || `/dashboard`,
|
|
||||||
search: (signInPathname && signInSearch) || undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clearAccessToken = () => localStorage.removeItem(ACCESS_TOKEN);
|
|
||||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
|
|
||||||
|
|
||||||
export function addAccessTokenParameter(url: string) {
|
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
|
||||||
if (!accessToken) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
|
|
||||||
return parsedUrl.toString();
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { xhrRequestAdapter } from '@alova/adapter-xhr';
|
|
||||||
import { createAlova } from 'alova';
|
|
||||||
import ReactHook from 'alova/react';
|
|
||||||
import { unpack } from '../api/unpack';
|
|
||||||
|
|
||||||
export const ACCESS_TOKEN = 'access_token';
|
|
||||||
|
|
||||||
const host = window.location.host;
|
|
||||||
export const WEB_SOCKET_ROOT = 'ws://' + host + '/ws/';
|
|
||||||
export const EVENT_SOURCE_ROOT = 'http://' + host + '/es/';
|
|
||||||
|
|
||||||
export const alovaInstance = createAlova({
|
|
||||||
statesHook: ReactHook,
|
|
||||||
timeout: 3000, // 3 seconds but throwing a timeout error
|
|
||||||
localCache: null,
|
|
||||||
// localCache: {
|
|
||||||
// GET: {
|
|
||||||
// mode: 'placeholder', // see https://alova.js.org/learning/response-cache/#cache-replaceholder-mode
|
|
||||||
// expire: 2000
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
requestAdapter: xhrRequestAdapter(),
|
|
||||||
beforeRequest(method) {
|
|
||||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
|
||||||
method.config.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
responded: {
|
|
||||||
onSuccess: async (response) => {
|
|
||||||
// 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 = await response.data;
|
|
||||||
if (response.data instanceof ArrayBuffer) {
|
|
||||||
return unpack(data);
|
|
||||||
}
|
|
||||||
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: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
|
|
||||||
statesHook: ReactHook,
|
|
||||||
requestAdapter: xhrRequestAdapter()
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const PROJECT_NAME = 'EMS-ESP';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
|
||||||
|
|
||||||
import type { Features } from 'types';
|
|
||||||
|
|
||||||
export const readFeatures = () => alovaInstance.Get<Features>('/rest/features');
|
|
||||||
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,6 +0,0 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
|
||||||
import type { MqttSettings, MqttStatus } from 'types';
|
|
||||||
|
|
||||||
export const readMqttStatus = () => alovaInstance.Get<MqttStatus>('/rest/mqttStatus');
|
|
||||||
export const readMqttSettings = () => alovaInstance.Get<MqttSettings>('/rest/mqttSettings');
|
|
||||||
export const updateMqttSettings = (data: MqttSettings) => alovaInstance.Post<MqttSettings>('/rest/mqttSettings', data);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
|
||||||
|
|
||||||
import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types';
|
|
||||||
|
|
||||||
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatus>('/rest/networkStatus');
|
|
||||||
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
|
||||||
export const listNetworks = () =>
|
|
||||||
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
|
||||||
name: 'listNetworks',
|
|
||||||
timeout: 20000 // timeout 20 seconds
|
|
||||||
});
|
|
||||||
export const readNetworkSettings = () =>
|
|
||||||
alovaInstance.Get<NetworkSettings>('/rest/networkSettings', { name: 'networkSettings' });
|
|
||||||
export const updateNetworkSettings = (wifiSettings: NetworkSettings) =>
|
|
||||||
alovaInstance.Post<NetworkSettings>('/rest/networkSettings', wifiSettings);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
|
||||||
import type { NTPSettings, NTPStatus, Time } from 'types';
|
|
||||||
|
|
||||||
export const readNTPStatus = () => alovaInstance.Get<NTPStatus>('/rest/ntpStatus');
|
|
||||||
export const readNTPSettings = () =>
|
|
||||||
alovaInstance.Get<NTPSettings>('/rest/ntpSettings', {
|
|
||||||
name: 'ntpSettings'
|
|
||||||
});
|
|
||||||
export const updateNTPSettings = (data: NTPSettings) => alovaInstance.Post<NTPSettings>('/rest/ntpSettings', data);
|
|
||||||
|
|
||||||
export const updateTime = (data: Time) => alovaInstance.Post<Time>('/rest/time', data);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
|
||||||
|
|
||||||
import type { SecuritySettings, Token } from 'types';
|
|
||||||
|
|
||||||
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings');
|
|
||||||
|
|
||||||
export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
|
|
||||||
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
|
||||||
|
|
||||||
export const generateToken = (username?: string) =>
|
|
||||||
alovaInstance.Get<Token>('/rest/generateToken', {
|
|
||||||
params: { username }
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
|
||||||
import type { OTASettings, SystemStatus, LogSettings } from 'types';
|
|
||||||
|
|
||||||
// SystemStatus - also used to ping in Restart monitor for pinging
|
|
||||||
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
|
||||||
|
|
||||||
// commands
|
|
||||||
export const restart = () => alovaInstance.Post('/rest/restart');
|
|
||||||
export const partition = () => alovaInstance.Post('/rest/partition');
|
|
||||||
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
|
|
||||||
|
|
||||||
// OTA
|
|
||||||
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
|
|
||||||
export const updateOTASettings = (data: any) => alovaInstance.Post('/rest/otaSettings', data);
|
|
||||||
|
|
||||||
// SystemLog
|
|
||||||
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
|
||||||
export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data);
|
|
||||||
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
|
|
||||||
|
|
||||||
// Get versions from github
|
|
||||||
export const getStableVersion = () =>
|
|
||||||
alovaInstanceGH.Get('latest', {
|
|
||||||
transformData(response: any) {
|
|
||||||
return response.data.name.substring(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export const getDevVersion = () =>
|
|
||||||
alovaInstanceGH.Get('tags/latest', {
|
|
||||||
transformData(response: any) {
|
|
||||||
return response.data.name.split(/\s+/).splice(-1)[0].substring(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
enableUpload: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
40
interface/src/authentication/AuthenticatedRoute.tsx
Normal file
40
interface/src/authentication/AuthenticatedRoute.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||||
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
|
import * as Authentication from './Authentication';
|
||||||
|
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
|
||||||
|
|
||||||
|
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
|
||||||
|
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
|
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
||||||
|
const { location } = this.props;
|
||||||
|
const renderComponent: RenderComponent = (props) => {
|
||||||
|
if (authenticationContext.me) {
|
||||||
|
return (
|
||||||
|
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
|
||||||
|
<Component {...props} />
|
||||||
|
</AuthenticatedContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Authentication.storeLoginRedirect(location);
|
||||||
|
enqueueSnackbar("Please sign in to continue", { variant: 'info' });
|
||||||
|
return (
|
||||||
|
<Redirect to='/' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Route {...rest} render={renderComponent} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
||||||
114
interface/src/authentication/Authentication.ts
Normal file
114
interface/src/authentication/Authentication.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import * as H from 'history';
|
||||||
|
|
||||||
|
import history from '../history';
|
||||||
|
import { Features } from '../features/types';
|
||||||
|
import { getDefaultRoute } from '../AppRouting';
|
||||||
|
|
||||||
|
export const ACCESS_TOKEN = 'access_token';
|
||||||
|
export const SIGN_IN_PATHNAME = 'signInPathname';
|
||||||
|
export const SIGN_IN_SEARCH = 'signInSearch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
|
||||||
|
*/
|
||||||
|
export function getStorage() {
|
||||||
|
return localStorage || sessionStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeLoginRedirect(location?: H.Location) {
|
||||||
|
if (location) {
|
||||||
|
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
|
||||||
|
getStorage().setItem(SIGN_IN_SEARCH, location.search);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLoginRedirect() {
|
||||||
|
getStorage().removeItem(SIGN_IN_PATHNAME);
|
||||||
|
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
|
||||||
|
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||||
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
|
clearLoginRedirect();
|
||||||
|
return {
|
||||||
|
pathname: signInPathname || getDefaultRoute(features),
|
||||||
|
search: (signInPathname && signInSearch) || undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the normal fetch routene with one with provides the access token if present.
|
||||||
|
*/
|
||||||
|
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||||
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
|
if (accessToken) {
|
||||||
|
params = params || {};
|
||||||
|
params.credentials = 'include';
|
||||||
|
params.headers = {
|
||||||
|
...params.headers,
|
||||||
|
"Authorization": 'Bearer ' + accessToken
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return fetch(url, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
|
||||||
|
* for a single file upload and takes care of adding the Authroization header and redirecting on
|
||||||
|
* authroization errors as we do for normal fetch operations.
|
||||||
|
*/
|
||||||
|
export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, file: File, onProgress: (event: ProgressEvent<EventTarget>) => void): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
xhr.open("POST", url, true);
|
||||||
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
|
if (accessToken) {
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
|
||||||
|
}
|
||||||
|
xhr.upload.onprogress = onProgress;
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status === 401 || xhr.status === 403) {
|
||||||
|
history.push("/unauthorized");
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
|
||||||
|
reject(new DOMException('Error', 'UploadError'));
|
||||||
|
};
|
||||||
|
xhr.onabort = function () {
|
||||||
|
reject(new DOMException('Aborted', 'AbortError'));
|
||||||
|
};
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the normal fetch routene which redirects on 401 response.
|
||||||
|
*/
|
||||||
|
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||||
|
return new Promise<Response>((resolve, reject) => {
|
||||||
|
authorizedFetch(url, params).then(response => {
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
history.push("/unauthorized");
|
||||||
|
} else {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addAccessTokenParameter(url: string) {
|
||||||
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
|
if (!accessToken) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
|
||||||
|
return parsedUrl.toString();
|
||||||
|
}
|
||||||
60
interface/src/authentication/AuthenticationContext.tsx
Normal file
60
interface/src/authentication/AuthenticationContext.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface Me {
|
||||||
|
username: string;
|
||||||
|
admin: boolean;
|
||||||
|
version: string; // proddy added
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationContextValue {
|
||||||
|
refresh: () => void;
|
||||||
|
signIn: (accessToken: string) => void;
|
||||||
|
signOut: () => void;
|
||||||
|
me?: Me;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
|
||||||
|
export const AuthenticationContext = React.createContext(
|
||||||
|
AuthenticationContextDefaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface AuthenticationContextProps {
|
||||||
|
authenticationContext: AuthenticationContextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
|
||||||
|
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AuthenticationContext.Consumer>
|
||||||
|
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
|
||||||
|
</AuthenticationContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
||||||
|
me: Me;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
|
||||||
|
export const AuthenticatedContext = React.createContext(
|
||||||
|
AuthenticatedContextDefaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface AuthenticatedContextProps {
|
||||||
|
authenticatedContext: AuthenticatedContextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
|
||||||
|
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AuthenticatedContext.Consumer>
|
||||||
|
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
|
||||||
|
</AuthenticatedContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
110
interface/src/authentication/AuthenticationWrapper.tsx
Normal file
110
interface/src/authentication/AuthenticationWrapper.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
import jwtDecode from 'jwt-decode';
|
||||||
|
|
||||||
|
import history from '../history'
|
||||||
|
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||||
|
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||||
|
import { AuthenticationContext, AuthenticationContextValue, Me } from './AuthenticationContext';
|
||||||
|
import FullScreenLoading from '../components/FullScreenLoading';
|
||||||
|
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||||
|
|
||||||
|
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
|
||||||
|
|
||||||
|
interface AuthenticationWrapperState {
|
||||||
|
context: AuthenticationContextValue;
|
||||||
|
initialized: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||||
|
|
||||||
|
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
||||||
|
|
||||||
|
constructor(props: AuthenticationWrapperProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
context: {
|
||||||
|
refresh: this.refresh,
|
||||||
|
signIn: this.signIn,
|
||||||
|
signOut: this.signOut,
|
||||||
|
},
|
||||||
|
initialized: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
return (
|
||||||
|
<AuthenticationContext.Provider value={this.state.context}>
|
||||||
|
{this.props.children}
|
||||||
|
</AuthenticationContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContentLoading() {
|
||||||
|
return (
|
||||||
|
<FullScreenLoading />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh = () => {
|
||||||
|
// commented out, always need security - proddy
|
||||||
|
// if (!this.props.features.security) {
|
||||||
|
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
||||||
|
if (accessToken) {
|
||||||
|
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||||
|
.then(response => {
|
||||||
|
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||||
|
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||||
|
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
||||||
|
variant: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signIn = (accessToken: string) => {
|
||||||
|
try {
|
||||||
|
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||||
|
const me: Me = decodeMeJWT(accessToken);
|
||||||
|
this.setState({ context: { ...this.state.context, me } });
|
||||||
|
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||||
|
throw new Error("Failed to parse JWT " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signOut = () => {
|
||||||
|
getStorage().removeItem(ACCESS_TOKEN);
|
||||||
|
this.setState({
|
||||||
|
context: {
|
||||||
|
...this.state.context,
|
||||||
|
me: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar("You have signed out", { variant: 'success', });
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
||||||
32
interface/src/authentication/UnauthenticatedRoute.tsx
Normal file
32
interface/src/authentication/UnauthenticatedRoute.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||||
|
|
||||||
|
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
|
||||||
|
import * as Authentication from './Authentication';
|
||||||
|
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||||
|
|
||||||
|
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
|
||||||
|
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
|
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
||||||
|
const renderComponent: RenderComponent = (props) => {
|
||||||
|
if (authenticationContext.me) {
|
||||||
|
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
|
||||||
|
}
|
||||||
|
if (Component) {
|
||||||
|
return (<Component {...props} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Route {...rest} render={renderComponent} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withFeatures(withAuthenticationContext(UnauthenticatedRoute));
|
||||||
6
interface/src/authentication/index.ts
Normal file
6
interface/src/authentication/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { default as AuthenticatedRoute } from './AuthenticatedRoute';
|
||||||
|
export { default as AuthenticationWrapper } from './AuthenticationWrapper';
|
||||||
|
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute';
|
||||||
|
|
||||||
|
export * from './Authentication';
|
||||||
|
export * from './AuthenticationContext';
|
||||||
59
interface/src/components/ApplicationError.tsx
Normal file
59
interface/src/components/ApplicationError.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
|
||||||
|
import WarningIcon from "@material-ui/icons/Warning"
|
||||||
|
|
||||||
|
const styles = makeStyles(
|
||||||
|
{
|
||||||
|
siteErrorPage: {
|
||||||
|
display: "flex",
|
||||||
|
height: "100vh",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexDirection: "column"
|
||||||
|
},
|
||||||
|
siteErrorPagePanel: {
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "280px 0 40px 0",
|
||||||
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundPosition: "50% 40px",
|
||||||
|
backgroundSize: "200px auto",
|
||||||
|
width: "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ApplicationErrorProps {
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
|
||||||
|
const classes = styles();
|
||||||
|
return (
|
||||||
|
<div className={classes.siteErrorPage}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}>
|
||||||
|
<WarningIcon fontSize="large" color="error" />
|
||||||
|
<Box ml={2}>
|
||||||
|
<Typography variant="h4">
|
||||||
|
Application error
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="subtitle1" gutterBottom>
|
||||||
|
Failed to configure the application, please refresh to try again.
|
||||||
|
</Typography>
|
||||||
|
{error &&
|
||||||
|
(
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Error: {error}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplicationError;
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { FormControlLabel } from '@mui/material';
|
import React, { FC } from "react";
|
||||||
import type { FormControlLabelProps } from '@mui/material';
|
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||||
<div>
|
<div>
|
||||||
<FormControlLabel {...props} />
|
<FormControlLabel {...props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default BlockFormControlLabel;
|
export default BlockFormControlLabel;
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Box } from '@mui/material';
|
|
||||||
import type { BoxProps } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
'& button, & a, & .MuiCard-root': {
|
|
||||||
mt: 2,
|
|
||||||
mx: 0.6,
|
|
||||||
'&:last-child': {
|
|
||||||
mr: 0
|
|
||||||
},
|
|
||||||
'&:first-of-type': {
|
|
||||||
ml: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ButtonRow;
|
|
||||||
11
interface/src/components/ErrorButton.tsx
Normal file
11
interface/src/components/ErrorButton.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Button, styled } from "@material-ui/core";
|
||||||
|
|
||||||
|
const ErrorButton = styled(Button)(({ theme }) => ({
|
||||||
|
color: theme.palette.getContrastText(theme.palette.error.main),
|
||||||
|
backgroundColor: theme.palette.error.main,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.error.dark,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default ErrorButton;
|
||||||
7
interface/src/components/FormActions.tsx
Normal file
7
interface/src/components/FormActions.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { styled, Box } from "@material-ui/core";
|
||||||
|
|
||||||
|
const FormActions = styled(Box)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1)
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default FormActions;
|
||||||
13
interface/src/components/FormButton.tsx
Normal file
13
interface/src/components/FormButton.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Button, styled } from "@material-ui/core";
|
||||||
|
|
||||||
|
const FormButton = styled(Button)(({ theme }) => ({
|
||||||
|
margin: theme.spacing(0, 1),
|
||||||
|
'&:last-child': {
|
||||||
|
marginRight: 0,
|
||||||
|
},
|
||||||
|
'&:first-child': {
|
||||||
|
marginLeft: 0,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default FormButton;
|
||||||
32
interface/src/components/FullScreenLoading.tsx
Normal file
32
interface/src/components/FullScreenLoading.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||||
|
import { Typography, Theme } from '@material-ui/core';
|
||||||
|
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||||
|
fullScreenLoading: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100vh",
|
||||||
|
flexDirection: "column"
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
margin: theme.spacing(4),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FullScreenLoading = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={classes.fullScreenLoading}>
|
||||||
|
<CircularProgress className={classes.progress} size={100} />
|
||||||
|
<Typography variant="h4">
|
||||||
|
Loading…
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FullScreenLoading;
|
||||||
23
interface/src/components/HighlightAvatar.tsx
Normal file
23
interface/src/components/HighlightAvatar.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Avatar, makeStyles } from "@material-ui/core";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
interface HighlightAvatarProps {
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: (props: HighlightAvatarProps) => ({
|
||||||
|
backgroundColor: props.color
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
||||||
|
const classes = useStyles(props);
|
||||||
|
return (
|
||||||
|
<Avatar className={classes.root}>
|
||||||
|
{props.children}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HighlightAvatar;
|
||||||
294
interface/src/components/MenuAppBar.tsx
Normal file
294
interface/src/components/MenuAppBar.tsx
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import React, { RefObject, Fragment } from 'react';
|
||||||
|
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
|
||||||
|
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
|
||||||
|
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
||||||
|
import { Card, CardContent, CardActions } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
|
||||||
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
|
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
||||||
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
|
import LockIcon from '@material-ui/icons/Lock';
|
||||||
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
|
|
||||||
|
import ProjectMenu from '../project/ProjectMenu';
|
||||||
|
import { PROJECT_NAME } from '../api';
|
||||||
|
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||||
|
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||||
|
|
||||||
|
const drawerWidth = 290;
|
||||||
|
|
||||||
|
const styles = (theme: Theme) => createStyles({
|
||||||
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
flexGrow: 1
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
marginLeft: drawerWidth,
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: `calc(100% - ${drawerWidth}px)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolbarImage: {
|
||||||
|
[theme.breakpoints.up('xs')]: {
|
||||||
|
height: 24,
|
||||||
|
marginRight: theme.spacing(2)
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
height: 36,
|
||||||
|
marginRight: theme.spacing(3)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolbar: theme.mixins.toolbar,
|
||||||
|
drawerPaper: {
|
||||||
|
width: drawerWidth,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexGrow: 1
|
||||||
|
},
|
||||||
|
authMenu: {
|
||||||
|
zIndex: theme.zIndex.tooltip,
|
||||||
|
maxWidth: 400,
|
||||||
|
},
|
||||||
|
authMenuActions: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
"& > * + *": {
|
||||||
|
marginLeft: theme.spacing(2),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface MenuAppBarState {
|
||||||
|
mobileOpen: boolean;
|
||||||
|
authMenuOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
|
||||||
|
sectionTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||||
|
|
||||||
|
constructor(props: MenuAppBarProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
mobileOpen: false,
|
||||||
|
authMenuOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorRef: RefObject<HTMLButtonElement> = React.createRef();
|
||||||
|
|
||||||
|
handleToggle = () => {
|
||||||
|
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose = (event: React.MouseEvent<Document>) => {
|
||||||
|
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ authMenuOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrawerToggle = () => {
|
||||||
|
this.setState({ mobileOpen: !this.state.mobileOpen });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
|
||||||
|
const { mobileOpen, authMenuOpen } = this.state;
|
||||||
|
const path = this.props.match.url;
|
||||||
|
const drawer = (
|
||||||
|
<div>
|
||||||
|
<Toolbar>
|
||||||
|
<Box display="flex">
|
||||||
|
<img src="/app/icon.png" className={classes.toolbarImage} alt={PROJECT_NAME} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h6" color="textPrimary">
|
||||||
|
{PROJECT_NAME}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography align="right" variant="caption" color="textPrimary">
|
||||||
|
v{authenticatedContext.me.version}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Divider absolute />
|
||||||
|
</Toolbar>
|
||||||
|
|
||||||
|
{features.project && (
|
||||||
|
<Fragment>
|
||||||
|
<ProjectMenu />
|
||||||
|
<Divider />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<List>
|
||||||
|
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SettingsEthernetIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Network Connection" />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SettingsInputAntennaIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Access Point" />
|
||||||
|
</ListItem>
|
||||||
|
{features.ntp && (
|
||||||
|
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<AccessTimeIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Network Time" />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
{features.mqtt && (
|
||||||
|
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DeviceHubIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="MQTT" />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
{features.security && (
|
||||||
|
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<LockIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Security" />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
|
||||||
|
<ListItemIcon>
|
||||||
|
<SettingsIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="System" />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const userMenu = (
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
ref={this.anchorRef}
|
||||||
|
aria-owns={authMenuOpen ? 'menu-list-grow' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={this.handleToggle}
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<AccountCircleIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
|
||||||
|
<ClickAwayListener onClickAway={this.handleClose}>
|
||||||
|
<Card id="menu-list-grow">
|
||||||
|
<CardContent>
|
||||||
|
<List disablePadding>
|
||||||
|
<ListItem disableGutters>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<AccountCircleIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
<Divider />
|
||||||
|
<CardActions className={classes.authMenuActions}>
|
||||||
|
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Popper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<AppBar position="fixed" className={classes.appBar} elevation={0}>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
aria-label="Open drawer"
|
||||||
|
edge="start"
|
||||||
|
onClick={this.handleDrawerToggle}
|
||||||
|
className={classes.menuButton}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
|
||||||
|
{sectionTitle}
|
||||||
|
</Typography>
|
||||||
|
{features.security && userMenu}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<nav className={classes.drawer}>
|
||||||
|
<Hidden mdUp implementation="css">
|
||||||
|
<Drawer
|
||||||
|
variant="temporary"
|
||||||
|
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
|
||||||
|
open={mobileOpen}
|
||||||
|
onClose={this.handleDrawerToggle}
|
||||||
|
classes={{
|
||||||
|
paper: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
</Hidden>
|
||||||
|
<Hidden smDown implementation="css">
|
||||||
|
<Drawer
|
||||||
|
classes={{
|
||||||
|
paper: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
variant="permanent"
|
||||||
|
open
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
</Hidden>
|
||||||
|
</nav>
|
||||||
|
<main className={classes.content}>
|
||||||
|
<div className={classes.toolbar} />
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(
|
||||||
|
withTheme(
|
||||||
|
withFeatures(
|
||||||
|
withAuthenticatedContext(
|
||||||
|
withStyles(styles)(MenuAppBar)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
|
||||||
import ErrorIcon from '@mui/icons-material/Error';
|
|
||||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
|
||||||
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
|
|
||||||
import { Box, Typography, useTheme } from '@mui/material';
|
|
||||||
import type { BoxProps, SvgIconProps, Theme } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error';
|
|
||||||
|
|
||||||
export interface MessageBoxProps extends BoxProps {
|
|
||||||
level: MessageBoxLevel;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LEVEL_ICONS: { [type in MessageBoxLevel]: React.ComponentType<SvgIconProps> } = {
|
|
||||||
success: CheckCircleOutlineOutlinedIcon,
|
|
||||||
info: InfoOutlinedIcon,
|
|
||||||
warning: ReportProblemOutlinedIcon,
|
|
||||||
error: ErrorIcon
|
|
||||||
};
|
|
||||||
|
|
||||||
const LEVEL_BACKGROUNDS: { [type in MessageBoxLevel]: (theme: Theme) => string } = {
|
|
||||||
success: (theme: Theme) => theme.palette.success.dark,
|
|
||||||
info: (theme: Theme) => theme.palette.info.main,
|
|
||||||
warning: (theme: Theme) => theme.palette.warning.dark,
|
|
||||||
error: (theme: Theme) => theme.palette.error.dark
|
|
||||||
};
|
|
||||||
|
|
||||||
const MessageBox: FC<MessageBoxProps> = ({ level, message, sx, children, ...rest }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const Icon = LEVEL_ICONS[level];
|
|
||||||
const backgroundColor = LEVEL_BACKGROUNDS[level](theme);
|
|
||||||
const color = 'white';
|
|
||||||
return (
|
|
||||||
<Box p={2} display="flex" alignItems="center" borderRadius={1} sx={{ backgroundColor, color, ...sx }} {...rest}>
|
|
||||||
<Icon />
|
|
||||||
<Typography sx={{ ml: 2, flexGrow: 1 }} variant="body1">
|
|
||||||
{message}
|
|
||||||
</Typography>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MessageBox;
|
|
||||||
58
interface/src/components/PasswordValidator.tsx
Normal file
58
interface/src/components/PasswordValidator.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||||
|
import { InputAdornment, IconButton } from '@material-ui/core';
|
||||||
|
import {Visibility,VisibilityOff } from '@material-ui/icons';
|
||||||
|
|
||||||
|
const styles = createStyles({
|
||||||
|
input: {
|
||||||
|
"&::-ms-reveal": {
|
||||||
|
display: "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
||||||
|
|
||||||
|
interface PasswordValidatorState {
|
||||||
|
showPassword: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
showPassword: false
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleShowPassword = () => {
|
||||||
|
this.setState({
|
||||||
|
showPassword: !this.state.showPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, ...rest } = this.props;
|
||||||
|
return (
|
||||||
|
<TextValidator
|
||||||
|
{...rest}
|
||||||
|
type={this.state.showPassword ? 'text' : 'password'}
|
||||||
|
InputProps={{
|
||||||
|
classes,
|
||||||
|
endAdornment:
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label="Toggle password visibility"
|
||||||
|
onClick={this.toggleShowPassword}
|
||||||
|
>
|
||||||
|
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(PasswordValidator);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user