mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f6ad533f6 | ||
|
|
4ae68fc3fd | ||
|
|
b97a9e3e5b | ||
|
|
e79e104837 | ||
|
|
7851e83162 | ||
|
|
389f0bd2e8 | ||
|
|
a4bbbb460a | ||
|
|
d01697a065 | ||
|
|
a027d17a7d | ||
|
|
a27787d0d5 | ||
|
|
24baf844fd | ||
|
|
6f125b7fbb | ||
|
|
51b00cb280 | ||
|
|
31ffa8483e | ||
|
|
e7cbd97662 | ||
|
|
0b0ca1efd1 | ||
|
|
4518396833 | ||
|
|
8de6448b74 | ||
|
|
6f1b65e70b | ||
|
|
b93890440a | ||
|
|
7cee4916a2 | ||
|
|
3e7b9a7dd9 | ||
|
|
069c63b55a | ||
|
|
e9bd1d4b81 | ||
|
|
1fa1ee5b24 | ||
|
|
8800b88f62 | ||
|
|
fd6df7279b | ||
|
|
0dceb25569 | ||
|
|
96d5324945 | ||
|
|
971df73f13 | ||
|
|
24a4cb85ff | ||
|
|
7f9582d01a | ||
|
|
dfa5e23e90 | ||
|
|
016e18002c | ||
|
|
e0b89ae3ed | ||
|
|
daad2ffe6c | ||
|
|
a9caadaf5e | ||
|
|
0d8d750e46 | ||
|
|
7a394c8e89 | ||
|
|
bcf83616f8 | ||
|
|
32bf13ca9a | ||
|
|
608500e417 | ||
|
|
bfe498758c |
24
.github/workflows/github-releases-to-discord.yml
vendored
24
.github/workflows/github-releases-to-discord.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
name: 'github-releases-to-discord'
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
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
|
|
||||||
38
.github/workflows/pre_release.yml
vendored
38
.github/workflows/pre_release.yml
vendored
@@ -1,24 +1,24 @@
|
|||||||
name: 'pre-release'
|
name: "pre-release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
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/checkout@v3
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
|
|
||||||
- name: Get EMS-ESP source code and version
|
- name: Get EMS-ESP source code and version
|
||||||
id: build_info
|
id: build_info
|
||||||
@@ -34,32 +34,24 @@ jobs:
|
|||||||
- name: Build WebUI
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
yarn install
|
npm ci
|
||||||
yarn typesafe-i18n --no-watch
|
npx typesafe-i18n --no-watch
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
yarn build
|
npm run 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: Build E32V2 firmware
|
|
||||||
run: |
|
|
||||||
platformio run -e ci_16M
|
|
||||||
|
|
||||||
- name: Create a GH Release
|
- name: Create a GH Release
|
||||||
id: 'automatic_releases'
|
id: "automatic_releases"
|
||||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
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: 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/*.*
|
||||||
|
|
||||||
|
|||||||
43
.github/workflows/sonar_check.yml
vendored
43
.github/workflows/sonar_check.yml
vendored
@@ -7,24 +7,51 @@ on:
|
|||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and analyze
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# if: github.repository_owner == 'emsesp'
|
if: github.repository_owner == 'emsesp'
|
||||||
# if: github.repository == 'emsesp/EMS-ESP32'
|
# if: github.repository == 'emsesp/EMS-ESP32'
|
||||||
env:
|
env:
|
||||||
BUILD_WRAPPER_OUT_DIR: bw-output
|
# https://binaries.sonarsource.com/?prefix=Distribution/sonar-scanner-cli/
|
||||||
|
SONAR_SCANNER_VERSION: 4.7.0.2747
|
||||||
|
SONAR_SERVER_URL: "https://sonarcloud.io"
|
||||||
|
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
- name: Install sonar-scanner and build-wrapper
|
- name: Set up JDK 11
|
||||||
uses: SonarSource/sonarcloud-github-c-cpp@v2
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
- name: Cache SonarCloud packages
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.sonar/cache
|
||||||
|
key: ${{ runner.os }}-sonar
|
||||||
|
restore-keys: ${{ runner.os }}-sonar
|
||||||
|
- name: Download and set up sonar-scanner
|
||||||
|
env:
|
||||||
|
SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/.sonar
|
||||||
|
curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }}
|
||||||
|
unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/
|
||||||
|
echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH
|
||||||
|
- name: Download and set up build-wrapper
|
||||||
|
env:
|
||||||
|
BUILD_WRAPPER_DOWNLOAD_URL: ${{ env.SONAR_SERVER_URL }}/static/cpp/build-wrapper-linux-x86.zip
|
||||||
|
run: |
|
||||||
|
curl -sSLo $HOME/.sonar/build-wrapper-linux-x86.zip ${{ env.BUILD_WRAPPER_DOWNLOAD_URL }}
|
||||||
|
unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/
|
||||||
|
echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH
|
||||||
- name: Run build-wrapper
|
- name: Run build-wrapper
|
||||||
run: |
|
run: |
|
||||||
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
|
make clean
|
||||||
|
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make clean all
|
||||||
- name: Run sonar-scanner
|
- name: Run sonar-scanner
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
|
sonar-scanner
|
||||||
|
|||||||
33
.github/workflows/tagged_release.yml
vendored
33
.github/workflows/tagged_release.yml
vendored
@@ -1,23 +1,23 @@
|
|||||||
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
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
node-version: '16'
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Install PlatformIO
|
- name: Install PlatformIO
|
||||||
run: |
|
run: |
|
||||||
@@ -29,24 +29,19 @@ jobs:
|
|||||||
- name: Build WebUI
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
yarn install
|
npm ci
|
||||||
yarn typesafe-i18n --no-watch
|
npx typesafe-i18n --no-watch
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
yarn build
|
npm run 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/*.*
|
|
||||||
39
.gitignore
vendored
39
.gitignore
vendored
@@ -1,47 +1,32 @@
|
|||||||
# vscode
|
# vscode
|
||||||
.vscode/*
|
.vscode
|
||||||
|
|
||||||
# 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
|
||||||
*_old
|
|
||||||
|
|
||||||
# OS specific
|
# OS specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*Thumbs.db
|
*Thumbs.db
|
||||||
|
|
||||||
# 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
|
|
||||||
interface/vite.config.ts.timestamp*
|
|
||||||
|
|
||||||
# scripts
|
|
||||||
test.sh
|
test.sh
|
||||||
scripts/run.sh
|
|
||||||
scripts/__pycache__
|
scripts/__pycache__
|
||||||
scripts/stackdmp.txt
|
.temp
|
||||||
|
|
||||||
# i18n generated files
|
# i18n generated files
|
||||||
interface/src/i18n/i18n-react.tsx
|
interface/src/i18n/i18n-react.tsx
|
||||||
@@ -53,8 +38,8 @@ interface/src/i18n/i18n-util.async.ts
|
|||||||
# sonar
|
# sonar
|
||||||
.scannerwork/
|
.scannerwork/
|
||||||
sonar/
|
sonar/
|
||||||
bw-output/
|
build_wrapper_output_directory/
|
||||||
|
|
||||||
# testing
|
|
||||||
emsesp
|
|
||||||
|
|
||||||
|
# other build files
|
||||||
|
dump_entities.csv
|
||||||
|
dump_entities.xls*
|
||||||
|
|||||||
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
|
||||||
// for the documentation about the extensions.json format
|
|
||||||
"recommendations": [
|
|
||||||
"platformio.platformio-ide"
|
|
||||||
],
|
|
||||||
"unwantedRecommendations": [
|
|
||||||
"ms-vscode.cpptools-extension-pack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
210
CHANGELOG.md
210
CHANGELOG.md
@@ -5,212 +5,12 @@ 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.5] March 23 2024
|
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
|
||||||
|
|
||||||
- The Wifi Tx Power setting in Network Settings will be reset to Auto
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- thermostat boost mode and boost time [#1446](https://github.com/emsesp/EMS-ESP32/issues/1446)
|
|
||||||
- heatpump energy meters [#1463](https://github.com/emsesp/EMS-ESP32/issues/1463)
|
|
||||||
- heatpump max power [#1475](https://github.com/emsesp/EMS-ESP32/issues/1475)
|
|
||||||
- checkbox for MQTT-TLS enable [#1474](https://github.com/emsesp/EMS-ESP32/issues/1474)
|
|
||||||
- added SK (Slovak) language. Thanks @misa1515
|
|
||||||
- CPU info [#1497](https://github.com/emsesp/EMS-ESP32/pull/1497)
|
|
||||||
- Show network hostname in Web UI under Network Status
|
|
||||||
- Improved HA Discovery so each section (EMS device, Scheduler, Analog, Temperature, Custom, Shower) have their own section
|
|
||||||
- boiler Bosch C1200W, id 12, [#1536](https://github.com/emsesp/EMS-ESP32/issues/1536)
|
|
||||||
- mixer MM100 telegram 0x2CC [#1554](https://github.com/emsesp/EMS-ESP32/issues/1554)
|
|
||||||
- boiler hpSetDiffPressure [#1563](https://github.com/emsesp/EMS-ESP32/issues/1563)
|
|
||||||
- custom variables [#1423](https://github.com/emsesp/EMS-ESP32/issues/1423)
|
|
||||||
- weather compensation [#1642](https://github.com/emsesp/EMS-ESP32/issues/1642)
|
|
||||||
- env and partitions for DevKitC-1-N32R8 [#1635](https://github.com/emsesp/EMS-ESP32/discussions/1635)
|
|
||||||
- command `restart partitionname` and button long press to start with other partition [#1657](https://github.com/emsesp/EMS-ESP32/issues/1657)
|
|
||||||
- command `set service <mqtt|ota|ntp|ap> <enable|disable>` [#1663](https://github.com/emsesp/EMS-ESP32/issues/1663)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- exhaust temperature for some boilers
|
|
||||||
- add back boil2hyst [#1477](https://github.com/emsesp/EMS-ESP32/issues/1477)
|
|
||||||
- subscribed MQTT topics not detecting changes by EMS-ESP [#1494](https://github.com/emsesp/EMS-ESP32/issues/1494)
|
|
||||||
- changed HA name and grouping to be consistent [#1528](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
|
||||||
- MQTT autodiscovery in Domoticz not working [#1360](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
|
||||||
- dhw comfort for new ems+, [#1495](https://github.com/emsesp/EMS-ESP32/issues/1495)
|
|
||||||
- added writeable icon to Web's Custom Entity page for each entity shown in the table
|
|
||||||
- Wifi Tx Power not adjusted [#1614](https://github.com/emsesp/EMS-ESP32/issues/1614)
|
|
||||||
- MQTT discovery of custom entity doesn't consider type of data [#1587](https://github.com/emsesp/EMS-ESP32/issues/1587)
|
|
||||||
- WiFi TxPower wasn't correctly used. Added an 'Auto' setting, which is the default.
|
|
||||||
- dns w/wo IPv6 [#1644](https://github.com/emsesp/EMS-ESP32/issues/1644)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- HA don't set entity_category to Diagnostic/Configuration for EMS entities [#1459](https://github.com/emsesp/EMS-ESP32/discussions/1459)
|
|
||||||
- upgraded ArduinoJson to 7.0.0 #1538 and then 7.0.2
|
|
||||||
- small changes to the API for analog and temperature sensors
|
|
||||||
- Length of mqtt Broker adress [#1619](https://github.com/emsesp/EMS-ESP32/issues/1619)
|
|
||||||
- C++ optimizations - see <https://github.com/emsesp/EMS-ESP32/pull/1615>
|
|
||||||
- Send MQTT heartbeat immediately after connection [#1628](https://github.com/emsesp/EMS-ESP32/issues/1628)
|
|
||||||
- 16MB partitions with second nvs, larger FS, Coredump, optional factory partition
|
|
||||||
- stop fetching empty telegrams after 5 min
|
|
||||||
|
|
||||||
## [3.6.4] November 24 2023
|
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
|
||||||
|
|
||||||
Writeable Text entities have moved from type `sensor` to `text` in Home Assistant to make them also editable within an HA dashboard. Examples are `datetime`, `holidays`, `switchtime`, `vacations`, `maintenancedate`. You will need to manually remove any old discovery topics from your MQTT broker using an application like MQTT Explorer.
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- humidity for ventilation devices
|
|
||||||
- telegrams for RC100H, hc2, etc. (seen on discord, not tested)
|
|
||||||
- names for BC400, GB192i.2, read temperatures for low loss header and heatblock [#1317](https://github.com/emsesp/EMS-ESP32/discussions/1317)
|
|
||||||
- option for `forceheatingoff` [#1262](https://github.com/emsesp/EMS-ESP32/issues/1262)
|
|
||||||
- remote thermostat emulation RC100H for RC3xx [#1278](https://github.com/emsesp/EMS-ESP32/discussions/1278)
|
|
||||||
- shower_data MQTT payload contains the timestamp [#1329](https://github.com/emsesp/EMS-ESP32/issues/1329)
|
|
||||||
- HA discovery for writeable text entities [#1337](https://github.com/emsesp/EMS-ESP32/pull/1377)
|
|
||||||
- autodetect board_profile, store in nvs, add telnet command option, add E32V2
|
|
||||||
- heat pump high res energy counters [#1348, #1349. #1350](https://github.com/emsesp/EMS-ESP32/issues/1348)
|
|
||||||
- optional bssid in network settings
|
|
||||||
- extension module EM100 [#1315](https://github.com/emsesp/EMS-ESP32/discussions/1315)
|
|
||||||
- digital_out with new options for polarity and startup state
|
|
||||||
- added 'system allvalues' command that dumps all the EMS device values, plus sensors and any custom entities
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- remove command `remoteseltemp`, thermostat accept it only from remote thermostat
|
|
||||||
- shower_data MQTT payload contains the timestamp [#1329](https://github.com/emsesp/EMS-ESP32/issues/1329)
|
|
||||||
- fixed helper text in Web Device Entity dialog box for numerical ranges
|
|
||||||
- MQTT base with paths not working in HA [#1393](https://github.com/emsesp/EMS-ESP32/issues/1393)
|
|
||||||
- set/read thermostat mode for RC100-RC300, [#1440](https://github.com/emsesp/EMS-ESP32/issues/1440) [#1442](https://github.com/emsesp/EMS-ESP32/issues/1442)
|
|
||||||
- some setting commands for ems-boiler have used wrong ems+ telegram in 3.6.3
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- update to platform 6.4.0, arduino 2.0.14 / idf 4.4.6
|
|
||||||
- small changes for arduino 3.0.0 / idf 5.1 compatibility (not backward compatible to platform 6.3.2 and before)
|
|
||||||
- AP start after 10 sec, stay until station/eth connected
|
|
||||||
- tested wifi-all-channel-scan (3.6.3-dev4 a-e), removed again because of connect issues
|
|
||||||
- mqtt disconnect stops queue
|
|
||||||
|
|
||||||
## [3.6.2] October 1 2023
|
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Power entities
|
|
||||||
- Optional input of BSSID for AP connection
|
|
||||||
- Return empty json if no entries in scheduler/custom/analogsensor/temperaturesensor
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- Wifi full scan to get strongest AP
|
|
||||||
- Add missing dhw tags
|
|
||||||
- Sending a dash/- to the Reset command doesn't return an error [#1308](https://github.com/emsesp/EMS-ESP32/discussions/1308)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- MQTT queue max 300 messages, check heap and maxAlloc
|
|
||||||
- API call commands are logged as WARN in the log
|
|
||||||
- Reset Command renamed to 'reset' in lowercase in EN
|
|
||||||
|
|
||||||
## [3.6.1] September 9 2023
|
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
|
||||||
|
|
||||||
- `shower_data` MQTT topic shows duration is seconds (was previously a full english sentence)
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Show WiFi rssi in Network Status Page, show quality as color
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- Issue in espMqttClient causing a memory leak when MQTT broker is disconnected due to network unavailability [#1264](https://github.com/emsesp/EMS-ESP32/issues/1264)
|
|
||||||
- Using MQTT enum values correctly formatted in MQTT Discovery [#1280](https://github.com/emsesp/EMS-ESP32/issues/1280)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- MQTT free mem check set to 60 kb
|
|
||||||
- Small cosmetic changes to Searching in Customization web page
|
|
||||||
- Updated to espressif32@6.4.0
|
|
||||||
|
|
||||||
# [3.6.0] August 13 2023
|
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
|
||||||
|
|
||||||
There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please read carefully before applying the update.
|
|
||||||
|
|
||||||
- The sensors have been renamed. `dallassensor` is now `temperaturesensor` in the MQTT topic and named `ts` in the Customizations file. Likewise `analogs` is now `analogsensor` in MQTT and called `as` in the Customizations file. If you have previous customizations you will need to manually update by downloading, changing the JSON file and uploading. It's also recommended cleaning up any old MQTT topics from your broker using an application like MQTTExplorer.
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Workaround for better Domoticz MQTT integration? [#904](https://github.com/emsesp/EMS-ESP32/issues/904)
|
|
||||||
- Show MAC address without connecting to network enhancement [#933](https://github.com/emsesp/EMS-ESP32/issues/933)
|
|
||||||
- Warn user in WebUI of unsaved changes [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
|
||||||
- Detect old Tado thermostat, device-id 0x19, no entities
|
|
||||||
- Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
|
||||||
- Added Scheduler [#701](https://github.com/emsesp/EMS-ESP32/issues/701)
|
|
||||||
- Added Custom Entities read/write from EMS bus
|
|
||||||
- Build S3 binary with github actions
|
|
||||||
- Greenstar HIU [#1158](https://github.com/emsesp/EMS-ESP32/issues/1158)
|
|
||||||
- AM200 code 10 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
|
|
||||||
- Ventilation device (Logavent HRV176) [#1172](https://github.com/emsesp/EMS-ESP32/issues/1172)
|
|
||||||
- Turn ETH off on wifi connect [#1167](https://github.com/emsesp/EMS-ESP32/issues/1167)
|
|
||||||
- Support for multiple EMS-ESPs with HA [#1196](https://github.com/emsesp/EMS-ESP32/issues/1196)
|
|
||||||
- Italian translation [#1199](https://github.com/emsesp/EMS-ESP32/issues/1199)
|
|
||||||
- Turkish language support [#1076](https://github.com/emsesp/EMS-ESP32/issues/1076)
|
|
||||||
- Buderus GB182 - HC1 mode change not work bug [#1193](https://github.com/emsesp/EMS-ESP32/issues/1193)
|
|
||||||
- Minimal flow temperature enhancement [#1192](https://github.com/emsesp/EMS-ESP32/issues/1192)
|
|
||||||
- Roomtemperature Switching Difference enhancement [#1191](https://github.com/emsesp/EMS-ESP32/issues/1191)
|
|
||||||
- Dew Point Temperature Difference enhancement [#1190](https://github.com/emsesp/EMS-ESP32/issues/1190)
|
|
||||||
- Control of heating circuit mode enhancement [#1187](https://github.com/emsesp/EMS-ESP32/issues/1187)
|
|
||||||
- Warn user in WebUI of unsaved changes enhancement [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
|
||||||
- Create safebuild app to fit into factory partition to give ESP32 more flash memory enhancement [#608](https://github.com/emsesp/EMS-ESP32/issues/608)
|
|
||||||
- Support ESP32 S2, C3 mini and S3 [#605](https://github.com/emsesp/EMS-ESP32/issues/605)
|
|
||||||
- Support Buderus AM200 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
|
|
||||||
- Custom telegram handler [#1155](https://github.com/emsesp/EMS-ESP32/issues/1155)
|
|
||||||
- Added support for TLS in MQTT (ESP32-S3 only) [#1178](https://github.com/emsesp/EMS-ESP32/issues/1178)
|
|
||||||
- Boardprofile BBQKees Gateway S3
|
|
||||||
- Custom entity type RAW [#1212](https://github.com/emsesp/EMS-ESP32/discussions/1212)
|
|
||||||
- API command response [#1212](https://github.com/emsesp/EMS-ESP32/discussions/1212)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035)
|
|
||||||
- Enum order of RC3x nofrost mode
|
|
||||||
- Heartbeat interval
|
|
||||||
- Exhaust temperature always zero on GB125/MC110/RC310 bug [#1147](https://github.com/emsesp/EMS-ESP32/issues/1147)
|
|
||||||
- thermostat modetype is not changing when mode changes (e.g. to night) bugSomething isn't working [#1098](https://github.com/emsesp/EMS-ESP32/issues/1098)
|
|
||||||
- NTP: cant apply changed timezone [#1182](https://github.com/emsesp/EMS-ESP32/issues/1182)
|
|
||||||
- Missing Status of VS1 for Buderus SM200 enhancement [#1034](https://github.com/emsesp/EMS-ESP32/issues/1034)
|
|
||||||
- Allowed gpios for S3
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- Optional upgrade to platform-espressif32 6.3.0 (after 5.3.0) [#862](https://github.com/emsesp/EMS-ESP32/issues/862)
|
|
||||||
- Use byte 3 for detection RC30 active heatingcircuit [#786](https://github.com/emsesp/EMS-ESP32/issues/786)
|
|
||||||
- Write repeated selflowtemp if tx-queue is empty without verify [#954](https://github.com/emsesp/EMS-ESP32/issues/954)
|
|
||||||
- HA discovery recreate after disconnect by device [#1067](https://github.com/emsesp/EMS-ESP32/issues/1067)
|
|
||||||
- File upload: check flash size (overflow) instead of filesize
|
|
||||||
- Improved HA Discovery so previous configs no longer need to be removed when starting [#1077](https://github.com/emsesp/EMS-ESP32/pull/1077) (thanks @pswid)
|
|
||||||
- Enlarge UART-Stack to 2,5k
|
|
||||||
- Retry timeout for Mqtt-QOS1/2 10seconds
|
|
||||||
- Optimize WebUI rendering when using Dialog Boxes [#1116](https://github.com/emsesp/EMS-ESP32/issues/1116)
|
|
||||||
- Optimize Web libraries to reduce bundle size (3.6.x) [#1112](https://github.com/emsesp/EMS-ESP32/issues/1112)
|
|
||||||
- Use [espMqttClient](https://github.com/bertmelis/espMqttClient) with integrated queue [#1178](https://github.com/emsesp/EMS-ESP32/issues/1178)
|
|
||||||
- Move Sensors from Web dashboard to it's own tab enhancement [#1170](https://github.com/emsesp/EMS-ESP32/issues/1170)
|
|
||||||
- Optimize WebUI dashboard data [#1169](https://github.com/emsesp/EMS-ESP32/issues/1169)
|
|
||||||
- Replace React core library with Preact to save on memory footprint
|
|
||||||
- Response to `system/send` raw reads gives combined data for telegrams with more parts
|
|
||||||
|
|
||||||
# [3.5.0] February 6 2023
|
# [3.5.0] February 6 2023
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
## **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.
|
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
|
||||||
|
- Support for multiple EMS-ESPs [#759] has been added as an optional setting for MQTT. When enabled, which is now the default, all MQTT Discovery Entity IDs will include the MQTT base name and the shortname of the EMS-ESP device entity. For example what was previously `sensor.boiler_actual_boiler_temperature` will now become `sensor.ems_esp_boiler_boiltemp`. If you still want to use the old format and retain the history and script compatibility in Home Assistant then set this back to the old format.
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
@@ -286,7 +86,7 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
|||||||
|
|
||||||
- fix Table resizing in WebUI [#519](https://github.com/emsesp/EMS-ESP32/issues/519)
|
- 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)
|
- allow larger customization files [#570](https://github.com/emsesp/EMS-ESP32/issues/570)
|
||||||
- losing entity wwcomfort [#581](https://github.com/emsesp/EMS-ESP32/issues/581)
|
- losing entitiy wwcomfort [#581](https://github.com/emsesp/EMS-ESP32/issues/581)
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
@@ -313,7 +113,7 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
|||||||
|
|
||||||
- WebUI optimizations, updated look&feel and better performance [#124](https://github.com/emsesp/EMS-ESP32/issues/124)
|
- 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)
|
- 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)
|
- New Customization Service in WebUI. First feature is the ability to enable/disabled Enitites (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)
|
- 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
|
- 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
|
- Merged in MichaelDvP's changes like Fahrenheit conversion, publish single (for IOBroker) and a few other critical optimizations
|
||||||
@@ -413,7 +213,7 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
|||||||
- Added pool data to telegrams 0x494 & 0x495 [#102](https://github.com/emsesp/EMS-ESP32/issues/102)
|
- 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 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 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)
|
- 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
|
- 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)
|
- 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)
|
- Add current room influence for RC300 [#136](https://github.com/emsesp/EMS-ESP32/issues/136)
|
||||||
@@ -635,4 +435,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,11 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [3.x]
|
# [3.5.1]
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
|
- Detect old Tado thermostat, device-id 0x19, no entities
|
||||||
|
- Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||||
|
- Add entity to force heating off (for systems without thermostat) [#951](https://github.com/emsesp/EMS-ESP32/issues/951)
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
- HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035)
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
- Use byte 0 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
|
||||||
|
|||||||
20
Makefile
20
Makefile
@@ -17,8 +17,8 @@ MAKEFLAGS+="j "
|
|||||||
#TARGET := $(notdir $(CURDIR))
|
#TARGET := $(notdir $(CURDIR))
|
||||||
TARGET := emsesp
|
TARGET := emsesp
|
||||||
BUILD := build
|
BUILD := build
|
||||||
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton lib/semver lib/espMqttClient/src lib/espMqttClient/src/*
|
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton lib/semver
|
||||||
INCLUDES := src lib_standalone lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/semver lib/* src/devices
|
INCLUDES := src lib_standalone lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/semver lib/* src/devices
|
||||||
LIBRARIES :=
|
LIBRARIES :=
|
||||||
|
|
||||||
CPPCHECK = cppcheck
|
CPPCHECK = cppcheck
|
||||||
@@ -28,21 +28,19 @@ CHECKFLAGS = -q --force --std=c++11
|
|||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Languages Standard
|
# Languages Standard
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
C_STANDARD := -std=c17
|
# C_STANDARD := -std=c17
|
||||||
# CXX_STANDARD := -std=c++17
|
# CXX_STANDARD := -std=c++17
|
||||||
CXX_STANDARD := -std=gnu++11
|
C_STANDARD := -std=c11
|
||||||
|
CXX_STANDARD := -std=c++11
|
||||||
# C_STANDARD := -std=c11
|
|
||||||
# CXX_STANDARD := -std=c++11
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Defined Symbols
|
# Defined Symbols
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||||
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST -D__linux__ -DEMC_RX_BUFFER_SIZE=1500
|
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_USE_SERIAL
|
||||||
DEFINES += $(ARGS)
|
DEFINES += $(ARGS)
|
||||||
|
|
||||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.6.5-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.5.0b11\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Sources & Files
|
# Sources & Files
|
||||||
@@ -81,7 +79,9 @@ CPPFLAGS += -g3
|
|||||||
CPPFLAGS += -Os
|
CPPFLAGS += -Os
|
||||||
|
|
||||||
CFLAGS += $(CPPFLAGS)
|
CFLAGS += $(CPPFLAGS)
|
||||||
CFLAGS += -Wall -Wextra -Werror -Wswitch-enum -Wno-unused-parameter -Wno-inconsistent-missing-override -Wno-missing-braces -Wno-unused-lambda-capture -Wno-sign-compare
|
CFLAGS += -Wall
|
||||||
|
CFLAGS += -Wextra
|
||||||
|
CFLAGS += -Wno-unused-parameter
|
||||||
|
|
||||||
CXXFLAGS += $(CFLAGS) -MMD
|
CXXFLAGS += $(CFLAGS) -MMD
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
- Native support for Home Assistant, Domoticz and openHAB via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
- Native support for Home Assistant, Domoticz and openHAB via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
||||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
- 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 [110 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways, switches, heat sources)
|
||||||
|
|
||||||
## **Documentation**
|
## **Documentation**
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ EMS-ESP is a project owned and maintained by [proddy](https://github.com/proddy)
|
|||||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
- [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
|
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON
|
||||||
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client, with custom modifications from @MichaelDvP and @proddy
|
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
||||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||||
|
|
||||||
## **License**
|
## **License**
|
||||||
|
|||||||
5
RELEASENOTES.md
Normal file
5
RELEASENOTES.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 
|
||||||
|
|
||||||
|
# Firmware Installation
|
||||||
|
|
||||||
|
Follow the instructions in the [documentation](https://emsesp.github.io/docs) on how to install the firmware binaries in the Assets below.
|
||||||
7
RELEASENOTES_DEV.md
Normal file
7
RELEASENOTES_DEV.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 
|
||||||
|
|
||||||
|
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.
|
||||||
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,8 +1,6 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
nvs, data, nvs, 0x9000, 0x005000,
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
otadata, data, ota, , 0x002000,
|
otadata, data, ota, , 0x2000,
|
||||||
app0, app, ota_0, , 0x5D0000,
|
app0, app, ota_0, , 0x7F0000,
|
||||||
app1, app, ota_1, , 0x5D0000,
|
app1, app, ota_1, , 0x7F0000,
|
||||||
nvs1, data, nvs, , 0x040000,
|
spiffs, data, spiffs, , 64K,
|
||||||
spiffs, data, spiffs, , 0x400000,
|
|
||||||
coredump, data, coredump,, 0x010000,
|
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x005000,
|
|
||||||
otadata, data, ota, , 0x002000,
|
|
||||||
boot, app, factory, , 0x280000,
|
|
||||||
app0, app, ota_0, , 0x590000,
|
|
||||||
app1, app, ota_1, , 0x590000,
|
|
||||||
nvs1, data, nvs, , 0x040000,
|
|
||||||
spiffs, data, spiffs, , 0x200000,
|
|
||||||
coredump, data, coredump,, 0x010000,
|
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x005000,
|
|
||||||
otadata, data, ota, , 0x002000,
|
|
||||||
app0, app, ota_0, , 0xDD0000,
|
|
||||||
app1, app, ota_1, , 0xDD0000,
|
|
||||||
nvs1, data, nvs, , 0x040000,
|
|
||||||
spiffs, data, spiffs, , 0x400000,
|
|
||||||
coredump, data, coredump,, 0x010000,
|
|
||||||
|
@@ -36,6 +36,7 @@ build_flags =
|
|||||||
-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_KEEP_ALIVE=60
|
-D FACTORY_MQTT_KEEP_ALIVE=60
|
||||||
-D FACTORY_MQTT_CLEAN_SESSION=false
|
-D FACTORY_MQTT_CLEAN_SESSION=false
|
||||||
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
|
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
|
||||||
|
|||||||
8
interface/.env
Normal file
8
interface/.env
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# This enables lint extensions
|
||||||
|
EXTEND_ESLINT=true
|
||||||
|
|
||||||
|
# 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 +0,0 @@
|
|||||||
VITE_ALOVA_TIPS=0
|
|
||||||
REACT_APP_ALOVA_TIPS=0
|
|
||||||
3
interface/.env.hosted
Normal file
3
interface/.env.hosted
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
|
REACT_APP_HOSTED=true
|
||||||
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,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"trailingComma": "none",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 120,
|
"semi": true,
|
||||||
"bracketSpacing": true
|
"trailingComma": "none",
|
||||||
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"adapter": "react",
|
"adapter": "react",
|
||||||
"baseLocale": "pl",
|
"baseLocale": "pl",
|
||||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
|
"$schema": "https://unpkg.com/typesafe-i18n@5.24.1/schema/typesafe-i18n.json"
|
||||||
}
|
}
|
||||||
893
interface/.yarn/releases/yarn-4.1.1.cjs
vendored
893
interface/.yarn/releases/yarn-4.1.1.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.1.1.cjs
|
|
||||||
30
interface/config-overrides.js
Normal file
30
interface/config-overrides.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const ProgmemGenerator = require('./progmem-generator.js');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = function override(config, env) {
|
||||||
|
const hosted = process.env.REACT_APP_HOSTED;
|
||||||
|
|
||||||
|
if (env === 'production' && !hosted) {
|
||||||
|
// rename the ouput file, we need it's path to be short, for embedded FS
|
||||||
|
config.output.filename = 'js/[id].[chunkhash:4].js';
|
||||||
|
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
||||||
|
|
||||||
|
// take out the manifest plugin
|
||||||
|
config.plugins = config.plugins.filter((plugin) => !(plugin instanceof WebpackManifestPlugin));
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
// don't emit license file
|
||||||
|
const terserPlugin = config.optimization.minimizer.find((plugin) => plugin instanceof TerserPlugin);
|
||||||
|
terserPlugin.options.extractComments = false;
|
||||||
|
|
||||||
|
// build progmem data files
|
||||||
|
config.plugins.push(new ProgmemGenerator({ outputPath: '../lib/framework/WWWData.h', bytesPerLine: 20 }));
|
||||||
|
}
|
||||||
|
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>
|
|
||||||
31267
interface/package-lock.json
generated
Normal file
31267
interface/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,78 +1,104 @@
|
|||||||
{
|
{
|
||||||
"name": "EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"version": "3.6.5",
|
"version": "3.5.0",
|
||||||
"description": "build EMS-ESP WebUI",
|
|
||||||
"homepage": "https://emsesp.github.io/docs",
|
|
||||||
"author": "proddy",
|
|
||||||
"license": "MIT",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"proxy": "http://localhost:3080",
|
||||||
"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",
|
|
||||||
"old_mock-api": "bun --watch ../mock-api/server.js",
|
|
||||||
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-api\" \"vite\"",
|
|
||||||
"old_standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:old_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.3",
|
"@emotion/react": "^11.10.6",
|
||||||
"@babel/core": "^7.24.3",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@emotion/react": "^11.11.4",
|
"@msgpack/msgpack": "^2.8.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@mui/icons-material": "^5.11.9",
|
||||||
"@mui/icons-material": "^5.15.14",
|
"@mui/material": "^5.11.10",
|
||||||
"@mui/material": "^5.15.14",
|
"@table-library/react-table-library": "4.0.26",
|
||||||
"@table-library/react-table-library": "4.1.7",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/imagemin": "^8.0.5",
|
"@types/node": "^18.14.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/react": "^18.0.28",
|
||||||
"@types/node": "^20.11.30",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react": "^18.2.69",
|
|
||||||
"@types/react-dom": "^18.2.22",
|
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"alova": "^2.18.0",
|
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
"history": "^5.3.0",
|
"axios": "^1.3.4",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime-types": "^2.1.35",
|
"notistack": "^2.0.8",
|
||||||
"react": "latest",
|
"react": "^18.2.0",
|
||||||
"react-dom": "latest",
|
"react-app-rewired": "^2.2.1",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^4.7.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.8.1",
|
||||||
"react-toastify": "^10.0.5",
|
"react-scripts": "5.0.1",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"typesafe-i18n": "^5.26.2",
|
"typesafe-i18n": "^5.24.1",
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^4.9.5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-app-rewired start",
|
||||||
|
"build": "react-app-rewired build",
|
||||||
|
"test": "react-app-rewired test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
||||||
|
"build-hosted": "env-cmd -f .env.hosted npm run build",
|
||||||
|
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
|
||||||
|
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
||||||
|
"standalone": "npm-run-all -p start typesafe-i18n mock-api",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
|
"typesafe-i18n": "typesafe-i18n"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"eol-last": 1,
|
||||||
|
"react/jsx-closing-bracket-location": 1,
|
||||||
|
"react/jsx-closing-tag-location": 1,
|
||||||
|
"react/jsx-wrap-multilines": 1,
|
||||||
|
"react/jsx-curly-newline": 1,
|
||||||
|
"no-multiple-empty-lines": [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
"max": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-trailing-spaces": 1,
|
||||||
|
"semi": 1,
|
||||||
|
"no-extra-semi": 1,
|
||||||
|
"react/jsx-max-props-per-line": [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
"when": "multiline"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-first-prop-new-line": [
|
||||||
|
1,
|
||||||
|
"multiline"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-shadow": 1,
|
||||||
|
"max-len": [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
"code": 220
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"arrow-parens": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
"nodemon": "^2.0.20",
|
||||||
"@preact/preset-vite": "^2.8.2",
|
"npm-run-all": "^4.1.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
"http-proxy-middleware": "^2.0.6"
|
||||||
"@typescript-eslint/parser": "^7.3.1",
|
}
|
||||||
"concurrently": "^8.2.2",
|
|
||||||
"eslint": "^8.57.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.34.1",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"preact": "^10.20.1",
|
|
||||||
"prettier": "^3.2.5",
|
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
|
||||||
"terser": "^5.29.2",
|
|
||||||
"vite": "^5.2.4",
|
|
||||||
"vite-plugin-imagemin": "^0.6.1",
|
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@4.1.1"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,9 @@
|
|||||||
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');
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
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 char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> 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}, "${file.hash}");`
|
|
||||||
)
|
|
||||||
.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) => {
|
||||||
@@ -40,6 +17,10 @@ function getFilesSync(dir, files = []) {
|
|||||||
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);
|
||||||
@@ -47,68 +28,94 @@ function cleanAndOpen(path) {
|
|||||||
return createWriteStream(path, { flags: 'w+' });
|
return createWriteStream(path, { flags: 'w+' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ProgmemGenerator {
|
||||||
|
constructor(options = {}) {
|
||||||
|
const { outputPath, bytesPerLine = 20, indent = ' ', includes = ARDUINO_INCLUDES } = options;
|
||||||
|
this.options = { outputPath, bytesPerLine, indent, includes };
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler) {
|
||||||
|
compiler.hooks.emit.tapAsync({ name: 'ProgmemGenerator' }, (compilation, callback) => {
|
||||||
|
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
||||||
|
const fileInfo = [];
|
||||||
|
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
||||||
|
try {
|
||||||
|
const writeIncludes = () => {
|
||||||
|
writeStream.write(includes);
|
||||||
|
};
|
||||||
|
|
||||||
const writeFile = (relativeFilePath, buffer) => {
|
const writeFile = (relativeFilePath, buffer) => {
|
||||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||||
const mimeType = mime.lookup(relativeFilePath);
|
const mimeType = mime.lookup(relativeFilePath);
|
||||||
var size = 0;
|
var size = 0;
|
||||||
writeStream.write('const uint8_t ' + variable + '[] = {');
|
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
||||||
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
|
const zipBuffer = zlib.gzipSync(buffer);
|
||||||
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
|
|
||||||
|
|
||||||
// create sha
|
|
||||||
const hashSum = crypto.createHash('sha256');
|
|
||||||
hashSum.update(zipBuffer);
|
|
||||||
const hash = hashSum.digest('hex');
|
|
||||||
|
|
||||||
zipBuffer.forEach((b) => {
|
zipBuffer.forEach((b) => {
|
||||||
if (!(size % bytesPerLine)) {
|
if (!(size % bytesPerLine)) {
|
||||||
writeStream.write('\n');
|
writeStream.write('\n');
|
||||||
writeStream.write(indent);
|
writeStream.write(indent);
|
||||||
}
|
}
|
||||||
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ',');
|
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ',');
|
||||||
size++;
|
size++;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (size % bytesPerLine) {
|
if (size % bytesPerLine) {
|
||||||
writeStream.write('\n');
|
writeStream.write('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
writeStream.write('};\n\n');
|
writeStream.write('};\n\n');
|
||||||
|
|
||||||
fileInfo.push({
|
fileInfo.push({
|
||||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||||
mimeType,
|
mimeType,
|
||||||
variable,
|
variable,
|
||||||
size,
|
size
|
||||||
hash
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
|
|
||||||
totalSize += size;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// start
|
const writeFiles = () => {
|
||||||
console.log('Generating ' + outputPath + ' from ' + sourcePath);
|
|
||||||
const includes = ARDUINO_INCLUDES;
|
|
||||||
const indent = INDENT;
|
|
||||||
const fileInfo = [];
|
|
||||||
const writeStream = cleanAndOpen(resolve(outputPath));
|
|
||||||
|
|
||||||
// includes
|
|
||||||
writeStream.write(includes);
|
|
||||||
|
|
||||||
// process static files
|
// process static files
|
||||||
const buildPath = resolve(sourcePath);
|
const buildPath = compilation.options.output.path;
|
||||||
for (const filePath of getFilesSync(buildPath)) {
|
for (const filePath of getFilesSync(buildPath)) {
|
||||||
const readStream = readFileSync(filePath);
|
const readStream = readFileSync(filePath);
|
||||||
const relativeFilePath = relative(buildPath, filePath);
|
const relativeFilePath = relative(buildPath, filePath);
|
||||||
writeFile(relativeFilePath, readStream);
|
writeFile(relativeFilePath, readStream);
|
||||||
}
|
}
|
||||||
|
// process assets
|
||||||
|
const { assets } = compilation;
|
||||||
|
Object.keys(assets).forEach((relativeFilePath) => {
|
||||||
|
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// add class
|
const generateWWWClass = () => {
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
||||||
|
|
||||||
|
class WWWData {
|
||||||
|
${indent}public:
|
||||||
|
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||||
|
${fileInfo
|
||||||
|
.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`)
|
||||||
|
.join('\n')}
|
||||||
|
${indent.repeat(2)}}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeWWWClass = () => {
|
||||||
writeStream.write(generateWWWClass());
|
writeStream.write(generateWWWClass());
|
||||||
|
};
|
||||||
|
|
||||||
// end
|
writeIncludes();
|
||||||
|
writeFiles();
|
||||||
|
writeWWWClass();
|
||||||
|
|
||||||
|
writeStream.on('finish', () => {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
writeStream.end();
|
writeStream.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Total size: ' + totalSize / 1000 + ' KB');
|
module.exports = ProgmemGenerator;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -1,18 +1,26 @@
|
|||||||
/*
|
/*
|
||||||
* Uses font-size 400 (normal) only and Latin (plus extra unicode chars) to keep flash memory to a minimum
|
* Just supporting latin due to size constrains on the esp chip
|
||||||
* View fonts on https://fonts.google.com/
|
*
|
||||||
* Download woff2 using e.g. https://fonts.googleapis.com/css2?family=Lato or https://fonts.googleapis.com/css2?family=Roboto
|
* The framework only makes use of 400 (regular) + 500 (medium) weight fonts.
|
||||||
|
*
|
||||||
|
* If using light or strong typography variants you will need to add additional fonts.
|
||||||
*/
|
*/
|
||||||
@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+0104-0107, U+0118-0119, U+0131, U+0141-0144, U+0152-0153, U+015A-015B, U+0179-017C,
|
||||||
local('Roboto'),
|
U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||||
local('Roboto-Regular'),
|
U+FFFD;
|
||||||
url(../fonts/re.woff2) format('woff2');
|
}
|
||||||
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0152-0153, U+015A-015B,
|
|
||||||
U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
@font-face {
|
||||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/md.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+0131, U+0141-0144, U+0152-0153, U+015A-015B, U+0179-017C,
|
||||||
|
U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||||
|
U+FFFD;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
interface/public/fonts/md.woff2
Normal file
BIN
interface/public/fonts/md.woff2
Normal file
Binary file not shown.
Binary file not shown.
17
interface/public/index.html
Normal file
17
interface/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1, minimum-scale=1"
|
||||||
|
/>
|
||||||
|
<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,48 +1,61 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { FC, createRef, createContext, useContext, useEffect, useState, RefObject } from 'react';
|
||||||
import { ToastContainer, Slide } from 'react-toastify';
|
import { SnackbarProvider } from 'notistack';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.min.css';
|
import { IconButton } from '@mui/material';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
|
import { FeaturesLoader } from './contexts/features';
|
||||||
|
|
||||||
|
import CustomTheme from './CustomTheme';
|
||||||
|
import AppRouting from './AppRouting';
|
||||||
|
|
||||||
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
||||||
import type { FC } from 'react';
|
import TypesafeI18n from './i18n/i18n-react';
|
||||||
import AppRouting from 'AppRouting';
|
import { detectLocale } from './i18n/i18n-util';
|
||||||
import CustomTheme from 'CustomTheme';
|
import { loadLocaleAsync } from './i18n/i18n-util.async';
|
||||||
|
|
||||||
import TypesafeI18n from 'i18n/i18n-react';
|
|
||||||
import { detectLocale } from 'i18n/i18n-util';
|
|
||||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
|
||||||
|
|
||||||
const detectedLocale = detectLocale(localStorageDetector);
|
const detectedLocale = detectLocale(localStorageDetector);
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
|
const notistackRef: RefObject<any> = createRef();
|
||||||
|
|
||||||
|
const onClickDismiss = (key: string | number | undefined) => () => {
|
||||||
|
notistackRef.current.closeSnackbar(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColorModeContext = createContext({ toggleColorMode: () => {} });
|
||||||
|
|
||||||
|
const colorMode = useContext(ColorModeContext);
|
||||||
|
|
||||||
const [wasLoaded, setWasLoaded] = useState(false);
|
const [wasLoaded, setWasLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
|
loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!wasLoaded) return null;
|
if (!wasLoaded) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ColorModeContext.Provider value={colorMode}>
|
||||||
<TypesafeI18n locale={detectedLocale}>
|
<TypesafeI18n locale={detectedLocale}>
|
||||||
<CustomTheme>
|
<CustomTheme>
|
||||||
|
<SnackbarProvider
|
||||||
|
maxSnack={3}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
|
ref={notistackRef}
|
||||||
|
action={(key) => (
|
||||||
|
<IconButton onClick={onClickDismiss(key)} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FeaturesLoader>
|
||||||
<AppRouting />
|
<AppRouting />
|
||||||
<ToastContainer
|
</FeaturesLoader>
|
||||||
position="bottom-left"
|
</SnackbarProvider>
|
||||||
autoClose={3000}
|
|
||||||
hideProgressBar={false}
|
|
||||||
newestOnTop={false}
|
|
||||||
closeOnClick={true}
|
|
||||||
rtl={false}
|
|
||||||
pauseOnFocusLoss={false}
|
|
||||||
draggable={false}
|
|
||||||
pauseOnHover={false}
|
|
||||||
transition={Slide}
|
|
||||||
closeButton={false}
|
|
||||||
theme="light"
|
|
||||||
/>
|
|
||||||
</CustomTheme>
|
</CustomTheme>
|
||||||
</TypesafeI18n>
|
</TypesafeI18n>
|
||||||
|
</ColorModeContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { FC, useContext, useEffect } from 'react';
|
||||||
|
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
|
import { useSnackbar, VariantType } from 'notistack';
|
||||||
|
|
||||||
import { Route, Routes, Navigate, useLocation } from 'react-router-dom';
|
import { useI18nContext } from './i18n/i18n-react';
|
||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import { Authentication, AuthenticationContext } from './contexts/authentication';
|
||||||
import type { FC } from 'react';
|
import { FeaturesContext } from './contexts/features';
|
||||||
|
import { RequireAuthenticated, RequireUnauthenticated } from './components';
|
||||||
|
|
||||||
import AuthenticatedRouting from 'AuthenticatedRouting';
|
import SignIn from './SignIn';
|
||||||
import SignIn from 'SignIn';
|
import AuthenticatedRouting from './AuthenticatedRouting';
|
||||||
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
|
||||||
|
|
||||||
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
|
|
||||||
interface SecurityRedirectProps {
|
interface SecurityRedirectProps {
|
||||||
message: string;
|
message: string;
|
||||||
|
variant?: VariantType;
|
||||||
signOut?: boolean;
|
signOut?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RootRedirect: FC<SecurityRedirectProps> = ({ message, signOut }) => {
|
const RootRedirect: FC<SecurityRedirectProps> = ({ message, variant, signOut }) => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
signOut && authenticationContext.signOut(false);
|
signOut && authenticationContext.signOut(false);
|
||||||
toast.success(message);
|
enqueueSnackbar(message, { variant });
|
||||||
}, [message, signOut, authenticationContext]);
|
}, [message, variant, signOut, authenticationContext, enqueueSnackbar]);
|
||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export const RemoveTrailingSlashes = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppRouting: FC = () => {
|
const AppRouting: FC = () => {
|
||||||
|
const { features } = useContext(FeaturesContext);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +50,8 @@ const AppRouting: FC = () => {
|
|||||||
<RemoveTrailingSlashes />
|
<RemoveTrailingSlashes />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
||||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />} />
|
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
|
||||||
|
{features.security && (
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -57,6 +60,7 @@ const AppRouting: FC = () => {
|
|||||||
</RequireUnauthenticated>
|
</RequireUnauthenticated>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Route
|
<Route
|
||||||
path="/*"
|
path="/*"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
import { FC, useCallback, useContext, useEffect } from 'react';
|
||||||
import Dashboard from './project/Dashboard';
|
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import Help from './project/Help';
|
import { AxiosError } from 'axios';
|
||||||
import Settings from './project/Settings';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import { Layout, RequireAdmin } from 'components';
|
import { FeaturesContext } from './contexts/features';
|
||||||
import AccessPoint from 'framework/ap/AccessPoint';
|
import * as AuthenticationApi from './api/authentication';
|
||||||
import Mqtt from 'framework/mqtt/Mqtt';
|
import { PROJECT_PATH } from './api/env';
|
||||||
import NetworkConnection from 'framework/network/NetworkConnection';
|
import { AXIOS } from './api/endpoints';
|
||||||
import NetworkTime from 'framework/ntp/NetworkTime';
|
import { Layout, RequireAdmin } from './components';
|
||||||
import Security from 'framework/security/Security';
|
|
||||||
import System from 'framework/system/System';
|
|
||||||
|
|
||||||
const AuthenticatedRouting: FC = () => (
|
import ProjectRouting from './project/ProjectRouting';
|
||||||
// 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]);
|
|
||||||
|
|
||||||
|
import NetworkConnection from './framework/network/NetworkConnection';
|
||||||
|
import AccessPoint from './framework/ap/AccessPoint';
|
||||||
|
import NetworkTime from './framework/ntp/NetworkTime';
|
||||||
|
import Mqtt from './framework/mqtt/Mqtt';
|
||||||
|
import System from './framework/system/System';
|
||||||
|
import Security from './framework/security/Security';
|
||||||
|
|
||||||
|
const AuthenticatedRouting: FC = () => {
|
||||||
|
const { features } = useContext(FeaturesContext);
|
||||||
|
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]);
|
||||||
|
|
||||||
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
{features.project && <Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />}
|
||||||
<Route
|
|
||||||
path="/settings/*"
|
|
||||||
element={
|
|
||||||
<RequireAdmin>
|
|
||||||
<Settings />
|
|
||||||
</RequireAdmin>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/help/*" element={<Help />} />
|
|
||||||
|
|
||||||
<Route path="/network/*" element={<NetworkConnection />} />
|
<Route path="/network/*" element={<NetworkConnection />} />
|
||||||
<Route path="/ap/*" element={<AccessPoint />} />
|
<Route path="/ap/*" element={<AccessPoint />} />
|
||||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
{features.ntp && <Route path="/ntp/*" element={<NetworkTime />} />}
|
||||||
<Route path="/mqtt/*" element={<Mqtt />} />
|
{features.mqtt && <Route path="/mqtt/*" element={<Mqtt />} />}
|
||||||
|
{features.security && (
|
||||||
<Route
|
<Route
|
||||||
path="/security/*"
|
path="/security/*"
|
||||||
element={
|
element={
|
||||||
@@ -55,10 +55,12 @@ const AuthenticatedRouting: FC = () => (
|
|||||||
</RequireAdmin>
|
</RequireAdmin>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Route path="/system/*" element={<System />} />
|
<Route path="/system/*" element={<System />} />
|
||||||
<Route path="/*" element={<Navigate to="/" />} />
|
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute(features)} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default AuthenticatedRouting;
|
export default AuthenticatedRouting;
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
||||||
import type { FC } from 'react';
|
import { blueGrey, blue } from '@mui/material/colors';
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import { RequiredChildrenProps } from './utils';
|
||||||
|
|
||||||
export const dialogStyle = {
|
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
borderRadius: '8px',
|
|
||||||
borderColor: '#565656',
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderWidth: '1px'
|
|
||||||
},
|
|
||||||
backdropFilter: 'blur(1px)'
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = responsiveFontSizes(
|
const theme = responsiveFontSizes(
|
||||||
createTheme({
|
createTheme({
|
||||||
@@ -22,13 +14,10 @@ const theme = responsiveFontSizes(
|
|||||||
palette: {
|
palette: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#2196f3' // blue[500]
|
main: blue[500]
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: '#607d8b' // blueGrey[500]
|
main: blueGrey[500]
|
||||||
},
|
|
||||||
text: {
|
|
||||||
disabled: '#eee' // white
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,38 +1,34 @@
|
|||||||
|
import { FC, useContext, useState } from 'react';
|
||||||
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
import { Box, Fab, Paper, Typography, Button } from '@mui/material';
|
||||||
import ForwardIcon from '@mui/icons-material/Forward';
|
import ForwardIcon from '@mui/icons-material/Forward';
|
||||||
import { Box, Paper, Typography, MenuItem, TextField, Button } from '@mui/material';
|
|
||||||
import { useRequest } from 'alova';
|
|
||||||
import { useContext, useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
|
|
||||||
import type { Locales } from 'i18n/i18n-types';
|
import * as AuthenticationApi from './api/authentication';
|
||||||
import type { ChangeEventHandler, FC } from 'react';
|
import { PROJECT_NAME } from './api/env';
|
||||||
import type { SignInRequest } from 'types';
|
import { AuthenticationContext } from './contexts/authentication';
|
||||||
import * as AuthenticationApi from 'api/authentication';
|
|
||||||
import { PROJECT_NAME } from 'api/env';
|
|
||||||
|
|
||||||
import { ValidatedPasswordField, ValidatedTextField } from 'components';
|
import { extractErrorMessage, onEnterCallback, updateValue } from './utils';
|
||||||
import { AuthenticationContext } from 'contexts/authentication';
|
import { SignInRequest } from './types';
|
||||||
|
import { ValidatedTextField } from './components';
|
||||||
|
import { SIGN_IN_REQUEST_VALIDATOR, validate } from './validators';
|
||||||
|
|
||||||
import DEflag from 'i18n/DE.svg';
|
import { I18nContext } from './i18n/i18n-react';
|
||||||
import FRflag from 'i18n/FR.svg';
|
import type { Locales } from './i18n/i18n-types';
|
||||||
import GBflag from 'i18n/GB.svg';
|
import { loadLocaleAsync } from './i18n/i18n-util.async';
|
||||||
import ITflag from 'i18n/IT.svg';
|
|
||||||
import NLflag from 'i18n/NL.svg';
|
import { ReactComponent as NLflag } from './i18n/NL.svg';
|
||||||
import NOflag from 'i18n/NO.svg';
|
import { ReactComponent as DEflag } from './i18n/DE.svg';
|
||||||
import PLflag from 'i18n/PL.svg';
|
import { ReactComponent as GBflag } from './i18n/GB.svg';
|
||||||
import SKflag from 'i18n/SK.svg';
|
import { ReactComponent as SVflag } from './i18n/SV.svg';
|
||||||
import SVflag from 'i18n/SV.svg';
|
import { ReactComponent as PLflag } from './i18n/PL.svg';
|
||||||
import TRflag from 'i18n/TR.svg';
|
import { ReactComponent as NOflag } from './i18n/NO.svg';
|
||||||
import { I18nContext } from 'i18n/i18n-react';
|
import { ReactComponent as FRflag } from './i18n/FR.svg';
|
||||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
|
||||||
import { onEnterCallback, updateValue } from 'utils';
|
|
||||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
|
||||||
|
|
||||||
const SignIn: FC = () => {
|
const SignIn: FC = () => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const { LL, setLocale, locale } = useContext(I18nContext);
|
|
||||||
|
|
||||||
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -41,29 +37,8 @@ const SignIn: FC = () => {
|
|||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), {
|
|
||||||
immediate: false
|
|
||||||
});
|
|
||||||
|
|
||||||
onSuccess((response) => {
|
|
||||||
if (response.data) {
|
|
||||||
authenticationContext.signIn(response.data.access_token);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateLoginRequestValue = updateValue(setSignInRequest);
|
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 () => {
|
const validateAndSignIn = async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
SIGN_IN_REQUEST_VALIDATOR.messages({
|
SIGN_IN_REQUEST_VALIDATOR.messages({
|
||||||
@@ -71,17 +46,34 @@ const SignIn: FC = () => {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
||||||
await signIn();
|
signIn();
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(errors);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const signIn = async () => {
|
||||||
|
try {
|
||||||
|
const { data: loginResponse } = await AuthenticationApi.signIn(signInRequest);
|
||||||
|
authenticationContext.signIn(loginResponse.access_token);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
enqueueSnackbar(LL.INVALID_LOGIN(), { variant: 'warning' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar(extractErrorMessage(error, LL.ERROR()), { variant: 'error' });
|
||||||
|
}
|
||||||
|
setProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitOnEnter = onEnterCallback(signIn);
|
const submitOnEnter = onEnterCallback(signIn);
|
||||||
|
|
||||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
|
const { LL, setLocale, locale } = useContext(I18nContext);
|
||||||
const loc = target.value as Locales;
|
|
||||||
|
const selectLocale = async (loc: Locales) => {
|
||||||
localStorage.setItem('lang', loc);
|
localStorage.setItem('lang', loc);
|
||||||
await loadLocaleAsync(loc);
|
await loadLocaleAsync(loc);
|
||||||
setLocale(loc);
|
setLocale(loc);
|
||||||
@@ -101,91 +93,81 @@ const SignIn: FC = () => {
|
|||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
paddingTop: '172px',
|
paddingTop: '200px',
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: '50% ' + theme.spacing(2),
|
backgroundPosition: '50% ' + theme.spacing(2),
|
||||||
|
backgroundSize: 'auto 150px',
|
||||||
width: '100%'
|
width: '100%'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||||
|
<Box
|
||||||
<TextField name="locale" variant="outlined" value={locale} onChange={onLocaleSelected} size="small" select>
|
sx={{
|
||||||
<MenuItem key="de" value="de">
|
'& button, & a, & .MuiCard-root': {
|
||||||
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
mt: 0.5,
|
||||||
DE
|
mx: 0.5
|
||||||
</MenuItem>
|
}
|
||||||
<MenuItem key="en" value="en">
|
}}
|
||||||
<img src={GBflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
>
|
||||||
|
<Button size="small" variant={locale === 'en' ? 'contained' : 'outlined'} onClick={() => selectLocale('en')}>
|
||||||
|
<GBflag style={{ width: 24 }} />
|
||||||
EN
|
EN
|
||||||
</MenuItem>
|
</Button>
|
||||||
<MenuItem key="fr" value="fr">
|
<Button size="small" variant={locale === 'de' ? 'contained' : 'outlined'} onClick={() => selectLocale('de')}>
|
||||||
<img src={FRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<DEflag style={{ width: 24 }} />
|
||||||
|
DE
|
||||||
|
</Button>
|
||||||
|
<Button size="small" variant={locale === 'fr' ? 'contained' : 'outlined'} onClick={() => selectLocale('fr')}>
|
||||||
|
<FRflag style={{ width: 24 }} />
|
||||||
FR
|
FR
|
||||||
</MenuItem>
|
</Button>
|
||||||
<MenuItem key="it" value="it">
|
<Button size="small" variant={locale === 'nl' ? 'contained' : 'outlined'} onClick={() => selectLocale('nl')}>
|
||||||
<img src={ITflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<NLflag style={{ width: 24 }} />
|
||||||
IT
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="nl" value="nl">
|
|
||||||
<img src={NLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
NL
|
NL
|
||||||
</MenuItem>
|
</Button>
|
||||||
<MenuItem key="no" value="no">
|
<Button size="small" variant={locale === 'no' ? 'contained' : 'outlined'} onClick={() => selectLocale('no')}>
|
||||||
<img src={NOflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<NOflag style={{ width: 24 }} />
|
||||||
NO
|
NO
|
||||||
</MenuItem>
|
</Button>
|
||||||
<MenuItem key="pl" value="pl">
|
<Button size="small" variant={locale === 'pl' ? 'contained' : 'outlined'} onClick={() => selectLocale('pl')}>
|
||||||
<img src={PLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<PLflag style={{ width: 24 }} />
|
||||||
PL
|
PL
|
||||||
</MenuItem>
|
</Button>
|
||||||
<MenuItem key="sk" value="sk">
|
<Button size="small" variant={locale === 'sv' ? 'contained' : 'outlined'} onClick={() => selectLocale('sv')}>
|
||||||
<img src={SKflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<SVflag style={{ width: 24 }} />
|
||||||
SK
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="sv" value="sv">
|
|
||||||
<img src={SVflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
SV
|
SV
|
||||||
</MenuItem>
|
</Button>
|
||||||
<MenuItem key="tr" value="tr">
|
</Box>
|
||||||
<img src={TRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
TR
|
|
||||||
</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Box display="flex" flexDirection="column" alignItems="center">
|
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
sx={{
|
|
||||||
width: 240
|
|
||||||
}}
|
|
||||||
name="username"
|
name="username"
|
||||||
label={LL.USERNAME(0)}
|
label={LL.USERNAME(0)}
|
||||||
value={signInRequest.username}
|
value={signInRequest.username}
|
||||||
onChange={updateLoginRequestValue}
|
onChange={updateLoginRequestValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<ValidatedPasswordField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
sx={{
|
type="password"
|
||||||
width: 240
|
|
||||||
}}
|
|
||||||
name="password"
|
name="password"
|
||||||
label={LL.PASSWORD()}
|
label={LL.PASSWORD()}
|
||||||
value={signInRequest.password}
|
value={signInRequest.password}
|
||||||
onChange={updateLoginRequestValue}
|
onChange={updateLoginRequestValue}
|
||||||
onKeyDown={submitOnEnter}
|
onKeyDown={submitOnEnter}
|
||||||
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</Box>
|
<Fab variant="extended" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
||||||
|
|
||||||
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
|
||||||
<ForwardIcon sx={{ mr: 1 }} />
|
<ForwardIcon sx={{ mr: 1 }} />
|
||||||
{LL.SIGN_IN()}
|
{LL.SIGN_IN()}
|
||||||
</Button>
|
</Fab>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import type { APSettings, APStatus } from 'types';
|
import { APSettings, APStatus } from '../types';
|
||||||
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
export const readAPStatus = () => alovaInstance.Get<APStatus>('/rest/apStatus');
|
export function readAPStatus(): AxiosPromise<APStatus> {
|
||||||
export const readAPSettings = () => alovaInstance.Get<APSettings>('/rest/apSettings');
|
return AXIOS.get('/apStatus');
|
||||||
export const updateAPSettings = (data: APSettings) => alovaInstance.Post<APSettings>('/rest/apSettings', data);
|
}
|
||||||
|
|
||||||
|
export function readAPSettings(): AxiosPromise<APSettings> {
|
||||||
|
return AXIOS.get('/apSettings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAPSettings(apSettings: APSettings): AxiosPromise<APSettings> {
|
||||||
|
return AXIOS.post('/apSettings', apSettings);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
import { jwtDecode } from 'jwt-decode';
|
import { AxiosPromise } from 'axios';
|
||||||
import { ACCESS_TOKEN, alovaInstance } from './endpoints';
|
import * as H from 'history';
|
||||||
import type * as H from 'history';
|
import jwtDecode from 'jwt-decode';
|
||||||
import type { Path } from 'react-router-dom';
|
import { Path } from 'react-router-dom';
|
||||||
|
|
||||||
import type { Me, SignInRequest, SignInResponse } from 'types';
|
import { Features, Me, SignInRequest, SignInResponse } from '../types';
|
||||||
|
|
||||||
|
import { ACCESS_TOKEN, AXIOS } from './endpoints';
|
||||||
|
import { PROJECT_PATH } from './env';
|
||||||
|
|
||||||
export const SIGN_IN_PATHNAME = 'loginPathname';
|
export const SIGN_IN_PATHNAME = 'loginPathname';
|
||||||
export const SIGN_IN_SEARCH = 'loginSearch';
|
export const SIGN_IN_SEARCH = 'loginSearch';
|
||||||
|
|
||||||
export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization');
|
export const getDefaultRoute = (features: Features) => (features.project ? `/${PROJECT_PATH}` : '/wifi');
|
||||||
export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request);
|
|
||||||
|
|
||||||
|
export function verifyAuthorization(): AxiosPromise<void> {
|
||||||
|
return AXIOS.get('/verifyAuthorization');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signIn(request: SignInRequest): AxiosPromise<SignInResponse> {
|
||||||
|
return AXIOS.post('/signIn', request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
|
||||||
|
*/
|
||||||
export function getStorage() {
|
export function getStorage() {
|
||||||
return localStorage || sessionStorage;
|
return localStorage || sessionStorage;
|
||||||
}
|
}
|
||||||
@@ -27,18 +40,18 @@ export function clearLoginRedirect() {
|
|||||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect(): Partial<Path> {
|
export function fetchLoginRedirect(features: Features): Partial<Path> {
|
||||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
return {
|
return {
|
||||||
pathname: signInPathname || `/dashboard`,
|
pathname: signInPathname || getDefaultRoute(features),
|
||||||
search: (signInPathname && signInSearch) || undefined
|
search: (signInPathname && signInSearch) || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearAccessToken = () => localStorage.removeItem(ACCESS_TOKEN);
|
export const clearAccessToken = () => localStorage.removeItem(ACCESS_TOKEN);
|
||||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
|
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
|
||||||
|
|
||||||
export function addAccessTokenParameter(url: string) {
|
export function addAccessTokenParameter(url: string) {
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
|
|||||||
@@ -1,60 +1,105 @@
|
|||||||
import { xhrRequestAdapter } from '@alova/adapter-xhr';
|
import axios, { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios';
|
||||||
import { createAlova } from 'alova';
|
|
||||||
import ReactHook from 'alova/react';
|
|
||||||
import { unpack } from '../api/unpack';
|
|
||||||
|
|
||||||
|
import { decode } from '@msgpack/msgpack';
|
||||||
|
|
||||||
|
export const WS_BASE_URL = '/ws/';
|
||||||
|
export const API_BASE_URL = '/rest/';
|
||||||
|
export const ES_BASE_URL = '/es/';
|
||||||
|
export const EMSESP_API_BASE_URL = '/api/';
|
||||||
export const ACCESS_TOKEN = 'access_token';
|
export const ACCESS_TOKEN = 'access_token';
|
||||||
|
export const WEB_SOCKET_ROOT = calculateWebSocketRoot(WS_BASE_URL);
|
||||||
|
export const EVENT_SOURCE_ROOT = calculateEventSourceRoot(ES_BASE_URL);
|
||||||
|
|
||||||
const host = window.location.host;
|
export const AXIOS = axios.create({
|
||||||
export const WEB_SOCKET_ROOT = 'ws://' + host + '/ws/';
|
baseURL: API_BASE_URL,
|
||||||
export const EVENT_SOURCE_ROOT = 'http://' + host + '/es/';
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
transformRequest: [
|
||||||
responded: {
|
(data, headers) => {
|
||||||
onSuccess: async (response) => {
|
if (headers) {
|
||||||
// if (response.status === 202) {
|
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||||
// throw new Error('Wait'); // wifi scan in progress
|
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||||
// } 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);
|
|
||||||
}
|
}
|
||||||
|
if (headers['Content-Type'] !== 'application/json') {
|
||||||
return 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);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export const alovaInstanceGH = createAlova({
|
export const AXIOS_API = axios.create({
|
||||||
baseURL: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
|
baseURL: EMSESP_API_BASE_URL,
|
||||||
statesHook: ReactHook,
|
headers: {
|
||||||
requestAdapter: xhrRequestAdapter()
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
transformRequest: [
|
||||||
|
(data, headers) => {
|
||||||
|
if (headers) {
|
||||||
|
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||||
|
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
if (headers['Content-Type'] !== 'application/json') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AXIOS_BIN = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
transformRequest: [
|
||||||
|
(data, headers) => {
|
||||||
|
if (headers) {
|
||||||
|
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||||
|
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
if (headers['Content-Type'] !== 'application/json') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
transformResponse: [
|
||||||
|
(data) => {
|
||||||
|
return decode(data);
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function calculateWebSocketRoot(webSocketPath: string) {
|
||||||
|
const location = window.location;
|
||||||
|
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
return webProtocol + '//' + location.host + webSocketPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEventSourceRoot(endpointPath: string) {
|
||||||
|
const location = window.location;
|
||||||
|
return location.protocol + '//' + location.host + endpointPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadConfig {
|
||||||
|
cancelToken?: CancelToken;
|
||||||
|
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const startUploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise<void> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
return AXIOS.post(url, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
...(config || {})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const PROJECT_NAME = 'EMS-ESP';
|
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME || 'EMS-ESP';
|
||||||
|
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH || 'project';
|
||||||
|
|||||||
9
interface/src/api/features.ts
Normal file
9
interface/src/api/features.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
|
import { Features } from '../types';
|
||||||
|
|
||||||
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
|
export function readFeatures(): AxiosPromise<Features> {
|
||||||
|
return AXIOS.get('/features');
|
||||||
|
}
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
import { AxiosPromise } from 'axios';
|
||||||
import type { MqttSettings, MqttStatus } from 'types';
|
import { MqttSettings, MqttStatus } from '../types';
|
||||||
|
|
||||||
export const readMqttStatus = () => alovaInstance.Get<MqttStatus>('/rest/mqttStatus');
|
import { AXIOS } from './endpoints';
|
||||||
export const readMqttSettings = () => alovaInstance.Get<MqttSettings>('/rest/mqttSettings');
|
|
||||||
export const updateMqttSettings = (data: MqttSettings) => alovaInstance.Post<MqttSettings>('/rest/mqttSettings', data);
|
export function readMqttStatus(): AxiosPromise<MqttStatus> {
|
||||||
|
return AXIOS.get('/mqttStatus');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readMqttSettings(): AxiosPromise<MqttSettings> {
|
||||||
|
return AXIOS.get('/mqttSettings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMqttSettings(mqttSettings: MqttSettings): AxiosPromise<MqttSettings> {
|
||||||
|
return AXIOS.post('/mqttSettings', mqttSettings);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types';
|
import { WiFiNetworkList, NetworkSettings, NetworkStatus } from '../types';
|
||||||
|
|
||||||
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatus>('/rest/networkStatus');
|
import { AXIOS } from './endpoints';
|
||||||
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
|
||||||
export const listNetworks = () =>
|
export function readNetworkStatus(): AxiosPromise<NetworkStatus> {
|
||||||
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
return AXIOS.get('/networkStatus');
|
||||||
name: 'listNetworks',
|
}
|
||||||
timeout: 20000 // timeout 20 seconds
|
|
||||||
});
|
export function scanNetworks(): AxiosPromise<void> {
|
||||||
export const readNetworkSettings = () =>
|
return AXIOS.get('/scanNetworks');
|
||||||
alovaInstance.Get<NetworkSettings>('/rest/networkSettings', { name: 'networkSettings' });
|
}
|
||||||
export const updateNetworkSettings = (wifiSettings: NetworkSettings) =>
|
|
||||||
alovaInstance.Post<NetworkSettings>('/rest/networkSettings', wifiSettings);
|
export function listNetworks(): AxiosPromise<WiFiNetworkList> {
|
||||||
|
return AXIOS.get('/listNetworks');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readNetworkSettings(): AxiosPromise<NetworkSettings> {
|
||||||
|
return AXIOS.get('/networkSettings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNetworkSettings(wifiSettings: NetworkSettings): AxiosPromise<NetworkSettings> {
|
||||||
|
return AXIOS.post('/networkSettings', wifiSettings);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
import { AxiosPromise } from 'axios';
|
||||||
import type { NTPSettings, NTPStatus, Time } from 'types';
|
import { NTPSettings, NTPStatus, Time } from '../types';
|
||||||
|
|
||||||
export const readNTPStatus = () => alovaInstance.Get<NTPStatus>('/rest/ntpStatus');
|
import { AXIOS } from './endpoints';
|
||||||
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);
|
export function readNTPStatus(): AxiosPromise<NTPStatus> {
|
||||||
|
return AXIOS.get('/ntpStatus');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readNTPSettings(): AxiosPromise<NTPSettings> {
|
||||||
|
return AXIOS.get('/ntpSettings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNTPSettings(ntpSettings: NTPSettings): AxiosPromise<NTPSettings> {
|
||||||
|
return AXIOS.post('/ntpSettings', ntpSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTime(time: Time): AxiosPromise<Time> {
|
||||||
|
return AXIOS.post('/time', time);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import type { SecuritySettings, Token } from 'types';
|
import { SecuritySettings, Token } from '../types';
|
||||||
|
|
||||||
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings');
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
|
export function readSecuritySettings(): AxiosPromise<SecuritySettings> {
|
||||||
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
return AXIOS.get('/securitySettings');
|
||||||
|
}
|
||||||
|
|
||||||
export const generateToken = (username?: string) =>
|
export function updateSecuritySettings(securitySettings: SecuritySettings): AxiosPromise<SecuritySettings> {
|
||||||
alovaInstance.Get<Token>('/rest/generateToken', {
|
return AXIOS.post('/securitySettings', securitySettings);
|
||||||
params: { username }
|
}
|
||||||
});
|
|
||||||
|
export function generateToken(username?: string): AxiosPromise<Token> {
|
||||||
|
return AXIOS.get('/generateToken', { params: { username } });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,42 +1,44 @@
|
|||||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
import { AxiosPromise } from 'axios';
|
||||||
import type { OTASettings, SystemStatus, LogSettings } from 'types';
|
|
||||||
|
|
||||||
// SystemStatus - also used to ping in Restart monitor for pinging
|
import { OTASettings, SystemStatus, LogSettings, LogEntries } from '../types';
|
||||||
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
|
||||||
|
|
||||||
// commands
|
import { AXIOS, AXIOS_BIN, FileUploadConfig, startUploadFile } from './endpoints';
|
||||||
export const restart = () => alovaInstance.Post('/rest/restart');
|
|
||||||
export const partition = () => alovaInstance.Post('/rest/partition');
|
|
||||||
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
|
|
||||||
|
|
||||||
// OTA
|
export function readSystemStatus(timeout?: number): AxiosPromise<SystemStatus> {
|
||||||
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
|
return AXIOS.get('/systemStatus', { timeout });
|
||||||
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) => {
|
export function restart(): AxiosPromise<void> {
|
||||||
const formData = new FormData();
|
return AXIOS.post('/restart');
|
||||||
formData.append('file', file);
|
}
|
||||||
return alovaInstance.Post('/rest/uploadFile', formData, {
|
|
||||||
timeout: 60000, // override timeout for uploading firmware - 1 minute
|
export function partition(): AxiosPromise<void> {
|
||||||
enableUpload: true
|
return AXIOS.post('/partition');
|
||||||
});
|
}
|
||||||
};
|
|
||||||
|
export function factoryReset(): AxiosPromise<void> {
|
||||||
|
return AXIOS.post('/factoryReset');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readOTASettings(): AxiosPromise<OTASettings> {
|
||||||
|
return AXIOS.get('/otaSettings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateOTASettings(otaSettings: OTASettings): AxiosPromise<OTASettings> {
|
||||||
|
return AXIOS.post('/otaSettings', otaSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadFile = (file: File, config?: FileUploadConfig): AxiosPromise<void> =>
|
||||||
|
startUploadFile('/uploadFile', file, config);
|
||||||
|
|
||||||
|
export function readLogSettings(): AxiosPromise<LogSettings> {
|
||||||
|
return AXIOS.get('/logSettings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateLogSettings(logSettings: LogSettings): AxiosPromise<LogSettings> {
|
||||||
|
return AXIOS.post('/logSettings', logSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readLogEntries(): AxiosPromise<LogEntries> {
|
||||||
|
return AXIOS_BIN.get('/fetchLog');
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import { Box } from '@mui/material';
|
import { FC } from 'react';
|
||||||
import type { BoxProps } from '@mui/material';
|
import { Box, BoxProps } from '@mui/material';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => (
|
const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => {
|
||||||
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
'& button, & a, & .MuiCard-root': {
|
'& button, & a, & .MuiCard-root': {
|
||||||
@@ -21,5 +21,6 @@ const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => (
|
|||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ButtonRow;
|
export default ButtonRow;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { Box, BoxProps, SvgIconProps, Theme, Typography, useTheme } from '@mui/material';
|
||||||
|
|
||||||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||||
import ErrorIcon from '@mui/icons-material/Error';
|
|
||||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
|
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
|
||||||
import { Box, Typography, useTheme } from '@mui/material';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import type { BoxProps, SvgIconProps, Theme } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error';
|
type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Paper, Divider } from '@mui/material';
|
import { FC } from 'react';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import { Paper, Divider } from '@mui/material';
|
||||||
|
|
||||||
|
import { RequiredChildrenProps } from '../utils';
|
||||||
|
|
||||||
interface SectionContentProps extends RequiredChildrenProps {
|
interface SectionContentProps extends RequiredChildrenProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ export * from './upload';
|
|||||||
export { default as SectionContent } from './SectionContent';
|
export { default as SectionContent } from './SectionContent';
|
||||||
export { default as ButtonRow } from './ButtonRow';
|
export { default as ButtonRow } from './ButtonRow';
|
||||||
export { default as MessageBox } from './MessageBox';
|
export { default as MessageBox } from './MessageBox';
|
||||||
export { default as BlockNavigation } from './routing/BlockNavigation';
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { FormControlLabel } from '@mui/material';
|
import { FC } from 'react';
|
||||||
import type { FormControlLabelProps } from '@mui/material';
|
import { FormControlLabel, FormControlLabelProps } from '@mui/material';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
import { FC, useState } from 'react';
|
||||||
|
|
||||||
|
import { IconButton, InputAdornment } from '@mui/material';
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
import { IconButton, InputAdornment } from '@mui/material';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import ValidatedTextField from './ValidatedTextField';
|
import ValidatedTextField, { ValidatedTextFieldProps } from './ValidatedTextField';
|
||||||
import type { ValidatedTextFieldProps } from './ValidatedTextField';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
|
type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
|
||||||
|
|
||||||
@@ -20,7 +19,11 @@ const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ InputProps, .
|
|||||||
...InputProps,
|
...InputProps,
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
|
<IconButton
|
||||||
|
aria-label="toggle password visibility"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
|
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FormHelperText, TextField } from '@mui/material';
|
import { FC } from 'react';
|
||||||
import type { TextFieldProps } from '@mui/material';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
import type { FC } from 'react';
|
import { FormHelperText, TextField, TextFieldProps } from '@mui/material';
|
||||||
|
|
||||||
interface ValidatedFieldProps {
|
interface ValidatedFieldProps {
|
||||||
fieldErrors?: ValidateFieldsError;
|
fieldErrors?: ValidateFieldsError;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Box, Toolbar } from '@mui/material';
|
import { FC, useState, useEffect } from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import LayoutAppBar from './LayoutAppBar';
|
|
||||||
|
import { Box, Toolbar } from '@mui/material';
|
||||||
|
|
||||||
|
import { PROJECT_NAME } from '../../api/env';
|
||||||
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
|
|
||||||
import LayoutDrawer from './LayoutDrawer';
|
import LayoutDrawer from './LayoutDrawer';
|
||||||
|
import LayoutAppBar from './LayoutAppBar';
|
||||||
import { LayoutContext } from './context';
|
import { LayoutContext } from './context';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
export const DRAWER_WIDTH = 240;
|
||||||
import { PROJECT_NAME } from 'api/env';
|
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 210;
|
|
||||||
|
|
||||||
const Layout: FC<RequiredChildrenProps> = ({ children }) => {
|
const Layout: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import { FC, useContext } from 'react';
|
||||||
import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
|
|
||||||
import LayoutAuthMenu from './LayoutAuthMenu';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 210;
|
import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
|
||||||
|
import LayoutAuthMenu from './LayoutAuthMenu';
|
||||||
|
|
||||||
|
import { FeaturesContext } from '../../contexts/features';
|
||||||
|
|
||||||
|
export const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
interface LayoutAppBarProps {
|
interface LayoutAppBarProps {
|
||||||
title: string;
|
title: string;
|
||||||
onToggleDrawer: () => void;
|
onToggleDrawer: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => (
|
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
||||||
|
const { features } = useContext(FeaturesContext);
|
||||||
|
|
||||||
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -21,16 +28,23 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton color="inherit" edge="start" onClick={onToggleDrawer} sx={{ mr: 2, display: { md: 'none' } }}>
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open drawer"
|
||||||
|
edge="start"
|
||||||
|
onClick={onToggleDrawer}
|
||||||
|
sx={{ mr: 2, display: { md: 'none' } }}
|
||||||
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6" noWrap component="div">
|
<Typography variant="h6" noWrap component="div">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
<LayoutAuthMenu />
|
{features.security && <LayoutAuthMenu />}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default LayoutAppBar;
|
export default LayoutAppBar;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
import { FC, useState, useContext, ChangeEventHandler } from 'react';
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -9,27 +9,27 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Avatar,
|
Avatar,
|
||||||
styled,
|
styled,
|
||||||
|
TypographyProps,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField
|
TextField
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useState, useContext } from 'react';
|
|
||||||
import type { TypographyProps } from '@mui/material';
|
|
||||||
import type { Locales } from 'i18n/i18n-types';
|
|
||||||
import type { FC, ChangeEventHandler } from 'react';
|
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
|
||||||
import DEflag from 'i18n/DE.svg';
|
|
||||||
import FRflag from 'i18n/FR.svg';
|
|
||||||
import GBflag from 'i18n/GB.svg';
|
|
||||||
import ITflag from 'i18n/IT.svg';
|
|
||||||
import NLflag from 'i18n/NL.svg';
|
|
||||||
import NOflag from 'i18n/NO.svg';
|
|
||||||
import PLflag from 'i18n/PL.svg';
|
|
||||||
import SKflag from 'i18n/SK.svg';
|
|
||||||
import SVflag from 'i18n/SV.svg';
|
|
||||||
import TRflag from 'i18n/TR.svg';
|
|
||||||
|
|
||||||
import { I18nContext } from 'i18n/i18n-react';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||||
|
|
||||||
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
|
|
||||||
|
import { I18nContext } from '../../i18n/i18n-react';
|
||||||
|
import type { Locales } from '../../i18n/i18n-types';
|
||||||
|
import { loadLocaleAsync } from '../../i18n/i18n-util.async';
|
||||||
|
|
||||||
|
import { ReactComponent as NLflag } from '../../i18n/NL.svg';
|
||||||
|
import { ReactComponent as DEflag } from '../../i18n/DE.svg';
|
||||||
|
import { ReactComponent as GBflag } from '../../i18n/GB.svg';
|
||||||
|
import { ReactComponent as SVflag } from '../../i18n/SV.svg';
|
||||||
|
import { ReactComponent as PLflag } from '../../i18n/PL.svg';
|
||||||
|
import { ReactComponent as NOflag } from '../../i18n/NO.svg';
|
||||||
|
import { ReactComponent as FRflag } from '../../i18n/FR.svg';
|
||||||
|
|
||||||
const ItemTypography = styled(Typography)<TypographyProps>({
|
const ItemTypography = styled(Typography)<TypographyProps>({
|
||||||
maxWidth: '250px',
|
maxWidth: '250px',
|
||||||
@@ -74,46 +74,35 @@ const LayoutAuthMenu: FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
select
|
select
|
||||||
>
|
>
|
||||||
<MenuItem key="de" value="de">
|
|
||||||
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
DE
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="en" value="en">
|
<MenuItem key="en" value="en">
|
||||||
<img src={GBflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<GBflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
EN
|
EN
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<Divider />
|
||||||
|
<MenuItem key="de" value="de">
|
||||||
|
<DEflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
|
DE
|
||||||
|
</MenuItem>
|
||||||
<MenuItem key="fr" value="fr">
|
<MenuItem key="fr" value="fr">
|
||||||
<img src={FRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<FRflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
FR
|
FR
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem key="it" value="it">
|
|
||||||
<img src={ITflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
IT
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="nl" value="nl">
|
<MenuItem key="nl" value="nl">
|
||||||
<img src={NLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<NLflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
NL
|
NL
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem key="no" value="no">
|
<MenuItem key="no" value="no">
|
||||||
<img src={NOflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<NOflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
NO
|
NO
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem key="pl" value="pl">
|
<MenuItem key="pl" value="pl">
|
||||||
<img src={PLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<PLflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
PL
|
PL
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem key="sk" value="sk">
|
|
||||||
<img src={SKflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
SK
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="sv" value="sv">
|
<MenuItem key="sv" value="sv">
|
||||||
<img src={SVflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<SVflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
SV
|
SV
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem key="tr" value="tr">
|
|
||||||
<img src={TRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
TR
|
|
||||||
</MenuItem>
|
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
import { FC } from 'react';
|
||||||
import { DRAWER_WIDTH } from './Layout';
|
|
||||||
import LayoutMenu from './LayoutMenu';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import { PROJECT_NAME } from 'api/env';
|
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
||||||
|
|
||||||
|
import { PROJECT_NAME } from '../../api/env';
|
||||||
|
|
||||||
|
import LayoutMenu from './LayoutMenu';
|
||||||
|
import { DRAWER_WIDTH } from './Layout';
|
||||||
|
|
||||||
const LayoutDrawerLogo = styled('img')(({ theme }) => ({
|
const LayoutDrawerLogo = styled('img')(({ theme }) => ({
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
@@ -11,7 +13,7 @@ const LayoutDrawerLogo = styled('img')(({ theme }) => ({
|
|||||||
marginRight: theme.spacing(2)
|
marginRight: theme.spacing(2)
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up('sm')]: {
|
||||||
height: 38,
|
height: 36,
|
||||||
marginRight: theme.spacing(2)
|
marginRight: theme.spacing(2)
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -27,7 +29,9 @@ const LayoutDrawer: FC<LayoutDrawerProps> = ({ mobileOpen, onClose }) => {
|
|||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
<Box display="flex" alignItems="center" px={2}>
|
<Box display="flex" alignItems="center" px={2}>
|
||||||
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
|
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
|
||||||
<Typography variant="h6">{PROJECT_NAME}</Typography>
|
<Typography variant="h6" color="textPrimary">
|
||||||
|
{PROJECT_NAME}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider absolute />
|
<Divider absolute />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|||||||
@@ -1,45 +1,40 @@
|
|||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import { FC, useContext } from 'react';
|
||||||
|
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
|
||||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
|
||||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
|
||||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
|
||||||
import TuneIcon from '@mui/icons-material/Tune';
|
|
||||||
import { Divider, List } from '@mui/material';
|
import { Divider, List } from '@mui/material';
|
||||||
import { useContext } from 'react';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import LayoutMenuItem from 'components/layout/LayoutMenuItem';
|
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||||
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||||
|
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||||
|
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
import { FeaturesContext } from '../../contexts/features';
|
||||||
|
import ProjectMenu from '../../project/ProjectMenu';
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import LayoutMenuItem from './LayoutMenuItem';
|
||||||
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const LayoutMenu: FC = () => {
|
const LayoutMenu: FC = () => {
|
||||||
|
const { features } = useContext(FeaturesContext);
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{features.project && (
|
||||||
<List disablePadding component="nav">
|
<List disablePadding component="nav">
|
||||||
<LayoutMenuItem icon={DashboardIcon} label={LL.DASHBOARD()} to={`/dashboard`} />
|
<ProjectMenu />
|
||||||
<LayoutMenuItem
|
|
||||||
icon={TuneIcon}
|
|
||||||
label={LL.SETTINGS_OF('')}
|
|
||||||
to={`/settings`}
|
|
||||||
disabled={!authenticatedContext.me.admin}
|
|
||||||
/>
|
|
||||||
<LayoutMenuItem icon={InfoIcon} label={LL.HELP_OF('')} to={`/help`} />
|
|
||||||
<Divider />
|
<Divider />
|
||||||
</List>
|
</List>
|
||||||
|
)}
|
||||||
<List disablePadding component="nav">
|
<List disablePadding component="nav">
|
||||||
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
||||||
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
||||||
<LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />
|
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />}
|
||||||
<LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />
|
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
|
||||||
<LayoutMenuItem
|
<LayoutMenuItem
|
||||||
icon={LockIcon}
|
icon={LockIcon}
|
||||||
label={LL.SECURITY(0)}
|
label={LL.SECURITY(0)}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
import { FC } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import type { SvgIconProps } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import { routeMatches } from 'utils';
|
import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material';
|
||||||
|
|
||||||
|
import { grey } from '@mui/material/colors';
|
||||||
|
|
||||||
|
import { routeMatches } from '../../utils';
|
||||||
|
|
||||||
interface LayoutMenuItemProps {
|
interface LayoutMenuItemProps {
|
||||||
icon: React.ComponentType<SvgIconProps>;
|
icon: React.ComponentType<SvgIconProps>;
|
||||||
@@ -15,15 +17,13 @@ interface LayoutMenuItemProps {
|
|||||||
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
|
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const selected = routeMatches(to, pathname);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding selected={routeMatches(to, pathname)}>
|
||||||
<ListItemButton component={Link} to={to} disabled={disabled} selected={selected}>
|
<ListItemButton component={Link} to={to} disabled={disabled}>
|
||||||
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
|
<ListItemIcon sx={{ color: grey[500] }}>
|
||||||
<Icon />
|
<Icon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>{label}</ListItemText>
|
<ListItemText>{label}</ListItemText>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { Box, Paper, Typography } from '@mui/material';
|
import { Box, Paper, Typography } from '@mui/material';
|
||||||
import type { FC } from 'react';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
|
||||||
interface ApplicationErrorProps {
|
interface ApplicationErrorProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { Box, Button, CircularProgress, Typography } from '@mui/material';
|
import { Box, Button, CircularProgress, Typography } from '@mui/material';
|
||||||
import type { FC } from 'react';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
|
|
||||||
import { MessageBox } from 'components';
|
import { MessageBox } from '..';
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
interface FormLoaderProps {
|
interface FormLoaderProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CircularProgress, Box, Typography } from '@mui/material';
|
import { FC } from 'react';
|
||||||
import type { Theme } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { CircularProgress, Box, Typography, Theme } from '@mui/material';
|
||||||
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
interface LoadingSpinnerProps {
|
interface LoadingSpinnerProps {
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { unstable_Blocker as Blocker } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
|
|
||||||
interface BlockNavigationProps {
|
|
||||||
blocker: Blocker;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
|
|
||||||
const { LL } = useI18nContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}>
|
|
||||||
<DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle>
|
|
||||||
<DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button variant="outlined" onClick={() => blocker.reset?.()} color="secondary">
|
|
||||||
{LL.STAY()}
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" onClick={() => blocker.proceed?.()} color="primary">
|
|
||||||
{LL.LEAVE()}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlockNavigation;
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
|
|
||||||
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { FC, useContext, useEffect } from 'react';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import type { AuthenticatedContextValue } from 'contexts/authentication/context';
|
import {
|
||||||
import type { FC } from 'react';
|
AuthenticatedContext,
|
||||||
|
AuthenticatedContextValue,
|
||||||
|
AuthenticationContext
|
||||||
|
} from '../../contexts/authentication/context';
|
||||||
|
import { storeLoginRedirect } from '../../api/authentication';
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
import { storeLoginRedirect } from 'api/authentication';
|
|
||||||
import { AuthenticatedContext, AuthenticationContext } from 'contexts/authentication/context';
|
|
||||||
|
|
||||||
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import * as AuthenticationApi from '../../api/authentication';
|
||||||
import * as AuthenticationApi from 'api/authentication';
|
import { AuthenticationContext } from '../../contexts/authentication';
|
||||||
import { AuthenticationContext } from 'contexts/authentication';
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
|
import { FeaturesContext } from '../../contexts/features';
|
||||||
|
|
||||||
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
|
const { features } = useContext(FeaturesContext);
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
|
||||||
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect()} /> : <>{children}</>;
|
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect(features)} /> : <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireUnauthenticated;
|
export default RequireUnauthenticated;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Tabs, useMediaQuery, useTheme } from '@mui/material';
|
import React, { FC } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import { Tabs, useMediaQuery, useTheme } from '@mui/material';
|
||||||
|
|
||||||
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
|
|
||||||
interface RouterTabsProps extends RequiredChildrenProps {
|
interface RouterTabsProps extends RequiredChildrenProps {
|
||||||
value: string | false;
|
value: string | false;
|
||||||
@@ -14,7 +15,7 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
|
const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
const handleTabChange = (_event: any, path: string) => {
|
const handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||||
navigate(path);
|
navigate(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useMatch, useResolvedPath } from 'react-router-dom';
|
||||||
|
|
||||||
export const useRouterTab = () => {
|
export const useRouterTab = () => {
|
||||||
const loc = useLocation().pathname;
|
const routerTabPath = useResolvedPath(':tab');
|
||||||
const routerTab = loc.substring(0, loc.lastIndexOf('/')) ? loc : false;
|
const routerTabPathMatch = useMatch(routerTabPath.pathname);
|
||||||
|
|
||||||
// const routerTabPath = useResolvedPath(':tab');
|
|
||||||
// const routerTabPathMatch = useMatch(routerTabPath.pathname);
|
|
||||||
// const routerTab = routerTabPathMatch?.params?.tab || false;
|
|
||||||
|
|
||||||
|
const routerTab = routerTabPathMatch?.params?.tab || false;
|
||||||
return { routerTab } as const;
|
return { routerTab } as const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import { FC, Fragment } from 'react';
|
||||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
import { useDropzone, DropzoneState } from 'react-dropzone';
|
||||||
import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material';
|
|
||||||
import { Fragment } from 'react';
|
|
||||||
import { useDropzone } from 'react-dropzone';
|
|
||||||
import type { Theme } from '@mui/material';
|
|
||||||
import type { Progress } from 'alova';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import type { DropzoneState } from 'react-dropzone';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { AxiosProgressEvent } from 'axios';
|
||||||
|
|
||||||
|
import { Box, Button, LinearProgress, Theme, Typography, useTheme } from '@mui/material';
|
||||||
|
|
||||||
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const getBorderColor = (theme: Theme, props: DropzoneState) => {
|
const getBorderColor = (theme: Theme, props: DropzoneState) => {
|
||||||
if (props.isDragAccept) {
|
if (props.isDragAccept) {
|
||||||
@@ -26,13 +26,11 @@ const getBorderColor = (theme: Theme, props: DropzoneState) => {
|
|||||||
export interface SingleUploadProps {
|
export interface SingleUploadProps {
|
||||||
onDrop: (acceptedFiles: File[]) => void;
|
onDrop: (acceptedFiles: File[]) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
isUploading: boolean;
|
uploading: boolean;
|
||||||
progress: Progress;
|
progress?: AxiosProgressEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, progress }) => {
|
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, progress }) => {
|
||||||
const uploading = isUploading && progress.total > 0;
|
|
||||||
|
|
||||||
const dropzoneState = useDropzone({
|
const dropzoneState = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
accept: {
|
accept: {
|
||||||
@@ -40,21 +38,20 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, pr
|
|||||||
'application/json': ['.json'],
|
'application/json': ['.json'],
|
||||||
'text/plain': ['.md5']
|
'text/plain': ['.md5']
|
||||||
},
|
},
|
||||||
disabled: isUploading,
|
disabled: uploading,
|
||||||
multiple: false
|
multiple: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = dropzoneState;
|
const { getRootProps, getInputProps } = dropzoneState;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const progressText = () => {
|
const progressText = () => {
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
if (progress.total && progress.loaded) {
|
if (progress?.total) {
|
||||||
return progress.loaded <= progress.total
|
return LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%';
|
||||||
? LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%'
|
|
||||||
: LL.UPLOADING() + ': ' + Math.round((progress.total * 100) / progress.loaded) + '%';
|
|
||||||
}
|
}
|
||||||
|
return LL.UPLOADING() + `\u2026`;
|
||||||
}
|
}
|
||||||
return LL.UPLOAD_DROP_TEXT();
|
return LL.UPLOAD_DROP_TEXT();
|
||||||
};
|
};
|
||||||
@@ -84,14 +81,8 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, pr
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<Box width="100%" p={2}>
|
<Box width="100%" p={2}>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="determinate"
|
variant={!progress || progress.total ? 'determinate' : 'indeterminate'}
|
||||||
value={
|
value={!progress ? 0 : progress.total ? Math.round((progress.loaded * 100) / progress.total) : 0}
|
||||||
progress.total === 0 || progress.loaded === 0
|
|
||||||
? 0
|
|
||||||
: progress.loaded <= progress.total
|
|
||||||
? Math.round((progress.loaded * 100) / progress.total)
|
|
||||||
: Math.round((progress.total * 100) / progress.loaded)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>
|
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { default as SingleUpload } from './SingleUpload';
|
export { default as SingleUpload } from './SingleUpload';
|
||||||
|
export { default as useFileUpload } from './useFileUpload';
|
||||||
|
|||||||
70
interface/src/components/upload/useFileUpload.ts
Normal file
70
interface/src/components/upload/useFileUpload.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import axios, { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
import { extractErrorMessage } from '../../utils';
|
||||||
|
import { FileUploadConfig } from '../../api/endpoints';
|
||||||
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
|
interface MediaUploadOptions {
|
||||||
|
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFileUpload = ({ upload }: MediaUploadOptions) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const [uploading, setUploading] = useState<boolean>(false);
|
||||||
|
const [md5, setMd5] = useState<string>('');
|
||||||
|
const [uploadProgress, setUploadProgress] = useState<AxiosProgressEvent>();
|
||||||
|
const [uploadCancelToken, setUploadCancelToken] = useState<CancelTokenSource>();
|
||||||
|
|
||||||
|
const resetUploadingStates = () => {
|
||||||
|
setUploading(false);
|
||||||
|
setUploadProgress(undefined);
|
||||||
|
setUploadCancelToken(undefined);
|
||||||
|
setMd5('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelUpload = useCallback(() => {
|
||||||
|
uploadCancelToken?.cancel();
|
||||||
|
resetUploadingStates();
|
||||||
|
}, [uploadCancelToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
uploadCancelToken?.cancel();
|
||||||
|
};
|
||||||
|
}, [uploadCancelToken]);
|
||||||
|
|
||||||
|
const uploadFile = async (images: File[]) => {
|
||||||
|
try {
|
||||||
|
const cancelToken = axios.CancelToken.source();
|
||||||
|
setUploadCancelToken(cancelToken);
|
||||||
|
setUploading(true);
|
||||||
|
const response = await upload(images[0], {
|
||||||
|
onUploadProgress: setUploadProgress,
|
||||||
|
cancelToken: cancelToken.token
|
||||||
|
});
|
||||||
|
resetUploadingStates();
|
||||||
|
if (response.status === 200) {
|
||||||
|
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.SUCCESSFUL(), { variant: 'success' });
|
||||||
|
} else if (response.status === 201) {
|
||||||
|
setMd5(String(response.data));
|
||||||
|
enqueueSnackbar(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL(), { variant: 'success' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.ABORTED(), { variant: 'warning' });
|
||||||
|
} else {
|
||||||
|
resetUploadingStates();
|
||||||
|
enqueueSnackbar(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED()), { variant: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [uploadFile, cancelUpload, uploading, uploadProgress, md5] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFileUpload;
|
||||||
@@ -1,69 +1,71 @@
|
|||||||
import { useRequest } from 'alova';
|
import { FC, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useSnackbar } from 'notistack';
|
||||||
import { redirect } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { AuthenticationContext } from './context';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { Me } from 'types';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
|
||||||
import * as AuthenticationApi from 'api/authentication';
|
import * as AuthenticationApi from '../../api/authentication';
|
||||||
import { ACCESS_TOKEN } from 'api/endpoints';
|
import { ACCESS_TOKEN } from '../../api/endpoints';
|
||||||
import { LoadingSpinner } from 'components';
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { LoadingSpinner } from '../../components';
|
||||||
|
import { Me } from '../../types';
|
||||||
|
import { FeaturesContext } from '../features';
|
||||||
|
import { AuthenticationContext } from './context';
|
||||||
|
|
||||||
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
|
const { features } = useContext(FeaturesContext);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
const [initialized, setInitialized] = useState<boolean>(false);
|
const [initialized, setInitialized] = useState<boolean>(false);
|
||||||
const [me, setMe] = useState<Me>();
|
const [me, setMe] = useState<Me>();
|
||||||
|
|
||||||
const { send: verifyAuthorization } = useRequest(AuthenticationApi.verifyAuthorization(), {
|
|
||||||
immediate: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const signIn = (accessToken: string) => {
|
const signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
|
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||||
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
|
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
|
||||||
setMe(decodedMe);
|
setMe(decodedMe);
|
||||||
toast.success(LL.LOGGED_IN({ name: decodedMe.username }));
|
enqueueSnackbar(LL.LOGGED_IN({ name: decodedMe.username }), { variant: 'success' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
throw new Error('Failed to parse JWT');
|
throw new Error('Failed to parse JWT');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const signOut = (doRedirect: boolean) => {
|
const signOut = (redirect: boolean) => {
|
||||||
AuthenticationApi.clearAccessToken();
|
AuthenticationApi.clearAccessToken();
|
||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
if (doRedirect) {
|
if (redirect) {
|
||||||
// navigate('/');
|
navigate('/');
|
||||||
redirect('/');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
|
if (!features.security) {
|
||||||
|
setMe({ admin: true, username: 'admin' });
|
||||||
|
setInitialized(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
await verifyAuthorization()
|
try {
|
||||||
.then(() => {
|
await AuthenticationApi.verifyAuthorization();
|
||||||
setMe(AuthenticationApi.decodeMeJWT(accessToken));
|
setMe(AuthenticationApi.decodeMeJWT(accessToken));
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [features]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh();
|
refresh();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import type { Me } from 'types';
|
import { Me } from '../../types';
|
||||||
|
|
||||||
export interface AuthenticationContextValue {
|
export interface AuthenticationContextValue {
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
|
|||||||
47
interface/src/contexts/features/FeaturesLoader.tsx
Normal file
47
interface/src/contexts/features/FeaturesLoader.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import * as FeaturesApi from '../../api/features';
|
||||||
|
|
||||||
|
import { extractErrorMessage, RequiredChildrenProps } from '../../utils';
|
||||||
|
import { Features } from '../../types';
|
||||||
|
import { ApplicationError, LoadingSpinner } from '../../components';
|
||||||
|
|
||||||
|
import { FeaturesContext } from '.';
|
||||||
|
|
||||||
|
const FeaturesLoader: FC<RequiredChildrenProps> = (props) => {
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
const [features, setFeatures] = useState<Features>();
|
||||||
|
|
||||||
|
const loadFeatures = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await FeaturesApi.readFeatures();
|
||||||
|
setFeatures(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(extractErrorMessage(error, 'Failed to fetch application details.'));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFeatures();
|
||||||
|
}, [loadFeatures]);
|
||||||
|
|
||||||
|
if (features) {
|
||||||
|
return (
|
||||||
|
<FeaturesContext.Provider
|
||||||
|
value={{
|
||||||
|
features
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</FeaturesContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage) {
|
||||||
|
return <ApplicationError message={errorMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LoadingSpinner height="100vh" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeaturesLoader;
|
||||||
10
interface/src/contexts/features/context.ts
Normal file
10
interface/src/contexts/features/context.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
import { Features } from '../../types';
|
||||||
|
|
||||||
|
export interface FeaturesContextValue {
|
||||||
|
features: Features;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
|
||||||
|
export const FeaturesContext = createContext(FeaturesContextDefaultValue);
|
||||||
2
interface/src/contexts/features/index.ts
Normal file
2
interface/src/contexts/features/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './context';
|
||||||
|
export { default as FeaturesLoader } from './FeaturesLoader';
|
||||||
@@ -1,45 +1,32 @@
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import { FC, useState } from 'react';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
import { range } from 'lodash';
|
||||||
import { range } from 'lodash-es';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { APSettings } from 'types';
|
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||||
import * as APApi from 'api/ap';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
|
||||||
|
import { createAPSettingsValidator, validate } from '../../validators';
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField,
|
ValidatedTextField
|
||||||
BlockNavigation
|
} from '../../components';
|
||||||
} from 'components';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { APProvisionMode, APSettings } from '../../types';
|
||||||
import { APProvisionMode } from 'types';
|
import { numberValue, updateValue, useRest } from '../../utils';
|
||||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
import * as APApi from '../../api/ap';
|
||||||
|
|
||||||
import { createAPSettingsValidator, validate } from 'validators';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
export const isAPEnabled = ({ provision_mode }: APSettings) =>
|
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||||
|
};
|
||||||
|
|
||||||
const APSettingsForm: FC = () => {
|
const APSettingsForm: FC = () => {
|
||||||
const {
|
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<APSettings>({
|
||||||
loadData,
|
|
||||||
saving,
|
|
||||||
data,
|
|
||||||
updateDataValue,
|
|
||||||
origData,
|
|
||||||
dirtyFlags,
|
|
||||||
setDirtyFlags,
|
|
||||||
blocker,
|
|
||||||
saveData,
|
|
||||||
errorMessage
|
|
||||||
} = useRest<APSettings>({
|
|
||||||
read: APApi.readAPSettings,
|
read: APApi.readAPSettings,
|
||||||
update: APApi.updateAPSettings
|
update: APApi.updateAPSettings
|
||||||
});
|
});
|
||||||
@@ -48,7 +35,7 @@ const APSettingsForm: FC = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValue(setData);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -59,7 +46,7 @@ const APSettingsForm: FC = () => {
|
|||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(createAPSettingsValidator(data), data);
|
await validate(createAPSettingsValidator(data), data);
|
||||||
await saveData();
|
saveData();
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(errors);
|
||||||
}
|
}
|
||||||
@@ -176,37 +163,24 @@ const APSettingsForm: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<CancelIcon />}
|
startIcon={<SaveIcon />}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={loadData}
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<WarningIcon color="warning" />}
|
|
||||||
disabled={saving}
|
|
||||||
variant="contained"
|
|
||||||
color="info"
|
|
||||||
type="submit"
|
|
||||||
onClick={validateAndSubmit}
|
onClick={validateAndSubmit}
|
||||||
>
|
>
|
||||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
{LL.SAVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import ComputerIcon from '@mui/icons-material/Computer';
|
import { FC } from 'react';
|
||||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from '@mui/material';
|
||||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||||
import { useRequest } from 'alova';
|
import ComputerIcon from '@mui/icons-material/Computer';
|
||||||
import type { Theme } from '@mui/material';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { APStatus } from 'types';
|
import * as APApi from '../../api/ap';
|
||||||
import * as APApi from 'api/ap';
|
import { APNetworkStatus, APStatus } from '../../types';
|
||||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||||
|
import { useRest } from '../../utils';
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
import { APNetworkStatus } from 'types';
|
|
||||||
|
|
||||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -28,7 +27,7 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const APStatusForm: FC = () => {
|
const APStatusForm: FC = () => {
|
||||||
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
|
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ const APStatusForm: FC = () => {
|
|||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Tab } from '@mui/material';
|
import { FC, useContext } from 'react';
|
||||||
import { useContext } from 'react';
|
|
||||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||||
|
|
||||||
import APSettingsForm from './APSettingsForm';
|
import { Tab } from '@mui/material';
|
||||||
import APStatusForm from './APStatusForm';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
|
import APStatusForm from './APStatusForm';
|
||||||
|
import APSettingsForm from './APSettingsForm';
|
||||||
|
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
||||||
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const AccessPoint: FC = () => {
|
const AccessPoint: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
@@ -22,16 +22,11 @@ const AccessPoint: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RouterTabs value={routerTab}>
|
<RouterTabs value={routerTab}>
|
||||||
<Tab value="/ap/status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} />
|
<Tab value="status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} />
|
||||||
<Tab
|
<Tab value="settings" label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} disabled={!authenticatedContext.me.admin} />
|
||||||
value="/ap/settings"
|
|
||||||
label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))}
|
|
||||||
disabled={!authenticatedContext.me.admin}
|
|
||||||
/>
|
|
||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<APStatusForm />} />
|
<Route path="status" element={<APStatusForm />} />
|
||||||
<Route index element={<Navigate to="status" />} />
|
|
||||||
<Route
|
<Route
|
||||||
path="settings"
|
path="settings"
|
||||||
element={
|
element={
|
||||||
@@ -40,7 +35,7 @@ const AccessPoint: FC = () => {
|
|||||||
</RequireAdmin>
|
</RequireAdmin>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="*" element={<Navigate replace to="/ap/status" />} />
|
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Tab } from '@mui/material';
|
import React, { FC, useContext } from 'react';
|
||||||
import { useContext } from 'react';
|
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import MqttSettingsForm from './MqttSettingsForm';
|
|
||||||
|
import { Tab } from '@mui/material';
|
||||||
|
|
||||||
|
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
||||||
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
|
|
||||||
import MqttStatusForm from './MqttStatusForm';
|
import MqttStatusForm from './MqttStatusForm';
|
||||||
import type { FC } from 'react';
|
import MqttSettingsForm from './MqttSettingsForm';
|
||||||
|
|
||||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
|
|
||||||
const Mqtt: FC = () => {
|
const Mqtt: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
@@ -21,8 +22,8 @@ const Mqtt: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RouterTabs value={routerTab}>
|
<RouterTabs value={routerTab}>
|
||||||
<Tab value="/mqtt/status" label={LL.STATUS_OF('MQTT')} />
|
<Tab value="status" label={LL.STATUS_OF('MQTT')} />
|
||||||
<Tab value="/mqtt/settings" label={LL.SETTINGS_OF('MQTT')} disabled={!authenticatedContext.me.admin} />
|
<Tab value="settings" label={LL.SETTINGS_OF('MQTT')} disabled={!authenticatedContext.me.admin} />
|
||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<MqttStatusForm />} />
|
<Route path="status" element={<MqttStatusForm />} />
|
||||||
@@ -34,7 +35,7 @@ const Mqtt: FC = () => {
|
|||||||
</RequireAdmin>
|
</RequireAdmin>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="*" element={<Navigate replace to="/mqtt/status" />} />
|
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,39 +1,26 @@
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import { FC, useState } from 'react';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment, TextField } from '@mui/material';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { MqttSettings } from 'types';
|
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
|
||||||
import * as MqttApi from 'api/mqtt';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
|
||||||
|
import { createMqttSettingsValidator, validate } from '../../validators';
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField,
|
ValidatedTextField
|
||||||
BlockNavigation
|
} from '../../components';
|
||||||
} from 'components';
|
import { MqttSettings } from '../../types';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { numberValue, updateValue, useRest } from '../../utils';
|
||||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
import * as MqttApi from '../../api/mqtt';
|
||||||
|
|
||||||
import { createMqttSettingsValidator, validate } from 'validators';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const MqttSettingsForm: FC = () => {
|
const MqttSettingsForm: FC = () => {
|
||||||
const {
|
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<MqttSettings>({
|
||||||
loadData,
|
|
||||||
saving,
|
|
||||||
data,
|
|
||||||
updateDataValue,
|
|
||||||
origData,
|
|
||||||
dirtyFlags,
|
|
||||||
setDirtyFlags,
|
|
||||||
blocker,
|
|
||||||
saveData,
|
|
||||||
errorMessage
|
|
||||||
} = useRest<MqttSettings>({
|
|
||||||
read: MqttApi.readMqttSettings,
|
read: MqttApi.readMqttSettings,
|
||||||
update: MqttApi.updateMqttSettings
|
update: MqttApi.updateMqttSettings
|
||||||
});
|
});
|
||||||
@@ -42,7 +29,7 @@ const MqttSettingsForm: FC = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValue(setData);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -53,7 +40,7 @@ const MqttSettingsForm: FC = () => {
|
|||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(createMqttSettingsValidator(data), data);
|
await validate(createMqttSettingsValidator(data), data);
|
||||||
await saveData();
|
saveData();
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(errors);
|
||||||
}
|
}
|
||||||
@@ -66,20 +53,19 @@ const MqttSettingsForm: FC = () => {
|
|||||||
label={LL.ENABLE_MQTT()}
|
label={LL.ENABLE_MQTT()}
|
||||||
/>
|
/>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={6}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="host"
|
name="host"
|
||||||
label={LL.ADDRESS_OF(LL.BROKER())}
|
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.host}
|
value={data.host}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={6}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="port"
|
name="port"
|
||||||
@@ -92,7 +78,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
</Grid>
|
||||||
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
|
<Grid item xs={6}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="base"
|
name="base"
|
||||||
@@ -104,8 +92,8 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={6}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
name="client_id"
|
name="client_id"
|
||||||
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
|
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -115,8 +103,10 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
</Grid>
|
||||||
<TextField
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<ValidatedTextField
|
||||||
name="username"
|
name="username"
|
||||||
label={LL.USERNAME(0)}
|
label={LL.USERNAME(0)}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -126,7 +116,7 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={6}>
|
||||||
<ValidatedPasswordField
|
<ValidatedPasswordField
|
||||||
name="password"
|
name="password"
|
||||||
label={LL.PASSWORD()}
|
label={LL.PASSWORD()}
|
||||||
@@ -137,7 +127,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
</Grid>
|
||||||
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
|
<Grid item xs={6}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="keep_alive"
|
name="keep_alive"
|
||||||
@@ -153,8 +145,8 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={6}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
name="mqtt_qos"
|
name="mqtt_qos"
|
||||||
label="QoS"
|
label="QoS"
|
||||||
value={data.mqtt_qos}
|
value={data.mqtt_qos}
|
||||||
@@ -167,27 +159,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
<MenuItem value={0}>0</MenuItem>
|
<MenuItem value={0}>0</MenuItem>
|
||||||
<MenuItem value={1}>1</MenuItem>
|
<MenuItem value={1}>1</MenuItem>
|
||||||
<MenuItem value={2}>2</MenuItem>
|
<MenuItem value={2}>2</MenuItem>
|
||||||
</TextField>
|
</ValidatedTextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
{data.enableTLS !== undefined && (
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={<Checkbox name="enableTLS" checked={data.enableTLS} onChange={updateFormValue} />}
|
|
||||||
label={LL.ENABLE_TLS()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{data.enableTLS === true && (
|
|
||||||
<ValidatedPasswordField
|
|
||||||
name="rootCA"
|
|
||||||
label={LL.CERT()}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.rootCA}
|
|
||||||
onChange={updateFormValue}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />}
|
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />}
|
||||||
label={LL.MQTT_CLEAN_SESSION()}
|
label={LL.MQTT_CLEAN_SESSION()}
|
||||||
@@ -200,7 +174,7 @@ const MqttSettingsForm: FC = () => {
|
|||||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
{LL.FORMATTING()}
|
{LL.FORMATTING()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
name="nested_format"
|
name="nested_format"
|
||||||
label={LL.MQTT_FORMAT()}
|
label={LL.MQTT_FORMAT()}
|
||||||
value={data.nested_format}
|
value={data.nested_format}
|
||||||
@@ -212,20 +186,13 @@ const MqttSettingsForm: FC = () => {
|
|||||||
>
|
>
|
||||||
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
|
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
|
||||||
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
||||||
</TextField>
|
</ValidatedTextField>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
|
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
|
||||||
label={LL.MQTT_RESPONSE()}
|
label={LL.MQTT_RESPONSE()}
|
||||||
/>
|
/>
|
||||||
{!data.ha_enabled && (
|
{!data.ha_enabled && (
|
||||||
<Grid
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
container
|
|
||||||
rowSpacing={-1}
|
|
||||||
spacing={1}
|
|
||||||
direction="row"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
alignItems="flex-start"
|
|
||||||
>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
|
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
|
||||||
@@ -248,37 +215,16 @@ const MqttSettingsForm: FC = () => {
|
|||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
|
sx={{ pb: 1 }}
|
||||||
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
|
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
|
||||||
label={LL.MQTT_PUBLISH_TEXT_3()}
|
label={LL.MQTT_PUBLISH_TEXT_3()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{data.ha_enabled && (
|
{data.ha_enabled && (
|
||||||
<Grid
|
<>
|
||||||
container
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
sx={{ pl: 1 }}
|
<Grid item>
|
||||||
spacing={1}
|
<ValidatedTextField
|
||||||
direction="row"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
alignItems="flex-start"
|
|
||||||
>
|
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
|
||||||
<TextField
|
|
||||||
name="discovery_type"
|
|
||||||
label={LL.MQTT_PUBLISH_TEXT_5()}
|
|
||||||
value={data.discovery_type}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
onChange={updateFormValue}
|
|
||||||
margin="normal"
|
|
||||||
select
|
|
||||||
>
|
|
||||||
<MenuItem value={0}>Home Assistant</MenuItem>
|
|
||||||
<MenuItem value={1}>Domoticz</MenuItem>
|
|
||||||
<MenuItem value={2}>Domoticz (latest)</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
|
||||||
<TextField
|
|
||||||
name="discovery_prefix"
|
name="discovery_prefix"
|
||||||
label={LL.MQTT_PUBLISH_TEXT_4()}
|
label={LL.MQTT_PUBLISH_TEXT_4()}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -288,8 +234,8 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
name="entity_format"
|
name="entity_format"
|
||||||
label={LL.MQTT_ENTITY_FORMAT()}
|
label={LL.MQTT_ENTITY_FORMAT()}
|
||||||
value={data.entity_format}
|
value={data.entity_format}
|
||||||
@@ -302,9 +248,10 @@ const MqttSettingsForm: FC = () => {
|
|||||||
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
|
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
|
||||||
<MenuItem value={1}>{LL.MQTT_ENTITY_FORMAT_1()}</MenuItem>
|
<MenuItem value={1}>{LL.MQTT_ENTITY_FORMAT_1()}</MenuItem>
|
||||||
<MenuItem value={2}>{LL.MQTT_ENTITY_FORMAT_2()}</MenuItem>
|
<MenuItem value={2}>{LL.MQTT_ENTITY_FORMAT_2()}</MenuItem>
|
||||||
</TextField>
|
</ValidatedTextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
@@ -312,11 +259,11 @@ const MqttSettingsForm: FC = () => {
|
|||||||
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={6} sm={4}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_heartbeat"
|
name="publish_time_heartbeat"
|
||||||
label="Heartbeat"
|
label={LL.MQTT_INT_HEARTBEAT()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
}}
|
}}
|
||||||
@@ -328,8 +275,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={6} sm={4}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_boiler"
|
name="publish_time_boiler"
|
||||||
label={LL.MQTT_INT_BOILER()}
|
label={LL.MQTT_INT_BOILER()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@@ -343,8 +291,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={6} sm={4}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_thermostat"
|
name="publish_time_thermostat"
|
||||||
label={LL.MQTT_INT_THERMOSTATS()}
|
label={LL.MQTT_INT_THERMOSTATS()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@@ -358,8 +307,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={6} sm={4}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_solar"
|
name="publish_time_solar"
|
||||||
label={LL.MQTT_INT_SOLAR()}
|
label={LL.MQTT_INT_SOLAR()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@@ -373,8 +323,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={6} sm={4}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_mixer"
|
name="publish_time_mixer"
|
||||||
label={LL.MQTT_INT_MIXER()}
|
label={LL.MQTT_INT_MIXER()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@@ -388,8 +339,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={6} sm={4}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_sensor"
|
name="publish_time_sensor"
|
||||||
label={LL.TEMP_SENSORS()}
|
label={LL.TEMP_SENSORS()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@@ -403,8 +355,9 @@ const MqttSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={6} sm={4}>
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_other"
|
name="publish_time_other"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
@@ -419,38 +372,24 @@ const MqttSettingsForm: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<CancelIcon />}
|
startIcon={<SaveIcon />}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={loadData}
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<WarningIcon color="warning" />}
|
|
||||||
disabled={saving}
|
|
||||||
variant="contained"
|
|
||||||
color="info"
|
|
||||||
type="submit"
|
|
||||||
onClick={validateAndSubmit}
|
onClick={validateAndSubmit}
|
||||||
>
|
>
|
||||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
{LL.SAVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
import { FC } from 'react';
|
||||||
|
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from '@mui/material';
|
||||||
|
|
||||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import ReportIcon from '@mui/icons-material/Report';
|
import ReportIcon from '@mui/icons-material/Report';
|
||||||
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
||||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||||
import { useRequest } from 'alova';
|
|
||||||
import type { Theme } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { MqttStatus } from 'types';
|
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||||
import * as MqttApi from 'api/mqtt';
|
import { MqttStatus, MqttDisconnectReason } from '../../types';
|
||||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
import * as MqttApi from '../../api/mqtt';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useRest } from '../../utils';
|
||||||
import { MqttDisconnectReason } from 'types';
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
@@ -26,6 +26,7 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
|
|||||||
|
|
||||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
||||||
if (mqtt_fails === 0) return theme.palette.success.main;
|
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||||
|
|
||||||
if (mqtt_fails < 10) return theme.palette.warning.main;
|
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||||
|
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
@@ -38,7 +39,7 @@ export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatus, theme: Theme) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MqttStatusForm: FC = () => {
|
const MqttStatusForm: FC = () => {
|
||||||
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
|
const { loadData, data, errorMessage } = useRest<MqttStatus>({ read: MqttApi.readMqttStatus });
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
@@ -68,8 +69,10 @@ const MqttStatusForm: FC = () => {
|
|||||||
return 'Malformed credentials';
|
return 'Malformed credentials';
|
||||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||||
return 'Not authorized';
|
return 'Not authorized';
|
||||||
|
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||||
|
return 'Device out of memory';
|
||||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||||
return 'TLS fingerprint invalid';
|
return 'Server fingerprint invalid';
|
||||||
default:
|
default:
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
@@ -77,10 +80,11 @@ const MqttStatusForm: FC = () => {
|
|||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderConnectionStatus = () => (
|
const renderConnectionStatus = () => {
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
{!data.connected && (
|
{!data.connected && (
|
||||||
<>
|
<>
|
||||||
@@ -122,6 +126,7 @@ const MqttStatusForm: FC = () => {
|
|||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { Tab } from '@mui/material';
|
import React, { FC, useCallback, useContext, useState } from 'react';
|
||||||
import { useCallback, useContext, useState } from 'react';
|
|
||||||
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
|
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
|
||||||
import NetworkSettingsForm from './NetworkSettingsForm';
|
|
||||||
import NetworkStatusForm from './NetworkStatusForm';
|
|
||||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
|
||||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { WiFiNetwork } from 'types';
|
import { Tab } from '@mui/material';
|
||||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { WiFiNetwork } from '../../types';
|
||||||
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
|
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||||
|
import NetworkStatusForm from './NetworkStatusForm';
|
||||||
|
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||||
|
import NetworkSettingsForm from './NetworkSettingsForm';
|
||||||
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const NetworkConnection: FC = () => {
|
const NetworkConnection: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
@@ -44,13 +45,9 @@ const NetworkConnection: FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RouterTabs value={routerTab}>
|
<RouterTabs value={routerTab}>
|
||||||
<Tab value="/network/status" label={LL.STATUS_OF(LL.NETWORK(1))} />
|
<Tab value="status" label={LL.STATUS_OF(LL.NETWORK(1))} />
|
||||||
<Tab value="/network/scan" label={LL.NETWORK_SCAN()} disabled={!authenticatedContext.me.admin} />
|
<Tab value="scan" label={LL.NETWORK_SCAN()} disabled={!authenticatedContext.me.admin} />
|
||||||
<Tab
|
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} disabled={!authenticatedContext.me.admin} />
|
||||||
value="/network/settings"
|
|
||||||
label={LL.SETTINGS_OF(LL.NETWORK(1))}
|
|
||||||
disabled={!authenticatedContext.me.admin}
|
|
||||||
/>
|
|
||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<NetworkStatusForm />} />
|
<Route path="status" element={<NetworkStatusForm />} />
|
||||||
@@ -70,7 +67,7 @@ const NetworkConnection: FC = () => {
|
|||||||
</RequireAdmin>
|
</RequireAdmin>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="*" element={<Navigate replace to="/network/status" />} />
|
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</WiFiConnectionContext.Provider>
|
</WiFiConnectionContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import { FC, useContext, useEffect, useState } from 'react';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import { useSnackbar } from 'notistack';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
|
||||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
|
||||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
@@ -15,22 +12,15 @@ import {
|
|||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Typography,
|
Typography,
|
||||||
TextField,
|
InputAdornment
|
||||||
MenuItem
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
// eslint-disable-next-line import/named
|
|
||||||
import { updateState, useRequest } from 'alova';
|
|
||||||
import { useContext, useEffect, useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import RestartMonitor from '../system/RestartMonitor';
|
|
||||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
|
||||||
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { NetworkSettings } from 'types';
|
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||||
import * as NetworkApi from 'api/network';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import * as SystemApi from 'api/system';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
@@ -38,68 +28,57 @@ import {
|
|||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField,
|
ValidatedTextField,
|
||||||
MessageBox,
|
MessageBox
|
||||||
BlockNavigation
|
} from '../../components';
|
||||||
} from 'components';
|
import { NetworkSettings } from '../../types';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import * as NetworkApi from '../../api/network';
|
||||||
|
import { numberValue, updateValue, useRest } from '../../utils';
|
||||||
|
import * as EMSESP from '../../project/api';
|
||||||
|
|
||||||
import { updateValueDirty, useRest } from 'utils';
|
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||||
|
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||||
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
|
import { validate } from '../../validators';
|
||||||
|
import { createNetworkSettingsValidator } from '../../validators/network';
|
||||||
|
|
||||||
import { validate } from 'validators';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
import { createNetworkSettingsValidator } from 'validators/network';
|
import RestartMonitor from '../system/RestartMonitor';
|
||||||
|
|
||||||
const WiFiSettingsForm: FC = () => {
|
const WiFiSettingsForm: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
||||||
|
|
||||||
const [initialized, setInitialized] = useState(false);
|
const [initialized, setInitialized] = useState(false);
|
||||||
const [restarting, setRestarting] = useState(false);
|
const [restarting, setRestarting] = useState(false);
|
||||||
|
const { loadData, saving, data, setData, saveData, errorMessage, restartNeeded } = useRest<NetworkSettings>({
|
||||||
const {
|
|
||||||
loadData,
|
|
||||||
saving,
|
|
||||||
data,
|
|
||||||
updateDataValue,
|
|
||||||
origData,
|
|
||||||
dirtyFlags,
|
|
||||||
setDirtyFlags,
|
|
||||||
blocker,
|
|
||||||
saveData,
|
|
||||||
errorMessage,
|
|
||||||
restartNeeded
|
|
||||||
} = useRest<NetworkSettings>({
|
|
||||||
read: NetworkApi.readNetworkSettings,
|
read: NetworkApi.readNetworkSettings,
|
||||||
update: NetworkApi.updateNetworkSettings
|
update: NetworkApi.updateNetworkSettings
|
||||||
});
|
});
|
||||||
|
|
||||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
|
||||||
immediate: false
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialized && data) {
|
if (!initialized && data) {
|
||||||
if (selectedNetwork) {
|
if (selectedNetwork) {
|
||||||
updateState('networkSettings', (current_data) => ({
|
setData({
|
||||||
ssid: selectedNetwork.ssid,
|
ssid: selectedNetwork.ssid,
|
||||||
bssid: selectedNetwork.bssid,
|
password: '',
|
||||||
password: current_data ? current_data.password : '',
|
hostname: data?.hostname,
|
||||||
hostname: current_data?.hostname,
|
|
||||||
static_ip_config: false,
|
static_ip_config: false,
|
||||||
enableIPv6: false,
|
enableIPv6: false,
|
||||||
bandwidth20: false,
|
bandwidth20: false,
|
||||||
tx_power: 0,
|
tx_power: 20,
|
||||||
nosleep: false,
|
nosleep: false,
|
||||||
enableMDNS: true,
|
enableMDNS: true,
|
||||||
enableCORS: false,
|
enableCORS: false,
|
||||||
CORSOrigin: '*'
|
CORSOrigin: '*'
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
}
|
}
|
||||||
}, [initialized, setInitialized, data, selectedNetwork]);
|
}, [initialized, setInitialized, data, setData, selectedNetwork]);
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValue(setData);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -114,23 +93,19 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(createNetworkSettingsValidator(data), data);
|
await validate(createNetworkSettingsValidator(data), data);
|
||||||
await saveData();
|
saveData();
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(errors);
|
||||||
}
|
}
|
||||||
deselectNetwork();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCancel = async () => {
|
|
||||||
deselectNetwork();
|
|
||||||
await loadData();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const restart = async () => {
|
const restart = async () => {
|
||||||
await restartCommand().catch((error) => {
|
try {
|
||||||
toast.error(error.message);
|
await EMSESP.restart();
|
||||||
});
|
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -146,17 +121,10 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={selectedNetwork.ssid}
|
primary={selectedNetwork.ssid}
|
||||||
secondary={
|
secondary={'Security: ' + networkSecurityMode(selectedNetwork) + ', Ch: ' + selectedNetwork.channel}
|
||||||
'Security: ' +
|
|
||||||
networkSecurityMode(selectedNetwork) +
|
|
||||||
', Ch: ' +
|
|
||||||
selectedNetwork.channel +
|
|
||||||
', bssid: ' +
|
|
||||||
selectedNetwork.bssid
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton onClick={setCancel}>
|
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
@@ -174,16 +142,6 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ValidatedTextField
|
|
||||||
fieldErrors={fieldErrors}
|
|
||||||
name="bssid"
|
|
||||||
label={'BSSID (' + LL.NETWORK_BLANK_BSSID() + ')'}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.bssid}
|
|
||||||
onChange={updateFormValue}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||||
<ValidatedPasswordField
|
<ValidatedPasswordField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
@@ -196,40 +154,36 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TextField
|
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="tx_power"
|
name="tx_power"
|
||||||
label={LL.TX_POWER()}
|
label={LL.TX_POWER()}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: <InputAdornment position="end">dBm</InputAdornment>
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.tx_power}
|
value={numberValue(data.tx_power)}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
|
type="number"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
select
|
/>
|
||||||
>
|
|
||||||
<MenuItem value={0}>Auto</MenuItem>
|
|
||||||
<MenuItem value={78}>19.5 dBm</MenuItem>
|
|
||||||
<MenuItem value={76}>19 dBm</MenuItem>
|
|
||||||
<MenuItem value={74}>18.5 dBm</MenuItem>
|
|
||||||
<MenuItem value={68}>17 dBm</MenuItem>
|
|
||||||
<MenuItem value={60}>15 dBm</MenuItem>
|
|
||||||
<MenuItem value={52}>13 dBm</MenuItem>
|
|
||||||
<MenuItem value={44}>11 dBm</MenuItem>
|
|
||||||
<MenuItem value={34}>8.5 dBm</MenuItem>
|
|
||||||
<MenuItem value={28}>7 dBm</MenuItem>
|
|
||||||
<MenuItem value={20}>5 dBm</MenuItem>
|
|
||||||
<MenuItem value={8}>2 dBm</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
|
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
|
||||||
label={LL.NETWORK_DISABLE_SLEEP()}
|
label={LL.NETWORK_DISABLE_SLEEP()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
|
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
|
||||||
label={LL.NETWORK_LOW_BAND()}
|
label={LL.NETWORK_LOW_BAND()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
{LL.GENERAL_OPTIONS()}
|
{LL.GENERAL_OPTIONS()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="hostname"
|
name="hostname"
|
||||||
@@ -240,16 +194,19 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
||||||
label={LL.NETWORK_USE_DNS()}
|
label={LL.NETWORK_USE_DNS()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
|
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
|
||||||
label={LL.NETWORK_ENABLE_CORS()}
|
label={LL.NETWORK_ENABLE_CORS()}
|
||||||
/>
|
/>
|
||||||
{data.enableCORS && (
|
{data.enableCORS && (
|
||||||
<TextField
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
name="CORSOrigin"
|
name="CORSOrigin"
|
||||||
label={LL.NETWORK_CORS_ORIGIN()}
|
label={LL.NETWORK_CORS_ORIGIN()}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -259,12 +216,12 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{data.enableIPv6 !== undefined && (
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
|
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
|
||||||
label={LL.NETWORK_ENABLE_IPV6()}
|
label={LL.NETWORK_ENABLE_IPV6()}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />}
|
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />}
|
||||||
label={LL.NETWORK_FIXED_IP()}
|
label={LL.NETWORK_FIXED_IP()}
|
||||||
@@ -324,34 +281,23 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{restartNeeded && (
|
{restartNeeded && (
|
||||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||||
{LL.RESTART()}
|
{LL.RESTART()}
|
||||||
</Button>
|
</Button>
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
)}
|
)}
|
||||||
|
{!restartNeeded && (
|
||||||
{!restartNeeded && (selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<CancelIcon />}
|
startIcon={<SaveIcon />}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={loadData}
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<WarningIcon color="warning" />}
|
|
||||||
disabled={saving}
|
|
||||||
variant="contained"
|
|
||||||
color="info"
|
|
||||||
type="submit"
|
|
||||||
onClick={validateAndSubmit}
|
onClick={validateAndSubmit}
|
||||||
>
|
>
|
||||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
{LL.SAVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
)}
|
||||||
@@ -361,7 +307,6 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
|
||||||
{restarting ? <RestartMonitor /> : content()}
|
{restarting ? <RestartMonitor /> : content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from '@mui/material';
|
||||||
|
|
||||||
|
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
|
||||||
|
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||||
|
import WifiIcon from '@mui/icons-material/Wifi';
|
||||||
import DnsIcon from '@mui/icons-material/Dns';
|
import DnsIcon from '@mui/icons-material/Dns';
|
||||||
import GiteIcon from '@mui/icons-material/Gite';
|
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import RouterIcon from '@mui/icons-material/Router';
|
import RouterIcon from '@mui/icons-material/Router';
|
||||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
|
||||||
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
|
|
||||||
import WifiIcon from '@mui/icons-material/Wifi';
|
|
||||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
|
||||||
import { useRequest } from 'alova';
|
|
||||||
import type { Theme } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { NetworkStatus } from 'types';
|
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||||
import * as NetworkApi from 'api/network';
|
import { NetworkConnectionStatus, NetworkStatus } from '../../types';
|
||||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
import * as NetworkApi from '../../api/network';
|
||||||
|
import { useRest } from '../../utils';
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
import { NetworkConnectionStatus } from 'types';
|
|
||||||
|
|
||||||
const isConnected = ({ status }: NetworkStatus) =>
|
const isConnected = ({ status }: NetworkStatus) =>
|
||||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||||
@@ -39,15 +37,6 @@ const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const networkQualityHighlight = ({ rssi }: NetworkStatus, theme: Theme) => {
|
|
||||||
if (rssi <= -85) {
|
|
||||||
return theme.palette.error.main;
|
|
||||||
} else if (rssi <= -75) {
|
|
||||||
return theme.palette.warning.main;
|
|
||||||
}
|
|
||||||
return theme.palette.success.main;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isWiFi = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
export const isWiFi = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||||
export const isEthernet = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
export const isEthernet = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||||
|
|
||||||
@@ -69,7 +58,7 @@ const IPs = (status: NetworkStatus) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NetworkStatusForm: FC = () => {
|
const NetworkStatusForm: FC = () => {
|
||||||
const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus);
|
const { loadData, data, errorMessage } = useRest<NetworkStatus>({ read: NetworkApi.readNetworkStatus });
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
@@ -88,7 +77,7 @@ const NetworkStatusForm: FC = () => {
|
|||||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||||
return LL.CONNECTED(1) + ' ' + LL.FAILED(0);
|
return LL.CONNECTED(1) + ' ' + LL.FAILED();
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||||
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
@@ -100,7 +89,7 @@ const NetworkStatusForm: FC = () => {
|
|||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -116,24 +105,15 @@ const NetworkStatusForm: FC = () => {
|
|||||||
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar sx={{ bgcolor: networkStatusHighlight(data, theme) }}>
|
|
||||||
<GiteIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="Hostname" secondary={data.hostname} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
{isWiFi(data) && (
|
{isWiFi(data) && (
|
||||||
<>
|
<>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar sx={{ bgcolor: networkQualityHighlight(data, theme) }}>
|
<Avatar>
|
||||||
<SettingsInputAntennaIcon />
|
<SettingsInputAntennaIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="SSID (RSSI)" secondary={data.ssid + ' (' + data.rssi + ' dBm)'} />
|
<ListItemText primary="SSID" secondary={data.ssid} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import type { WiFiNetwork } from 'types';
|
import { WiFiNetwork } from '../../types';
|
||||||
|
|
||||||
export interface WiFiConnectionContextValue {
|
export interface WiFiConnectionContextValue {
|
||||||
selectedNetwork?: WiFiNetwork;
|
selectedNetwork?: WiFiNetwork;
|
||||||
|
|||||||
@@ -1,52 +1,88 @@
|
|||||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
import { useEffect, FC, useState, useCallback, useRef } from 'react';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
// eslint-disable-next-line import/named
|
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||||
import { updateState, useRequest } from 'alova';
|
|
||||||
import { useState, useRef } from 'react';
|
import * as NetworkApi from '../../api/network';
|
||||||
|
import { WiFiNetwork, WiFiNetworkList } from '../../types';
|
||||||
|
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||||
|
|
||||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||||
import type { FC } from 'react';
|
|
||||||
import * as NetworkApi from 'api/network';
|
|
||||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const NUM_POLLS = 10;
|
const NUM_POLLS = 10;
|
||||||
const POLLING_FREQUENCY = 1000;
|
const POLLING_FREQUENCY = 500;
|
||||||
|
|
||||||
|
const compareNetworks = (network1: WiFiNetwork, network2: WiFiNetwork) => {
|
||||||
|
if (network1.rssi < network2.rssi) return 1;
|
||||||
|
if (network1.rssi > network2.rssi) return -1;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
const WiFiNetworkScanner: FC = () => {
|
const WiFiNetworkScanner: FC = () => {
|
||||||
const pollCount = useRef(0);
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const pollCount = useRef(0);
|
||||||
|
const [networkList, setNetworkList] = useState<WiFiNetworkList>();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
|
||||||
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(NetworkApi.scanNetworks); // is called on page load to start network scan
|
const finishedWithError = useCallback(
|
||||||
const {
|
(message: string) => {
|
||||||
data: networkList,
|
enqueueSnackbar(message, { variant: 'error' });
|
||||||
send: getNetworkList,
|
setNetworkList(undefined);
|
||||||
onSuccess: onSuccessNetworkList
|
setErrorMessage(message);
|
||||||
} = useRequest(NetworkApi.listNetworks, {
|
},
|
||||||
immediate: false
|
[enqueueSnackbar]
|
||||||
});
|
);
|
||||||
|
|
||||||
onSuccessNetworkList((event) => {
|
const pollNetworkList = useCallback(async () => {
|
||||||
if (!event.data) {
|
try {
|
||||||
|
const response = await NetworkApi.listNetworks();
|
||||||
|
if (response.status === 202) {
|
||||||
const completedPollCount = pollCount.current + 1;
|
const completedPollCount = pollCount.current + 1;
|
||||||
if (completedPollCount < NUM_POLLS) {
|
if (completedPollCount < NUM_POLLS) {
|
||||||
pollCount.current = completedPollCount;
|
pollCount.current = completedPollCount;
|
||||||
setTimeout(getNetworkList, POLLING_FREQUENCY);
|
setTimeout(pollNetworkList, POLLING_FREQUENCY);
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage(LL.PROBLEM_LOADING());
|
finishedWithError(LL.PROBLEM_LOADING());
|
||||||
pollCount.current = 0;
|
}
|
||||||
|
} else {
|
||||||
|
const newNetworkList = response.data;
|
||||||
|
newNetworkList.networks.sort(compareNetworks);
|
||||||
|
setNetworkList(newNetworkList);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
|
||||||
|
} else {
|
||||||
|
finishedWithError(LL.PROBLEM_LOADING());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, [finishedWithError, LL]);
|
||||||
|
|
||||||
onCompleteScanNetworks(() => {
|
const startNetworkScan = useCallback(async () => {
|
||||||
pollCount.current = 0;
|
pollCount.current = 0;
|
||||||
|
setNetworkList(undefined);
|
||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
updateState('listNetworks', () => undefined);
|
try {
|
||||||
void getNetworkList();
|
await NetworkApi.scanNetworks();
|
||||||
});
|
setTimeout(pollNetworkList, POLLING_FREQUENCY);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
|
||||||
|
} else {
|
||||||
|
finishedWithError(LL.PROBLEM_LOADING());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [finishedWithError, pollNetworkList, LL]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
startNetworkScan();
|
||||||
|
}, [startNetworkScan]);
|
||||||
|
|
||||||
const renderNetworkScanner = () => {
|
const renderNetworkScanner = () => {
|
||||||
if (!networkList) {
|
if (!networkList) {
|
||||||
@@ -63,7 +99,7 @@ const WiFiNetworkScanner: FC = () => {
|
|||||||
startIcon={<PermScanWifiIcon />}
|
startIcon={<PermScanWifiIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={scanNetworks}
|
onClick={startNetworkScan}
|
||||||
disabled={!errorMessage && !networkList}
|
disabled={!errorMessage && !networkList}
|
||||||
>
|
>
|
||||||
{LL.SCAN_AGAIN()}…
|
{LL.SCAN_AGAIN()}…
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import LockIcon from '@mui/icons-material/Lock';
|
import { FC, useContext } from 'react';
|
||||||
|
|
||||||
|
import { Avatar, Badge, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText } from '@mui/material';
|
||||||
|
|
||||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import WifiIcon from '@mui/icons-material/Wifi';
|
import WifiIcon from '@mui/icons-material/Wifi';
|
||||||
import { Avatar, Badge, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText, useTheme } from '@mui/material';
|
|
||||||
import { useContext } from 'react';
|
import { MessageBox } from '../../components';
|
||||||
|
|
||||||
|
import { WiFiEncryptionType, WiFiNetwork, WiFiNetworkList } from '../../types';
|
||||||
|
|
||||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||||
import type { Theme } from '@mui/material';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import type { WiFiNetwork, WiFiNetworkList } from 'types';
|
|
||||||
import { MessageBox } from 'components';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
import { WiFiEncryptionType } from 'types';
|
|
||||||
|
|
||||||
interface WiFiNetworkSelectorProps {
|
interface WiFiNetworkSelectorProps {
|
||||||
networkList: WiFiNetworkList;
|
networkList: WiFiNetworkList;
|
||||||
@@ -34,48 +35,34 @@ export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
|||||||
return 'WPA2 Enterprise';
|
return 'WPA2 Enterprise';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
||||||
return 'None';
|
return 'None';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA3_PSK:
|
|
||||||
return 'WPA3';
|
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_WPA3_PSK:
|
|
||||||
return 'WPA2/WPA3';
|
|
||||||
default:
|
default:
|
||||||
return 'Unknown: ' + encryption_type;
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const networkQualityHighlight = ({ rssi }: WiFiNetwork, theme: Theme) => {
|
|
||||||
if (rssi <= -85) {
|
|
||||||
return theme.palette.error.main;
|
|
||||||
} else if (rssi <= -75) {
|
|
||||||
return theme.palette.warning.main;
|
|
||||||
}
|
|
||||||
return theme.palette.success.main;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
|
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||||
|
|
||||||
const renderNetwork = (network: WiFiNetwork) => (
|
const renderNetwork = (network: WiFiNetwork) => {
|
||||||
<ListItem key={network.bssid} onClick={() => wifiConnectionContext.selectNetwork(network)}>
|
return (
|
||||||
|
<ListItem key={network.bssid} button onClick={() => wifiConnectionContext.selectNetwork(network)}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={network.ssid}
|
primary={network.ssid}
|
||||||
secondary={
|
secondary={'Security: ' + networkSecurityMode(network) + ', Ch: ' + network.channel}
|
||||||
'Security: ' + networkSecurityMode(network) + ', Ch: ' + network.channel + ', bssid: ' + network.bssid
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Badge badgeContent={network.rssi + 'dBm'}>
|
<Badge badgeContent={network.rssi + 'db'}>
|
||||||
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
|
<WifiIcon />
|
||||||
</Badge>
|
</Badge>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (networkList.networks.length === 0) {
|
if (networkList.networks.length === 0) {
|
||||||
return <MessageBox mt={2} mb={1} message={LL.NETWORK_NO_WIFI()} level="info" />;
|
return <MessageBox mt={2} mb={1} message={LL.NETWORK_NO_WIFI()} level="info" />;
|
||||||
|
|||||||
@@ -1,48 +1,28 @@
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import { FC, useState } from 'react';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
|
||||||
// eslint-disable-next-line import/named
|
|
||||||
import { updateState } from 'alova';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import type { NTPSettings } from 'types';
|
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||||
import * as NTPApi from 'api/ntp';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
import {
|
|
||||||
BlockFormControlLabel,
|
import { validate } from '../../validators';
|
||||||
ButtonRow,
|
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
|
||||||
FormLoader,
|
import { NTPSettings } from '../../types';
|
||||||
SectionContent,
|
import { updateValue, useRest } from '../../utils';
|
||||||
ValidatedTextField,
|
import * as NTPApi from '../../api/ntp';
|
||||||
BlockNavigation
|
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
||||||
} from 'components';
|
import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
import { updateValueDirty, useRest } from 'utils';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
import { validate } from 'validators';
|
|
||||||
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
|
||||||
|
|
||||||
const NTPSettingsForm: FC = () => {
|
const NTPSettingsForm: FC = () => {
|
||||||
const {
|
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NTPSettings>({
|
||||||
loadData,
|
|
||||||
saving,
|
|
||||||
data,
|
|
||||||
updateDataValue,
|
|
||||||
origData,
|
|
||||||
dirtyFlags,
|
|
||||||
setDirtyFlags,
|
|
||||||
blocker,
|
|
||||||
saveData,
|
|
||||||
errorMessage
|
|
||||||
} = useRest<NTPSettings>({
|
|
||||||
read: NTPApi.readNTPSettings,
|
read: NTPApi.readNTPSettings,
|
||||||
update: NTPApi.updateNTPSettings
|
update: NTPApi.updateNTPSettings
|
||||||
});
|
});
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValue(setData);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -55,20 +35,18 @@ const NTPSettingsForm: FC = () => {
|
|||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(NTP_SETTINGS_VALIDATOR, data);
|
await validate(NTP_SETTINGS_VALIDATOR, data);
|
||||||
await saveData();
|
saveData();
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(errors);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
updateFormValue(event);
|
setData({
|
||||||
|
...data,
|
||||||
updateState('ntpSettings', (settings) => ({
|
|
||||||
...settings,
|
|
||||||
tz_label: event.target.value,
|
tz_label: event.target.value,
|
||||||
tz_format: TIME_ZONES[event.target.value]
|
tz_format: TIME_ZONES[event.target.value]
|
||||||
}));
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,37 +79,24 @@ const NTPSettingsForm: FC = () => {
|
|||||||
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
|
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
|
||||||
{timeZoneSelectItems()}
|
{timeZoneSelectItems()}
|
||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<CancelIcon />}
|
startIcon={<SaveIcon />}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={loadData}
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<WarningIcon color="warning" />}
|
|
||||||
disabled={saving}
|
|
||||||
variant="contained"
|
|
||||||
color="info"
|
|
||||||
type="submit"
|
|
||||||
onClick={validateAndSubmit}
|
onClick={validateAndSubmit}
|
||||||
>
|
>
|
||||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
{LL.SAVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user