Merge pull request #2665 from proddy/dev

optimize WebUI and finally remove lint warnings
This commit is contained in:
Proddy
2025-10-22 22:37:47 +02:00
committed by GitHub
95 changed files with 7471 additions and 13324 deletions

View File

@@ -69,7 +69,7 @@ Format: `<type>(<scope>): <subject>`
## Example ## Example
``` ```text
feat: add hat wobble feat: add hat wobble
^--^ ^------------^ ^--^ ^------------^
| | | |
@@ -96,7 +96,7 @@ References:
## Contributor License Agreement (CLA) ## Contributor License Agreement (CLA)
``` ```text
By making a contribution to this project, I certify that: By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I (a) The contribution was created in whole or in part by me and I

View File

@@ -35,7 +35,7 @@
[![chat](https://img.shields.io/discord/816637840644505620.svg?style=flat-square&color=blueviolet)](https://discord.gg/3J3GgnzpyT) [![chat](https://img.shields.io/discord/816637840644505620.svg?style=flat-square&color=blueviolet)](https://discord.gg/3J3GgnzpyT)
[![GitHub stars](https://img.shields.io/github/stars/emsesp/EMS-ESP32.svg?style=social&label=Star)](https://github.com/emsesp/EMS-ESP32/stargazers) [![GitHub stars](https://img.shields.io/github/stars/emsesp/EMS-ESP32.svg?style=social&label=Star)](https://github.com/emsesp/EMS-ESP32/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/emsesp/EMS-ESP32.svg?style=social&label=Fork)](https://github.com/emsesp/EMS-ES32P/network) [![GitHub forks](https://img.shields.io/github/forks/emsesp/EMS-ESP32.svg?style=social&label=Fork)](https://github.com/emsesp/EMS-ESP32/network)
[![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/paypalme/prderbyshire/2) [![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/paypalme/prderbyshire/2)
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller to communicate with **EMS** (Energy Management System) compatible equipment from manufacturers such as Bosch, Buderus, Nefit, Junkers, Worcester, Sieger, elm.leblanc and iVT. **EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller to communicate with **EMS** (Energy Management System) compatible equipment from manufacturers such as Bosch, Buderus, Nefit, Junkers, Worcester, Sieger, elm.leblanc and iVT.

View File

@@ -33,6 +33,7 @@
"src/core/modbus_entity_parameters.hpp", "src/core/modbus_entity_parameters.hpp",
"sdkconfig.*", "sdkconfig.*",
"managed_components/**", "managed_components/**",
"pnpm-*.yaml" "pnpm-*.yaml",
"vite.config.ts"
] ]
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,231 +0,0 @@
telegram_type_id,name,is_fetched
0x04,UBAFactory,fetched
0x06,RCTime,
0x0A,EasyMonitor,fetched
0x10,UBAErrorMessage1,
0x11,UBAErrorMessage2,
0x12,RCErrorMessage,
0x13,RCErrorMessage2,
0x14,UBATotalUptime,fetched
0x15,UBAMaintenanceData,
0x16,UBAParameters,fetched
0x18,UBAMonitorFast,
0x19,UBAMonitorSlow,
0x1A,UBASetPoints,
0x1C,UBAMaintenanceStatus,
0x1E,HydrTemp,
0x23,JunkersSetMixer,fetched
0x27,UBASettingsWW,fetched
0x28,WeatherComp,fetched
0x2A,MC110Status,
0x2E,Meters,
0x33,UBAParameterWW,fetched
0x34,UBAMonitorWW,
0x35,UBAFlags,
0x37,WWSettings,fetched
0x38,WWTimer,fetched
0x39,WWCircTimer,fetched
0x3A,RC30WWSettings,fetched
0x3B,Energy,
0x3D,RC35Set,
0x3E,RC35Monitor,
0x3F,RC35Timer,
0x40,RC30Temp,
0x41,RC30Monitor,
0x42,RC35Timer2,
0x47,RC35Set,
0x48,RC35Monitor,
0x49,RC35Timer,
0x4C,RC35Timer2,
0x51,RC35Set,
0x52,RC35Monitor,
0x53,RC35Timer,
0x56,RC35Timer2,
0x5B,RC35Set,
0x5C,RC35Monitor,
0x5D,RC35Timer,
0x60,RC35Timer2,
0x96,SM10Config,fetched
0x97,SM10Monitor,
0x9C,WM10MonitorMessage,
0x9D,WM10SetMessage,
0xA2,RCError,
0xA3,RCOutdoorTemp,
0xA5,IBASettings,fetched
0xA7,RC30Set,
0xA9,RC30Vacation,fetched
0xAA,MMConfigMessage,fetched
0xAB,MMStatusMessage,
0xAC,MMSetMessage,
0xAF,RC20Remote,
0xB0,RC10Set,
0xB1,RC10Monitor,
0xBB,HybridSettings,fetched
0xBF,ErrorMessage,
0xC0,RCErrorMessage,
0xC2,UBAErrorMessage3,
0xC6,UBAErrorMessage3,
0xD1,UBAOutdoorTemp,
0xE3,UBAMonitorSlowPlus2,
0xE4,UBAMonitorFastPlus,
0xE5,UBAMonitorSlowPlus,
0xE6,UBAParametersPlus,fetched
0xE9,UBAMonitorWWPlus,
0xEA,UBAParameterWWPlus,fetched
0x0101,ISM1Set,fetched
0x0103,ISM1StatusMessage,fetched
0x0104,ISM2StatusMessage,
0x010C,IPMStatusMessage,
0x011E,JunkersDisp,fetched
0x012E,HPEnergy1,
0x013B,HPEnergy2,
0x0165,JunkersSet,
0x0166,JunkersSet,
0x0167,JunkersSet,
0x0168,JunkersSet,
0x016E,Absent,fetched
0x016F,JunkersMonitor,
0x0170,JunkersMonitor,
0x0171,JunkersMonitor,
0x0172,JunkersMonitor,
0x0179,JunkersSet,
0x017A,JunkersSet,
0x017B,JunkersSet,
0x017C,JunkersSet,
0x01D3,JunkersDhw,fetched
0x023A,RC300OutdoorTemp,fetched
0x023E,PVSettings,fetched
0x0240,RC300Settings,fetched
0x0241,RC300Settings,fetched
0x0267,RC300Floordry,
0x0269,RC300Holiday,fetched
0x0291,HPMode,fetched
0x0292,HPMode,fetched
0x0293,HPMode,fetched
0x0294,HPMode,fetched
0x029B,RC300Curves,
0x029C,RC300Curves,
0x029D,RC300Curves,
0x029E,RC300Curves,
0x029F,RC300Curves,
0x02A0,RC300Curves,
0x02A1,RC300Curves,
0x02A2,RC300Curves,
0x02A5,RC300Monitor,
0x02A6,RC300Monitor,
0x02A7,RC300Monitor,
0x02A8,RC300Monitor,
0x02A9,RC300Monitor,
0x02AA,RC300Monitor,
0x02AB,RC300Monitor,
0x02AC,RC300Monitor,
0x02AF,RC300Summer,
0x02B0,RC300Summer,
0x02B1,RC300Summer,
0x02B2,RC300Summer,
0x02B3,RC300Summer,
0x02B4,RC300Summer,
0x02B5,RC300Summer,
0x02B6,RC300Summer,
0x02B9,RC300Set,
0x02BA,RC300Set,
0x02BB,RC300Set,
0x02BC,RC300Set,
0x02BD,RC300Set,
0x02BE,RC300Set,
0x02BF,RC300Set,
0x02C0,RC300Set,
0x02CC,HPPressure,fetched
0x02CD,MMPLUSConfigMessage,fetched
0x02CE,RC300Set2,
0x02D0,RC300Set2,
0x02D2,RC300Set2,
0x02D6,HPPump2,fetched
0x02D7,MMPLUSStatusMessage,
0x02E0,UBASetPoints,
0x02F5,RC300WWmode,fetched
0x02F6,RC300WW2mode,fetched
0x0313,MMPLUSConfigMessage_WWC,fetched
0x031B,RC300WWtemp,fetched
0x031D,RC300WWmode2,
0x031E,RC300WWmode2,
0x0331,MMPLUSStatusMessage_WWC,
0x0358,SM100SystemConfig,fetched
0x035A,SM100CircuitConfig,fetched
0x035C,SM100HeatAssist,fetched
0x035D,SM100Circuit2Config,fetched
0x035F,SM100Config1,fetched
0x0361,SM100Differential,fetched
0x0362,SM100Monitor,
0x0363,SM100Monitor2,
0x0364,SM100Status,
0x0366,SM100Config,
0x036A,SM100Status2,
0x0380,SM100CollectorConfig,fetched
0x038E,SM100Energy,fetched
0x0391,SM100Time,fetched
0x043F,CRHolidays,fetched
0x0467,HPSet,
0x0468,HPSet,
0x0469,HPSet,
0x046A,HPSet,
0x0471,RC300Summer2,
0x0472,RC300Summer2,
0x0473,RC300Summer2,
0x0474,RC300Summer2,
0x0475,RC300Summer2,
0x0476,RC300Summer2,
0x0477,RC300Summer2,
0x0478,RC300Summer2,
0x047B,HP2,
0x0484,HPSilentMode,fetched
0x0485,HpCooling,fetched
0x0486,HpInConfig,fetched
0x0488,HPValve,fetched
0x048A,HpPool,fetched
0x048B,HPPumps,fetched
0x048D,HpPower,fetched
0x048F,HpTemperatures,
0x0491,HPAdditionalHeater,fetched
0x0492,HpHeaterConfig,fetched
0x0494,UBAEnergySupplied,
0x0495,UBAInformation,
0x0499,HPDhwSettings,fetched
0x049C,HPSettings2,fetched
0x049D,HPSettings3,fetched
0x04A2,HpInput,fetched
0x04A5,HPFan,fetched
0x04A7,HPPowerLimit,fetched
0x04AA,HPPower2,fetched
0x04AE,HPEnergy,fetched
0x04AF,HPMeters,fetched
0x055C,VentilationSet,fetched
0x056B,VentilationMode,fetched
0x0583,VentilationMonitor,
0x0585,Blowerspeed,
0x0587,Bypass,
0x05BA,HpPoolStatus,fetched
0x05D9,Airquality,
0x0772,HIUSettings,
0x0779,HIUMonitor,
0x07A5,SM100wwCirc,fetched
0x07A6,SM100wwParam,fetched
0x07AA,SM100wwStatus,
0x07AB,SM100wwCommand,
0x07AC,SM100wwParam1,
0x07AD,SM100ValveStatus,
0x07AE,SM100wwKeepWarm,fetched
0x07D6,SM100wwTemperature,
0x07E0,SM100wwStatus2,fetched
0x0935,EM100SetMessage,fetched
0x0936,EM100OutMessage,
0x0937,EM100TempMessage,
0x0938,EM100InputMessage,
0x0939,EM100MonitorMessage,
0x093A,EM100ConfigMessage,
0x0998,HPSettings,fetched
0x0999,HPFunctionTest,fetched
0x099A,HPStarts,
0x099B,HPFlowTemp,
0x099C,HPComp,
0x09A0,HPTemperature,
1 telegram_type_id name is_fetched
telegram_type_id name is_fetched
0x04 UBAFactory fetched
0x06 RCTime
0x0A EasyMonitor fetched
0x10 UBAErrorMessage1
0x11 UBAErrorMessage2
0x12 RCErrorMessage
0x13 RCErrorMessage2
0x14 UBATotalUptime fetched
0x15 UBAMaintenanceData
0x16 UBAParameters fetched
0x18 UBAMonitorFast
0x19 UBAMonitorSlow
0x1A UBASetPoints
0x1C UBAMaintenanceStatus
0x1E HydrTemp
0x23 JunkersSetMixer fetched
0x27 UBASettingsWW fetched
0x28 WeatherComp fetched
0x2A MC110Status
0x2E Meters
0x33 UBAParameterWW fetched
0x34 UBAMonitorWW
0x35 UBAFlags
0x37 WWSettings fetched
0x38 WWTimer fetched
0x39 WWCircTimer fetched
0x3A RC30WWSettings fetched
0x3B Energy
0x3D RC35Set
0x3E RC35Monitor
0x3F RC35Timer
0x40 RC30Temp
0x41 RC30Monitor
0x42 RC35Timer2
0x47 RC35Set
0x48 RC35Monitor
0x49 RC35Timer
0x4C RC35Timer2
0x51 RC35Set
0x52 RC35Monitor
0x53 RC35Timer
0x56 RC35Timer2
0x5B RC35Set
0x5C RC35Monitor
0x5D RC35Timer
0x60 RC35Timer2
0x96 SM10Config fetched
0x97 SM10Monitor
0x9C WM10MonitorMessage
0x9D WM10SetMessage
0xA2 RCError
0xA3 RCOutdoorTemp
0xA5 IBASettings fetched
0xA7 RC30Set
0xA9 RC30Vacation fetched
0xAA MMConfigMessage fetched
0xAB MMStatusMessage
0xAC MMSetMessage
0xAF RC20Remote
0xB0 RC10Set
0xB1 RC10Monitor
0xBB HybridSettings fetched
0xBF ErrorMessage
0xC0 RCErrorMessage
0xC2 UBAErrorMessage3
0xC6 UBAErrorMessage3
0xD1 UBAOutdoorTemp
0xE3 UBAMonitorSlowPlus2
0xE4 UBAMonitorFastPlus
0xE5 UBAMonitorSlowPlus
0xE6 UBAParametersPlus fetched
0xE9 UBAMonitorWWPlus
0xEA UBAParameterWWPlus fetched
0x0101 ISM1Set fetched
0x0103 ISM1StatusMessage fetched
0x0104 ISM2StatusMessage
0x010C IPMStatusMessage
0x011E JunkersDisp fetched
0x012E HPEnergy1
0x013B HPEnergy2
0x0165 JunkersSet
0x0166 JunkersSet
0x0167 JunkersSet
0x0168 JunkersSet
0x016E Absent fetched
0x016F JunkersMonitor
0x0170 JunkersMonitor
0x0171 JunkersMonitor
0x0172 JunkersMonitor
0x0179 JunkersSet
0x017A JunkersSet
0x017B JunkersSet
0x017C JunkersSet
0x01D3 JunkersDhw fetched
0x023A RC300OutdoorTemp fetched
0x023E PVSettings fetched
0x0240 RC300Settings fetched
0x0241 RC300Settings fetched
0x0267 RC300Floordry
0x0269 RC300Holiday fetched
0x0291 HPMode fetched
0x0292 HPMode fetched
0x0293 HPMode fetched
0x0294 HPMode fetched
0x029B RC300Curves
0x029C RC300Curves
0x029D RC300Curves
0x029E RC300Curves
0x029F RC300Curves
0x02A0 RC300Curves
0x02A1 RC300Curves
0x02A2 RC300Curves
0x02A5 RC300Monitor
0x02A6 RC300Monitor
0x02A7 RC300Monitor
0x02A8 RC300Monitor
0x02A9 RC300Monitor
0x02AA RC300Monitor
0x02AB RC300Monitor
0x02AC RC300Monitor
0x02AF RC300Summer
0x02B0 RC300Summer
0x02B1 RC300Summer
0x02B2 RC300Summer
0x02B3 RC300Summer
0x02B4 RC300Summer
0x02B5 RC300Summer
0x02B6 RC300Summer
0x02B9 RC300Set
0x02BA RC300Set
0x02BB RC300Set
0x02BC RC300Set
0x02BD RC300Set
0x02BE RC300Set
0x02BF RC300Set
0x02C0 RC300Set
0x02CC HPPressure fetched
0x02CD MMPLUSConfigMessage fetched
0x02CE RC300Set2
0x02D0 RC300Set2
0x02D2 RC300Set2
0x02D6 HPPump2 fetched
0x02D7 MMPLUSStatusMessage
0x02E0 UBASetPoints
0x02F5 RC300WWmode fetched
0x02F6 RC300WW2mode fetched
0x0313 MMPLUSConfigMessage_WWC fetched
0x031B RC300WWtemp fetched
0x031D RC300WWmode2
0x031E RC300WWmode2
0x0331 MMPLUSStatusMessage_WWC
0x0358 SM100SystemConfig fetched
0x035A SM100CircuitConfig fetched
0x035C SM100HeatAssist fetched
0x035D SM100Circuit2Config fetched
0x035F SM100Config1 fetched
0x0361 SM100Differential fetched
0x0362 SM100Monitor
0x0363 SM100Monitor2
0x0364 SM100Status
0x0366 SM100Config
0x036A SM100Status2
0x0380 SM100CollectorConfig fetched
0x038E SM100Energy fetched
0x0391 SM100Time fetched
0x043F CRHolidays fetched
0x0467 HPSet
0x0468 HPSet
0x0469 HPSet
0x046A HPSet
0x0471 RC300Summer2
0x0472 RC300Summer2
0x0473 RC300Summer2
0x0474 RC300Summer2
0x0475 RC300Summer2
0x0476 RC300Summer2
0x0477 RC300Summer2
0x0478 RC300Summer2
0x047B HP2
0x0484 HPSilentMode fetched
0x0485 HpCooling fetched
0x0486 HpInConfig fetched
0x0488 HPValve fetched
0x048A HpPool fetched
0x048B HPPumps fetched
0x048D HpPower fetched
0x048F HpTemperatures
0x0491 HPAdditionalHeater fetched
0x0492 HpHeaterConfig fetched
0x0494 UBAEnergySupplied
0x0495 UBAInformation
0x0499 HPDhwSettings fetched
0x049C HPSettings2 fetched
0x049D HPSettings3 fetched
0x04A2 HpInput fetched
0x04A5 HPFan fetched
0x04A7 HPPowerLimit fetched
0x04AA HPPower2 fetched
0x04AE HPEnergy fetched
0x04AF HPMeters fetched
0x055C VentilationSet fetched
0x056B VentilationMode fetched
0x0583 VentilationMonitor
0x0585 Blowerspeed
0x0587 Bypass
0x05BA HpPoolStatus fetched
0x05D9 Airquality
0x0772 HIUSettings
0x0779 HIUMonitor
0x07A5 SM100wwCirc fetched
0x07A6 SM100wwParam fetched
0x07AA SM100wwStatus
0x07AB SM100wwCommand
0x07AC SM100wwParam1
0x07AD SM100ValveStatus
0x07AE SM100wwKeepWarm fetched
0x07D6 SM100wwTemperature
0x07E0 SM100wwStatus2 fetched
0x0935 EM100SetMessage fetched
0x0936 EM100OutMessage
0x0937 EM100TempMessage
0x0938 EM100InputMessage
0x0939 EM100MonitorMessage
0x093A EM100ConfigMessage
0x0998 HPSettings fetched
0x0999 HPFunctionTest fetched
0x099A HPStarts
0x099B HPFlowTemp
0x099C HPComp
0x09A0 HPTemperature

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "EMS-ESP", "name": "EMS-ESP",
"version": "3.7.2", "version": "3.7.3",
"description": "EMS-ESP WebUI", "description": "EMS-ESP WebUI",
"homepage": "https://emsesp.org", "homepage": "https://emsesp.org",
"author": "proddy, emsesp.org", "author": "proddy, emsesp.org",
@@ -45,23 +45,23 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.28.4", "@babel/core": "^7.28.4",
"@eslint/js": "^9.37.0", "@eslint/js": "^9.38.0",
"@preact/compat": "^18.3.1", "@preact/compat": "^18.3.1",
"@preact/preset-vite": "^2.10.2", "@preact/preset-vite": "^2.10.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^24.8.1", "@types/node": "^24.9.1",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^9.37.0", "eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"rollup-plugin-visualizer": "^6.0.5", "rollup-plugin-visualizer": "^6.0.5",
"terser": "^5.44.0", "terser": "^5.44.0",
"typescript-eslint": "^8.46.1", "typescript-eslint": "^8.46.2",
"vite": "^7.1.10", "vite": "^7.1.11",
"vite-plugin-imagemin": "^0.6.1", "vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"packageManager": "pnpm@10.18.3" "packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8"
} }

521
interface/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -37,10 +37,9 @@ const AuthenticatedRouting = () => {
<Route path="/dashboard/*" element={<Dashboard />} /> <Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/devices/*" element={<Devices />} /> <Route path="/devices/*" element={<Devices />} />
<Route path="/sensors/*" element={<Sensors />} /> <Route path="/sensors/*" element={<Sensors />} />
<Route path="/status/*" element={<Status />} />
<Route path="/help/*" element={<Help />} /> <Route path="/help/*" element={<Help />} />
<Route path="/*" element={<Navigate to="/" />} />
<Route path="/status/*" element={<Status />} />
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} /> <Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
<Route path="/status/activity" element={<Activity />} /> <Route path="/status/activity" element={<Activity />} />
<Route path="/status/log" element={<SystemLog />} /> <Route path="/status/log" element={<SystemLog />} />
@@ -68,6 +67,8 @@ const AuthenticatedRouting = () => {
<Route path="/customentities" element={<CustomEntities />} /> <Route path="/customentities" element={<CustomEntities />} />
</> </>
)} )}
<Route path="/*" element={<Navigate to="/" />} />
</Routes> </Routes>
</Layout> </Layout>
); );

View File

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

View File

@@ -70,6 +70,7 @@ export const readDeviceEntities = (id: number) =>
alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities`, { alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities`, {
params: { id }, params: { id },
responseType: 'arraybuffer', responseType: 'arraybuffer',
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
transform(data) { transform(data) {
return (data as DeviceEntity[]).map((de: DeviceEntity) => ({ return (data as DeviceEntity[]).map((de: DeviceEntity) => ({
...de, ...de,
@@ -92,6 +93,7 @@ export const writeDeviceName = (data: { id: number; name: string }) =>
// SettingsScheduler // SettingsScheduler
export const readSchedule = () => export const readSchedule = () =>
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', { alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
transform(data) { transform(data) {
return (data as Schedule).schedule.map((si: ScheduleItem) => ({ return (data as Schedule).schedule.map((si: ScheduleItem) => ({
...si, ...si,
@@ -129,6 +131,7 @@ export const writeModules = (data: {
// CustomEntities // CustomEntities
export const readCustomEntities = () => export const readCustomEntities = () =>
alovaInstance.Get<EntityItem[]>('/rest/customEntities', { alovaInstance.Get<EntityItem[]>('/rest/customEntities', {
// @ts-expect-error - exactOptionalPropertyTypes compatibility issue
transform(data) { transform(data) {
return (data as Entities).entities.map((ei: EntityItem) => ({ return (data as Entities).entities.map((ei: EntityItem) => ({
...ei, ...ei,

View File

@@ -30,7 +30,7 @@ export const getDevVersion = () =>
cacheFor: 60 * 10 * 1000, cacheFor: 60 * 10 * 1000,
transform(response: { data: { name: string; published_at: string } }) { transform(response: { data: { name: string; published_at: string } }) {
return { return {
name: response.data.name.split(/\s+/).splice(-1)[0].substring(1), name: response.data.name.split(/\s+/).splice(-1)[0]?.substring(1) || '',
published_at: response.data.published_at published_at: response.data.published_at
}; };
} }

View File

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

View File

@@ -137,8 +137,8 @@ const CustomEntities = () => {
const saveEntities = async () => { const saveEntities = async () => {
await writeEntities({ await writeEntities({
entities: entities entities: entities
.filter((ei) => !ei.deleted) .filter((ei: EntityItem) => !ei.deleted)
.map((condensed_ei) => ({ .map((condensed_ei: EntityItem) => ({
id: condensed_ei.id, id: condensed_ei.id,
ram: condensed_ei.ram, ram: condensed_ei.ram,
name: condensed_ei.name, name: condensed_ei.name,
@@ -231,6 +231,7 @@ const CustomEntities = () => {
value_type: 0, value_type: 0,
writeable: false, writeable: false,
deleted: false, deleted: false,
hide: false,
value: '' value: ''
}); });
setDialogOpen(true); setDialogOpen(true);
@@ -251,15 +252,17 @@ const CustomEntities = () => {
const renderEntity = () => { const renderEntity = () => {
if (!entities) { if (!entities) {
return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />; return (
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
);
} }
return ( return (
<Table <Table
data={{ data={{
nodes: entities nodes: entities
.filter((ei) => !ei.deleted) .filter((ei: EntityItem) => !ei.deleted)
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name))
}} }}
theme={entity_theme} theme={entity_theme}
layout={{ custom: true }} layout={{ custom: true }}

View File

@@ -74,7 +74,10 @@ const CustomEntitiesDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => { const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -123,7 +126,7 @@ const CustomEntitiesDialog = ({
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid size={12}> <Grid size={12}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="name" name="name"
label={LL.NAME(0)} label={LL.NAME(0)}
value={editItem.name} value={editItem.name}
@@ -211,7 +214,7 @@ const CustomEntitiesDialog = ({
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="device_id" name="device_id"
label={LL.ID_OF(LL.DEVICE())} label={LL.ID_OF(LL.DEVICE())}
margin="normal" margin="normal"
@@ -231,7 +234,7 @@ const CustomEntitiesDialog = ({
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="type_id" name="type_id"
label={LL.ID_OF(LL.TYPE(1))} label={LL.ID_OF(LL.TYPE(1))}
margin="normal" margin="normal"
@@ -251,7 +254,7 @@ const CustomEntitiesDialog = ({
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="offset" name="offset"
label={LL.OFFSET()} label={LL.OFFSET()}
margin="normal" margin="normal"
@@ -343,7 +346,7 @@ const CustomEntitiesDialog = ({
editItem.device_id !== '0' && ( editItem.device_id !== '0' && (
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="factor" name="factor"
label={LL.BYTES()} label={LL.BYTES()}
value={numberValue(editItem.factor as number)} value={numberValue(editItem.factor as number)}
@@ -361,7 +364,7 @@ const CustomEntitiesDialog = ({
{editItem.value_type === DeviceValueType.BOOL && ( {editItem.value_type === DeviceValueType.BOOL && (
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="factor" name="factor"
label={LL.BITMASK()} label={LL.BITMASK()}
value={editItem.factor as string} value={editItem.factor as string}

View File

@@ -125,13 +125,22 @@ const Customizations = () => {
const setOriginalSettings = (data: DeviceEntity[]) => { const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities( setDeviceEntities(
data.map((de) => ({ data.map((de) => {
const result: DeviceEntity = {
...de, ...de,
o_m: de.m, o_m: de.m
o_cn: de.cn, };
o_mi: de.mi, if (de.cn !== undefined) {
o_ma: de.ma result.o_cn = de.cn;
})) }
if (de.mi !== undefined) {
result.o_mi = de.mi;
}
if (de.ma !== undefined) {
result.o_ma = de.ma;
}
return result;
})
); );
}; };
@@ -244,8 +253,11 @@ const Customizations = () => {
setSelectedDevice(-1); setSelectedDevice(-1);
setSelectedDeviceTypeNameURL(''); setSelectedDeviceTypeNameURL('');
} else { } else {
setSelectedDeviceTypeNameURL(devices.devices[index].url || ''); const device = devices.devices[index];
setSelectedDeviceName(devices.devices[index].n); if (device) {
setSelectedDeviceTypeNameURL(device.url || '');
setSelectedDeviceName(device.n);
}
setNumChanges(0); setNumChanges(0);
setRestartNeeded(false); setRestartNeeded(false);
} }
@@ -396,14 +408,20 @@ const Customizations = () => {
await sendCustomizationEntities({ await sendCustomizationEntities({
id: selectedDevice, id: selectedDevice,
entity_ids: masked_entities entity_ids: masked_entities
}).catch((error: Error) => { })
.then(() => {
toast.success(LL.CUSTOMIZATIONS_SAVED());
})
.catch((error: Error) => {
if (error.message === 'Reboot required') { if (error.message === 'Reboot required') {
setRestartNeeded(true); setRestartNeeded(true);
} else { } else {
toast.error(error.message); toast.error(error.message);
} }
}); })
.finally(() => {
setOriginalSettings(deviceEntities); setOriginalSettings(deviceEntities);
});
} }
}; };
@@ -545,7 +563,7 @@ const Customizations = () => {
size="small" size="small"
color="secondary" color="secondary"
value={getMaskString(selectedFilters)} value={getMaskString(selectedFilters)}
onChange={(event, mask: string[]) => { onChange={(_, mask: string[]) => {
setSelectedFilters(getMaskNumber(mask)); setSelectedFilters(getMaskNumber(mask));
}} }}
> >

View File

@@ -54,7 +54,10 @@ const CustomizationsDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => { const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }

View File

@@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { IconContext } from 'react-icons/lib'; import { IconContext } from 'react-icons/lib';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -76,11 +76,12 @@ const Dashboard = () => {
} }
); );
const deviceValueDialogSave = async (devicevalue: DeviceValue) => { const deviceValueDialogSave = useCallback(
async (devicevalue: DeviceValue) => {
if (!selectedDashboardItem) { if (!selectedDashboardItem) {
return; return;
} }
const id = selectedDashboardItem.parentNode.id; // this is the parent ID const id = selectedDashboardItem.id; // this is the parent ID
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v }) await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => { .then(() => {
toast.success(LL.WRITE_CMD_SENT()); toast.success(LL.WRITE_CMD_SENT());
@@ -92,9 +93,13 @@ const Dashboard = () => {
setDeviceValueDialogOpen(false); setDeviceValueDialogOpen(false);
setSelectedDashboardItem(undefined); setSelectedDashboardItem(undefined);
}); });
}; },
[selectedDashboardItem, sendDeviceValue, LL]
);
const dashboard_theme = useTheme({ const dashboard_theme = useMemo(
() =>
useTheme({
Table: ` Table: `
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px; --data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
`, `,
@@ -122,12 +127,14 @@ const Dashboard = () => {
text-align: right; text-align: right;
} }
` `
}); }),
[]
);
const tree = useTree( const tree = useTree(
{ nodes: data.nodes }, { nodes: data.nodes },
{ {
onChange: undefined // not used but needed onChange: () => {} // not used but needed
}, },
{ {
treeIcon: { treeIcon: {
@@ -162,7 +169,8 @@ const Dashboard = () => {
: tree.fns.onRemoveAll(); // collapse tree : tree.fns.onRemoveAll(); // collapse tree
}, [parentNodes]); }, [parentNodes]);
const showType = (n?: string, t?: number) => { const showType = useCallback(
(n?: string, t?: number) => {
// if we have a name show it // if we have a name show it
if (n) { if (n) {
return n; return n;
@@ -183,7 +191,9 @@ const Dashboard = () => {
} }
} }
return ''; return '';
}; },
[LL]
);
const showName = (di: DashboardItem) => { const showName = (di: DashboardItem) => {
if (di.id < 100) { if (di.id < 100) {
@@ -201,20 +211,24 @@ const Dashboard = () => {
if (di.dv) { if (di.dv) {
return <span>{di.dv.id.slice(2)}</span>; return <span>{di.dv.id.slice(2)}</span>;
} }
return null;
}; };
const hasMask = (id: string, mask: number) => const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask; (parseInt(id.slice(0, 2), 16) & mask) === mask;
const editDashboardValue = (di: DashboardItem) => { const editDashboardValue = useCallback(
(di: DashboardItem) => {
if (me.admin && di.dv?.c) { if (me.admin && di.dv?.c) {
setSelectedDashboardItem(di); setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true); setDeviceValueDialogOpen(true);
} }
}; },
[me.admin]
);
const handleShowAll = ( const handleShowAll = (
event: React.MouseEvent<HTMLElement>, _event: React.MouseEvent<HTMLElement>,
toggle: boolean | null toggle: boolean | null
) => { ) => {
if (toggle !== null) { if (toggle !== null) {
@@ -225,7 +239,9 @@ const Dashboard = () => {
const renderContent = () => { const renderContent = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={fetchDashboard} errorMessage={error?.message} />; return (
<FormLoader onRetry={fetchDashboard} errorMessage={error?.message || ''} />
);
} }
const hasFavEntities = data.nodes.filter( const hasFavEntities = data.nodes.filter(

View File

@@ -329,13 +329,16 @@ const Devices = () => {
const handleDownloadCsv = () => { const handleDownloadCsv = () => {
const deviceIndex = coreData.devices.findIndex( const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id (d: Device) => d.id === device_select.state.id
); );
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return;
} }
const filename = const selectedDevice = coreData.devices[deviceIndex];
coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n; if (!selectedDevice) {
return;
}
const filename = selectedDevice.tn + '_' + selectedDevice.n;
const columns = [ const columns = [
{ {
@@ -350,7 +353,7 @@ const Devices = () => {
{ {
accessor: (dv: DeviceValue) => accessor: (dv: DeviceValue) =>
dv.u !== undefined && DeviceValueUOM_s[dv.u] dv.u !== undefined && DeviceValueUOM_s[dv.u]
? DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, '') ? DeviceValueUOM_s[dv.u]?.replace(/[^a-zA-Z0-9]/g, '')
: '', : '',
name: 'UoM' name: 'UoM'
}, },
@@ -373,7 +376,9 @@ const Devices = () => {
]; ];
const data = onlyFav const data = onlyFav
? deviceData.nodes.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) ? deviceData.nodes.filter((dv: DeviceValue) =>
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)
)
: deviceData.nodes; : deviceData.nodes;
const csvData = data.reduce( const csvData = data.reduce(
@@ -433,10 +438,14 @@ const Devices = () => {
const renderDeviceDetails = () => { const renderDeviceDetails = () => {
if (showDeviceInfo) { if (showDeviceInfo) {
const deviceIndex = coreData.devices.findIndex( const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id (d: Device) => d.id === device_select.state.id
); );
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return null;
}
const deviceDetails = coreData.devices[deviceIndex];
if (!deviceDetails) {
return null;
} }
return ( return (
@@ -449,47 +458,35 @@ const Devices = () => {
<DialogContent dividers> <DialogContent dividers>
<List dense={true}> <List dense={true}>
<ListItem> <ListItem>
<ListItemText <ListItemText primary={LL.TYPE(0)} secondary={deviceDetails.tn} />
primary={LL.TYPE(0)}
secondary={coreData.devices[deviceIndex].tn}
/>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText primary={LL.NAME(0)} secondary={deviceDetails.n} />
primary={LL.NAME(0)}
secondary={coreData.devices[deviceIndex].n}
/>
</ListItem> </ListItem>
{coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && ( {deviceDetails.t !== DeviceType.CUSTOM && (
<> <>
<ListItem> <ListItem>
<ListItemText <ListItemText primary={LL.BRAND()} secondary={deviceDetails.b} />
primary={LL.BRAND()}
secondary={coreData.devices[deviceIndex].b}
/>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={LL.ID_OF(LL.DEVICE())} primary={LL.ID_OF(LL.DEVICE())}
secondary={ secondary={
'0x' + '0x' +
( ('00' + deviceDetails.d.toString(16).toUpperCase()).slice(-2)
'00' +
coreData.devices[deviceIndex].d.toString(16).toUpperCase()
).slice(-2)
} }
/> />
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={LL.ID_OF(LL.PRODUCT())} primary={LL.ID_OF(LL.PRODUCT())}
secondary={coreData.devices[deviceIndex].p} secondary={deviceDetails.p}
/> />
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={LL.VERSION()} primary={LL.VERSION()}
secondary={coreData.devices[deviceIndex].v} secondary={deviceDetails.v}
/> />
</ListItem> </ListItem>
</> </>
@@ -508,6 +505,7 @@ const Devices = () => {
</Dialog> </Dialog>
); );
} }
return null;
}; };
const renderCoreData = () => ( const renderCoreData = () => (
@@ -598,21 +596,26 @@ const Devices = () => {
const shown_data = onlyFav const shown_data = onlyFav
? deviceData.nodes.filter( ? deviceData.nodes.filter(
(dv) => (dv: DeviceValue) =>
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
dv.id.slice(2).toLowerCase().includes(search.toLowerCase()) dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
) )
: deviceData.nodes.filter((dv) => : deviceData.nodes.filter((dv: DeviceValue) =>
dv.id.slice(2).toLowerCase().includes(search.toLowerCase()) dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
); );
const deviceIndex = coreData.devices.findIndex( const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id (d: Device) => d.id === device_select.state.id
); );
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return;
} }
const deviceInfo = coreData.devices[deviceIndex];
if (!deviceInfo) {
return;
}
const [, height] = size;
return ( return (
<Box <Box
sx={{ sx={{
@@ -623,15 +626,15 @@ const Devices = () => {
bottom: 0, bottom: 0,
top: 64, top: 64,
zIndex: 'modal', zIndex: 'modal',
maxHeight: () => size[1] - 126, maxHeight: () => (height || 0) - 126,
border: '1px solid #177ac9' border: '1px solid #177ac9'
}} }}
> >
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
<Typography noWrap variant="subtitle1" color="warning.main"> <Typography noWrap variant="subtitle1" color="warning.main">
{coreData.devices[deviceIndex].n}&nbsp;( {deviceInfo.n}&nbsp;(
{coreData.devices[deviceIndex].tn}) {deviceInfo.tn})
</Typography> </Typography>
<Grid justifyContent="flex-end"> <Grid justifyContent="flex-end">
<ButtonTooltip title={LL.CLOSE()}> <ButtonTooltip title={LL.CLOSE()}>
@@ -701,7 +704,7 @@ const Devices = () => {
' ' + ' ' +
shown_data.length + shown_data.length +
'/' + '/' +
coreData.devices[deviceIndex].e + deviceInfo.e +
' ' + ' ' +
LL.ENTITIES(shown_data.length)} LL.ENTITIES(shown_data.length)}
</span> </span>

View File

@@ -120,7 +120,7 @@ const DevicesDialog = ({
{editItem.l ? ( {editItem.l ? (
<TextField <TextField
name="v" name="v"
label={LL.VALUE(0)} // label={LL.VALUE(0)}
value={editItem.v} value={editItem.v}
disabled={!writeable} disabled={!writeable}
sx={{ width: '30ch' }} sx={{ width: '30ch' }}
@@ -135,7 +135,7 @@ const DevicesDialog = ({
</TextField> </TextField>
) : editItem.s || editItem.u !== DeviceValueUOM.NONE ? ( ) : editItem.s || editItem.u !== DeviceValueUOM.NONE ? (
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="v" name="v"
label={LL.VALUE(0)} label={LL.VALUE(0)}
value={numberValue(Math.round((editItem.v as number) * 10) / 10)} value={numberValue(Math.round((editItem.v as number) * 10) / 10)}
@@ -159,7 +159,7 @@ const DevicesDialog = ({
/> />
) : ( ) : (
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="v" name="v"
label={LL.VALUE(0)} label={LL.VALUE(0)}
value={editItem.v} value={editItem.v}

View File

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

View File

@@ -133,13 +133,15 @@ const Modules = () => {
}; };
const saveModules = async () => { const saveModules = async () => {
await updateModules({ await Promise.all(
modules: modules.map((condensed_mi) => ({ modules.map((condensed_mi: ModuleItem) =>
updateModules({
key: condensed_mi.key, key: condensed_mi.key,
enabled: condensed_mi.enabled, enabled: condensed_mi.enabled,
license: condensed_mi.license license: condensed_mi.license
}))
}) })
)
)
.then(() => { .then(() => {
toast.success(LL.MODULES_UPDATED()); toast.success(LL.MODULES_UPDATED());
}) })
@@ -154,7 +156,9 @@ const Modules = () => {
const renderContent = () => { const renderContent = () => {
if (!modules) { if (!modules) {
return <FormLoader onRetry={fetchModules} errorMessage={error?.message} />; return (
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
);
} }
if (modules.length === 0) { if (modules.length === 0) {

View File

@@ -135,8 +135,8 @@ const Scheduler = () => {
const saveSchedule = async () => { const saveSchedule = async () => {
await updateSchedule({ await updateSchedule({
schedule: schedule schedule: schedule
.filter((si) => !si.deleted) .filter((si: ScheduleItem) => !si.deleted)
.map((condensed_si) => ({ .map((condensed_si: ScheduleItem) => ({
id: condensed_si.id, id: condensed_si.id,
active: condensed_si.active, active: condensed_si.active,
flags: condensed_si.flags, flags: condensed_si.flags,
@@ -212,7 +212,9 @@ const Scheduler = () => {
const renderSchedule = () => { const renderSchedule = () => {
if (!schedule) { if (!schedule) {
return <FormLoader onRetry={fetchSchedule} errorMessage={error?.message} />; return (
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
);
} }
const dayBox = (si: ScheduleItem, flag: number) => ( const dayBox = (si: ScheduleItem, flag: number) => (
@@ -251,8 +253,8 @@ const Scheduler = () => {
<Table <Table
data={{ data={{
nodes: schedule nodes: schedule
.filter((si) => !si.deleted) .filter((si: ScheduleItem) => !si.deleted)
.sort((a, b) => a.flags - b.flags) .sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags)
}} }}
theme={schedule_theme} theme={schedule_theme}
layout={{ custom: true }} layout={{ custom: true }}

View File

@@ -144,7 +144,10 @@ const SchedulerDialog = ({
</Typography> </Typography>
); );
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => { const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -325,7 +328,7 @@ const SchedulerDialog = ({
</> </>
)} )}
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="cmd" name="cmd"
label={LL.COMMAND(0)} label={LL.COMMAND(0)}
multiline multiline
@@ -344,7 +347,7 @@ const SchedulerDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="name" name="name"
label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'} label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'}
value={editItem.name} value={editItem.name}

View File

@@ -57,7 +57,10 @@ const SensorsAnalogDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => { const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -88,7 +91,7 @@ const SensorsAnalogDialog = ({
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="g" name="g"
label="GPIO" label="GPIO"
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
@@ -107,7 +110,7 @@ const SensorsAnalogDialog = ({
)} )}
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="n" name="n"
label={LL.NAME(0)} label={LL.NAME(0)}
value={editItem.n} value={editItem.n}

View File

@@ -52,7 +52,10 @@ const SensorsTemperatureDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => { const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') { if (reason !== 'backdropClick') {
onClose(); onClose();
} }
@@ -82,7 +85,7 @@ const SensorsTemperatureDialog = ({
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="n" name="n"
label={LL.NAME(0)} label={LL.NAME(0)}
value={editItem.n} value={editItem.n}

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ const ApplicationSettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue updateDataValue as (value: unknown) => void
); );
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -135,7 +135,7 @@ const ApplicationSettings = () => {
const content = () => { const content = () => {
if (!data || !hardwareData) { if (!data || !hardwareData) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -219,7 +219,7 @@ const ApplicationSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="modbus_max_clients" name="modbus_max_clients"
label={LL.AP_MAX_CLIENTS()} label={LL.AP_MAX_CLIENTS()}
variant="outlined" variant="outlined"
@@ -231,7 +231,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="modbus_port" name="modbus_port"
label="Port" label="Port"
variant="outlined" variant="outlined"
@@ -243,7 +243,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="modbus_timeout" name="modbus_timeout"
label="Timeout" label="Timeout"
slotProps={{ slotProps={{
@@ -273,7 +273,7 @@ const ApplicationSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="syslog_host" name="syslog_host"
label="Host" label="Host"
variant="outlined" variant="outlined"
@@ -284,7 +284,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="syslog_port" name="syslog_port"
label="Port" label="Port"
variant="outlined" variant="outlined"
@@ -315,7 +315,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="syslog_mark_interval" name="syslog_mark_interval"
label={LL.MARK_INTERVAL()} label={LL.MARK_INTERVAL()}
slotProps={{ slotProps={{
@@ -485,7 +485,7 @@ const ApplicationSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="rx_gpio" name="rx_gpio"
label={LL.GPIO_OF('Rx')} label={LL.GPIO_OF('Rx')}
fullWidth fullWidth
@@ -498,7 +498,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="tx_gpio" name="tx_gpio"
label={LL.GPIO_OF('Tx')} label={LL.GPIO_OF('Tx')}
fullWidth fullWidth
@@ -511,7 +511,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="pbutton_gpio" name="pbutton_gpio"
label={LL.GPIO_OF(LL.BUTTON())} label={LL.GPIO_OF(LL.BUTTON())}
fullWidth fullWidth
@@ -524,7 +524,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="dallas_gpio" name="dallas_gpio"
label={ label={
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')' LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
@@ -539,7 +539,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="led_gpio" name="led_gpio"
label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'} label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'}
fullWidth fullWidth
@@ -743,7 +743,7 @@ const ApplicationSettings = () => {
{data.remote_timeout_en && ( {data.remote_timeout_en && (
<Box mt={2}> <Box mt={2}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="remote_timeout" name="remote_timeout"
label={LL.REMOTE_TIMEOUT()} label={LL.REMOTE_TIMEOUT()}
slotProps={{ slotProps={{
@@ -783,7 +783,7 @@ const ApplicationSettings = () => {
{data.shower_timer && ( {data.shower_timer && (
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="shower_min_duration" name="shower_min_duration"
label={LL.MIN_DURATION()} label={LL.MIN_DURATION()}
slotProps={{ slotProps={{
@@ -801,7 +801,7 @@ const ApplicationSettings = () => {
<> <>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="shower_alert_trigger" name="shower_alert_trigger"
label={LL.TRIGGER_TIME()} label={LL.TRIGGER_TIME()}
slotProps={{ slotProps={{
@@ -817,7 +817,7 @@ const ApplicationSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="shower_alert_coldshot" name="shower_alert_coldshot"
label={LL.COLD_SHOT_DURATION()} label={LL.COLD_SHOT_DURATION()}
slotProps={{ slotProps={{

View File

@@ -57,7 +57,7 @@ const DownloadUpload = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
return ( return (

View File

@@ -56,7 +56,7 @@ const MqttSettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue updateDataValue as (value: unknown) => void
); );
const SecondsInputProps = { const SecondsInputProps = {
@@ -65,7 +65,7 @@ const MqttSettings = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -93,7 +93,7 @@ const MqttSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="host" name="host"
label={LL.ADDRESS_OF(LL.BROKER())} label={LL.ADDRESS_OF(LL.BROKER())}
multiline multiline
@@ -105,7 +105,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="port" name="port"
label="Port" label="Port"
variant="outlined" variant="outlined"
@@ -117,7 +117,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="base" name="base"
label={LL.BASE_TOPIC()} label={LL.BASE_TOPIC()}
variant="outlined" variant="outlined"
@@ -158,7 +158,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="keep_alive" name="keep_alive"
label="Keep Alive" label="Keep Alive"
slotProps={{ slotProps={{
@@ -354,7 +354,7 @@ const MqttSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="publish_time_heartbeat" name="publish_time_heartbeat"
label="Heartbeat" label="Heartbeat"
slotProps={{ slotProps={{

View File

@@ -76,7 +76,7 @@ const NTPSettings = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue updateDataValue as (value: unknown) => void
); );
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -155,7 +155,7 @@ const NTPSettings = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
} }
const validateAndSubmit = async () => { const validateAndSubmit = async () => {
@@ -190,7 +190,7 @@ const NTPSettings = () => {
label={LL.ENABLE_NTP()} label={LL.ENABLE_NTP()}
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="server" name="server"
label={LL.NTP_SERVER()} label={LL.NTP_SERVER()}
fullWidth fullWidth
@@ -200,7 +200,7 @@ const NTPSettings = () => {
margin="normal" margin="normal"
/> />
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors || {}}
name="tz_label" name="tz_label"
label={LL.TIME_ZONE()} label={LL.TIME_ZONE()}
fullWidth fullWidth

View File

@@ -35,7 +35,7 @@ const Network = () => {
], ],
useLocation() useLocation()
); );
const routerTab = matchedRoutes?.[0].route.path || false; const routerTab = matchedRoutes?.[0]?.route.path || false;
const navigate = useNavigate(); const navigate = useNavigate();
@@ -56,7 +56,7 @@ const Network = () => {
return ( return (
<WiFiConnectionContext.Provider <WiFiConnectionContext.Provider
value={{ value={{
selectedNetwork, ...(selectedNetwork && { selectedNetwork }),
selectNetwork, selectNetwork,
deselectNetwork deselectNetwork
}} }}

View File

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

View File

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

View File

@@ -97,7 +97,7 @@ const ManageUsers = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
} }
const noAdminConfigured = () => !data.users.find((u) => u.admin); const noAdminConfigured = () => !data.users.find((u) => u.admin);
@@ -260,7 +260,11 @@ const ManageUsers = () => {
</Box> </Box>
</Box> </Box>
<GenerateToken username={generatingToken} onClose={closeGenerateToken} /> <GenerateToken
username={generatingToken || ''}
onClose={closeGenerateToken}
/>
{user && (
<User <User
user={user} user={user}
setUser={setUser} setUser={setUser}
@@ -269,6 +273,7 @@ const ManageUsers = () => {
onCancelEditing={cancelEditingUser} onCancelEditing={cancelEditingUser}
validator={createUserValidator(data.users, creating)} validator={createUserValidator(data.users, creating)}
/> />
)}
</> </>
); );
}; };

View File

@@ -19,7 +19,7 @@ const Security = () => {
], ],
useLocation() useLocation()
); );
const routerTab = matchedRoutes?.[0].route.path || false; const routerTab = matchedRoutes?.[0]?.route.path || false;
return ( return (
<> <>

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ const APStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
return ( return (

View File

@@ -67,7 +67,8 @@ const SystemActivity = () => {
}); });
const showName = (id: number) => { const showName = (id: number) => {
const name: keyof Translation['STATUS_NAMES'] = id; const name: keyof Translation['STATUS_NAMES'] =
id.toString() as keyof Translation['STATUS_NAMES'];
return LL.STATUS_NAMES[name](); return LL.STATUS_NAMES[name]();
}; };
@@ -87,7 +88,7 @@ const SystemActivity = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
return ( return (

View File

@@ -41,7 +41,7 @@ const HardwareStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
return ( return (

View File

@@ -99,7 +99,7 @@ const MqttStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
const renderConnectionStatus = () => ( const renderConnectionStatus = () => (

View File

@@ -68,7 +68,7 @@ const NTPStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
return ( return (

View File

@@ -120,7 +120,7 @@ const NetworkStatus = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
return ( return (

View File

@@ -248,7 +248,7 @@ const SystemStatus = () => {
const content = () => { const content = () => {
if (!data || !LL) { if (!data || !LL) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
return ( return (

View File

@@ -31,13 +31,14 @@ import type { LogEntry, LogSettings } from 'types';
import { LogLevel } from 'types'; import { LogLevel } from 'types';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
const TextColors = { const TextColors: Record<LogLevel, string> = {
[LogLevel.ERROR]: '#ff0000', // red [LogLevel.ERROR]: '#ff0000', // red
[LogLevel.WARNING]: '#ff0000', // red [LogLevel.WARNING]: '#ff0000', // red
[LogLevel.NOTICE]: '#ffffff', // white [LogLevel.NOTICE]: '#ffffff', // white
[LogLevel.INFO]: '#ffcc00', // yellow [LogLevel.INFO]: '#ffcc00', // yellow
[LogLevel.DEBUG]: '#00ffff', // cyan [LogLevel.DEBUG]: '#00ffff', // cyan
[LogLevel.TRACE]: '#00ffff' // cyan [LogLevel.TRACE]: '#00ffff', // cyan
[LogLevel.ALL]: '#ffffff' // white
}; };
const LogEntryLine = styled('span')( const LogEntryLine = styled('span')(
@@ -109,7 +110,7 @@ const SystemLog = () => {
origData, origData,
dirtyFlags, dirtyFlags,
setDirtyFlags, setDirtyFlags,
updateDataValue updateDataValue as (value: unknown) => void
); );
useSSE(fetchLogES, { useSSE(fetchLogES, {
@@ -190,7 +191,7 @@ const SystemLog = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
} }
return ( return (

View File

@@ -110,7 +110,7 @@ const Version = () => {
}, [latestVersion, latestDevVersion]); }, [latestVersion, latestDevVersion]);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }); const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const DIVISIONS = [ const DIVISIONS: Array<{ amount: number; name: string }> = [
{ amount: 60, name: 'seconds' }, { amount: 60, name: 'seconds' },
{ amount: 60, name: 'minutes' }, { amount: 60, name: 'minutes' },
{ amount: 24, name: 'hours' }, { amount: 24, name: 'hours' },
@@ -119,19 +119,22 @@ const Version = () => {
{ amount: 12, name: 'months' }, { amount: 12, name: 'months' },
{ amount: Number.POSITIVE_INFINITY, name: 'years' } { amount: Number.POSITIVE_INFINITY, name: 'years' }
]; ];
function formatTimeAgo(date) { function formatTimeAgo(date: Date) {
let duration = (date.getTime() - new Date().getTime()) / 1000; let duration = (date.getTime() - new Date().getTime()) / 1000;
for (let i = 0; i < DIVISIONS.length; i++) { for (let i = 0; i < DIVISIONS.length; i++) {
const division = DIVISIONS[i]; const division = DIVISIONS[i];
if (Math.abs(duration) < division.amount) { if (division && Math.abs(duration) < division.amount) {
return rtf.format( return rtf.format(
Math.round(duration), Math.round(duration),
division.name as Intl.RelativeTimeFormatUnit division.name as Intl.RelativeTimeFormatUnit
); );
} }
if (division) {
duration /= division.amount; duration /= division.amount;
} }
} }
return rtf.format(0, 'seconds');
}
const { send: sendAPI } = useRequest((data: APIcall) => API(data), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false
@@ -270,6 +273,14 @@ const Version = () => {
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}> <span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())} {LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
</span> </span>
<Button
sx={{ ml: 2 }}
variant="outlined"
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{LL.REINSTALL()}
</Button>
</> </>
); );
} }
@@ -293,7 +304,7 @@ const Version = () => {
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
const isDev = data.emsesp_version.includes('dev'); const isDev = data.emsesp_version.includes('dev');

View File

@@ -1,26 +1,24 @@
import type { FC } from 'react'; import { memo } from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import type { BoxProps } from '@mui/material'; import type { BoxProps } from '@mui/material';
const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => ( const ButtonRow = memo<BoxProps>(({ children, ...rest }) => (
<Box <Box
sx={{ sx={{
'& button, & a, & .MuiCard-root': { '& button, & a, & .MuiCard-root': {
mt: 2, mt: 2,
mx: 0.6, mx: 0.6,
'&:last-child': { '&:last-child': { mr: 0 },
mr: 0 '&:first-of-type': { ml: 0 }
},
'&:first-of-type': {
ml: 0
}
} }
}} }}
{...rest} {...rest}
> >
{children} {children}
</Box> </Box>
); ));
ButtonRow.displayName = 'ButtonRow';
export default ButtonRow; export default ButtonRow;

View File

@@ -1,7 +1,12 @@
import { Tooltip, type TooltipProps, styled, tooltipClasses } from '@mui/material'; import { Tooltip, type TooltipProps, styled, tooltipClasses } from '@mui/material';
export const ButtonTooltip = styled(({ className, ...props }: TooltipProps) => ( export const ButtonTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} placement="top" arrow classes={{ popper: className }} /> <Tooltip
{...props}
placement="top"
arrow
classes={{ ...(className && { popper: className }) }}
/>
))(({ theme }) => ({ ))(({ theme }) => ({
[`& .${tooltipClasses.arrow}`]: { [`& .${tooltipClasses.arrow}`]: {
color: theme.palette.success.main color: theme.palette.success.main

View File

@@ -1,10 +1,15 @@
// Optimized exports - use direct exports to reduce bundle size
export { default as SectionContent } from './SectionContent';
export { default as ButtonRow } from './ButtonRow';
export { default as MessageBox } from './MessageBox';
export { default as ButtonTooltip } from './ButtonTooltip';
// Re-export sub-modules
export * from './inputs'; export * from './inputs';
export * from './layout'; export * from './layout';
export * from './loading'; export * from './loading';
export * from './routing'; export * from './routing';
export * from './upload'; export * from './upload';
export { default as SectionContent } from './SectionContent';
export { default as ButtonRow } from './ButtonRow'; // Specific routing exports
export { default as MessageBox } from './MessageBox';
export { default as BlockNavigation } from './routing/BlockNavigation'; export { default as BlockNavigation } from './routing/BlockNavigation';
export { default as ButtonTooltip } from './ButtonTooltip';

View File

@@ -16,14 +16,14 @@ const ValidatedTextField: FC<ValidatedTextFieldProps> = ({
fieldErrors, fieldErrors,
...rest ...rest
}) => { }) => {
const errors = fieldErrors && fieldErrors[rest.name]; const errors = fieldErrors?.[rest.name];
const renderErrors = () =>
errors &&
errors.map((e) => <FormHelperText key={e.message}>{e.message}</FormHelperText>);
return ( return (
<> <>
<TextField error={!!errors} {...rest} /> <TextField error={!!errors} {...rest} />
{renderErrors()} {errors?.map((e) => (
<FormHelperText key={e.message}>{e.message}</FormHelperText>
))}
</> </>
); );
}; };

View File

@@ -23,7 +23,12 @@ const LayoutMenuItem = ({
const selected = routeMatches(to, pathname); const selected = routeMatches(to, pathname);
return ( return (
<ListItemButton component={Link} to={to} disabled={disabled} selected={selected}> <ListItemButton
component={Link}
to={to}
disabled={disabled || false}
selected={selected}
>
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}> <ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
<Icon /> <Icon />
</ListItemIcon> </ListItemIcon>

View File

@@ -58,12 +58,22 @@ const LayoutMenuItem = ({
} }
> >
<ListItemButton component={Link} to={to}> <ListItemButton component={Link} to={to}>
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} /> <RenderIcon
icon={icon}
{...(bgcolor && { bgcolor })}
label={label}
text={text}
/>
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
) : ( ) : (
<ListItem> <ListItem>
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} /> <RenderIcon
icon={icon}
{...(bgcolor && { bgcolor })}
label={label}
text={text}
/>
</ListItem> </ListItem>
)} )}
</> </>

View File

@@ -0,0 +1,20 @@
import { Box, CircularProgress } from '@mui/material';
const LazyLoader = () => (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="200px"
sx={{
backgroundColor: 'background.default',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider'
}}
>
<CircularProgress size={40} />
</Box>
);
export default LazyLoader;

View File

@@ -1,2 +1,3 @@
export { default as LoadingSpinner } from './LoadingSpinner'; export { default as LoadingSpinner } from './LoadingSpinner';
export { default as FormLoader } from './FormLoader'; export { default as FormLoader } from './FormLoader';
export { default as LazyLoader } from './LazyLoader';

View File

@@ -1,6 +1,5 @@
import type { Path } from 'react-router'; import type { Path } from 'react-router';
import type * as H from 'history';
import { jwtDecode } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
import type { Me, SignInRequest, SignInResponse } from 'types'; import type { Me, SignInRequest, SignInResponse } from 'types';
@@ -18,10 +17,10 @@ export function getStorage() {
return localStorage || sessionStorage; return localStorage || sessionStorage;
} }
export function storeLoginRedirect(location?: H.Location) { export function storeLoginRedirect(location?: { pathname: string; search: string }) {
if (location) { if (location) {
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname as string); getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
getStorage().setItem(SIGN_IN_SEARCH, location.search as string); getStorage().setItem(SIGN_IN_SEARCH, location.search);
} }
} }
@@ -36,7 +35,7 @@ export function fetchLoginRedirect(): Partial<Path> {
clearLoginRedirect(); clearLoginRedirect();
return { return {
pathname: signInPathname || `/dashboard`, pathname: signInPathname || `/dashboard`,
search: (signInPathname && signInSearch) || undefined ...(signInPathname && signInSearch && { search: signInSearch })
}; };
} }

View File

@@ -1,16 +1,59 @@
// Code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0 // Code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
import { type ChangeEvent, useRef, useState } from 'react'; import {
type ChangeEvent,
type DragEvent,
type MouseEvent,
useRef,
useState
} from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CloudUploadIcon from '@mui/icons-material/CloudUpload'; import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import UploadIcon from '@mui/icons-material/Upload'; import UploadIcon from '@mui/icons-material/Upload';
import { Box, Button } from '@mui/material'; import { Box, Button, Typography, styled } from '@mui/material';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import './dragNdrop.css'; const DocumentUploader = styled(Box)<{ active?: boolean }>(({ theme, active }) => ({
border: `2px dashed ${active ? '#6dc24b' : '#4282fe'}`,
backgroundColor: '#2e3339',
padding: theme.spacing(1.25),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
borderRadius: theme.spacing(1),
cursor: 'pointer',
minHeight: '120px',
transition: 'border-color 0.2s ease-in-out'
}));
const DragNdrop = ({ text, onFileSelected }) => { const UploadInfo = styled(Box)({
display: 'flex',
alignItems: 'center'
});
const FileInfo = styled(Box)({
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'space-between',
alignItems: 'center'
});
const FileName = styled(Typography)(({ theme }) => ({
fontSize: '14px',
color: '#6dc24b',
margin: theme.spacing(1, 0)
}));
interface DragNdropProps {
text: string;
onFileSelected: (file: File) => void;
}
const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
const [file, setFile] = useState<File>(); const [file, setFile] = useState<File>();
const [dragged, setDragged] = useState(false); const [dragged, setDragged] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
@@ -28,14 +71,17 @@ const DragNdrop = ({ text, onFileSelected }) => {
}; };
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) { if (!e.target.files || e.target.files.length === 0) {
return; return;
} }
checkFileExtension(e.target.files[0]); const selectedFile = e.target.files[0];
if (selectedFile) {
checkFileExtension(selectedFile);
}
e.target.value = ''; // this is to allow the same file to be selected again e.target.value = ''; // this is to allow the same file to be selected again
}; };
const handleDrop = (event) => { const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
const droppedFiles = event.dataTransfer.files; const droppedFiles = event.dataTransfer.files;
if (droppedFiles.length > 0) { if (droppedFiles.length > 0) {
@@ -43,38 +89,40 @@ const DragNdrop = ({ text, onFileSelected }) => {
} }
}; };
const handleRemoveFile = (event) => { const handleRemoveFile = (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
setFile(undefined); setFile(undefined);
setDragged(false); setDragged(false);
}; };
const handleUploadClick = (event) => { const handleUploadClick = (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
if (file) {
onFileSelected(file); onFileSelected(file);
}
}; };
const handleBrowseClick = () => { const handleBrowseClick = () => {
inputRef.current?.click(); inputRef.current?.click();
}; };
const handleDragOver = (event) => { const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); // prevent file from being opened event.preventDefault(); // prevent file from being opened
setDragged(true); setDragged(true);
}; };
return ( return (
<div <DocumentUploader
className={`document-uploader ${file || dragged ? 'active' : ''}`} active={!!(file || dragged)}
onDrop={handleDrop} onDrop={handleDrop}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={() => setDragged(false)} onDragLeave={() => setDragged(false)}
onClick={handleBrowseClick} onClick={handleBrowseClick}
> >
<div className="upload-info"> <UploadInfo>
<CloudUploadIcon sx={{ marginRight: 4 }} color="primary" fontSize="large" /> <CloudUploadIcon sx={{ marginRight: 4 }} color="primary" fontSize="large" />
<p>{text}</p> <Typography>{text}</Typography>
</div> </UploadInfo>
<input <input
type="file" type="file"
@@ -88,9 +136,9 @@ const DragNdrop = ({ text, onFileSelected }) => {
{file && ( {file && (
<> <>
<div className="file-info"> <FileInfo>
<p>{file.name}</p> <FileName>{file.name}</FileName>
</div> </FileInfo>
<Box> <Box>
<Button <Button
startIcon={<CancelIcon />} startIcon={<CancelIcon />}
@@ -112,7 +160,7 @@ const DragNdrop = ({ text, onFileSelected }) => {
</Box> </Box>
</> </>
)} )}
</div> </DocumentUploader>
); );
}; };

View File

@@ -12,7 +12,12 @@ import { useI18nContext } from 'i18n/i18n-react';
import DragNdrop from './DragNdrop'; import DragNdrop from './DragNdrop';
import { LinearProgressWithLabel } from './LinearProgressWithLabel'; import { LinearProgressWithLabel } from './LinearProgressWithLabel';
const SingleUpload = ({ text, doRestart }) => { interface SingleUploadProps {
text: string;
doRestart: () => void;
}
const SingleUpload = ({ text, doRestart }: SingleUploadProps) => {
const [md5, setMd5] = useState<string>(); const [md5, setMd5] = useState<string>();
const [file, setFile] = useState<File>(); const [file, setFile] = useState<File>();
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -25,8 +30,8 @@ const SingleUpload = ({ text, doRestart }) => {
} = useRequest(SystemApi.uploadFile, { } = useRequest(SystemApi.uploadFile, {
immediate: false immediate: false
}).onSuccess(({ data }) => { }).onSuccess(({ data }) => {
if (data) { if (data && typeof data === 'object' && 'md5' in data) {
setMd5(data.md5 as string); setMd5((data as { md5: string }).md5);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL()); toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
setFile(undefined); setFile(undefined);
} else { } else {
@@ -34,16 +39,19 @@ const SingleUpload = ({ text, doRestart }) => {
} }
}); });
useEffect(async () => { useEffect(() => {
const uploadFile = async () => {
if (file) { if (file) {
await sendUpload(file).catch((error: Error) => { await sendUpload(file).catch((error: Error) => {
if (error.message === 'The user aborted a request') { if (error.message.includes('The user aborted a request')) {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED()); toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else { } else {
toast.warning('Invalid file extension or incompatible bin file'); toast.warning('Invalid file extension or incompatible bin file');
} }
}); });
} }
};
void uploadFile();
}, [file]); }, [file]);
return ( return (

View File

@@ -1,33 +0,0 @@
.document-uploader {
border: 2px dashed #4282fe;
background-color: #2e3339;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
border-radius: 8px;
cursor: pointer;
&.active {
border-color: #6dc24b;
}
.upload-info {
display: flex;
align-items: center;
}
.file-info {
display: flex;
flex-direction: column;
width: 100%;
justify-content: space-between;
align-items: center;
p {
font-size: 14px;
color: #6dc24b;
}
}
}

View File

@@ -69,7 +69,12 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
// cache object to prevent re-renders // cache object to prevent re-renders
const obj = useMemo( const obj = useMemo(
() => ({ signIn, signOut, me, refresh }), () => ({
signIn,
signOut,
refresh,
...(me && { me })
}),
[signIn, signOut, me, refresh] [signIn, signOut, me, refresh]
); );

View File

@@ -162,6 +162,7 @@ const cz: Translation = {
UPLOAD: 'Nahrát', UPLOAD: 'Nahrát',
DOWNLOAD: '{{S|s|s}}táhnout', DOWNLOAD: '{{S|s|s}}táhnout',
INSTALL: 'Instalovat', INSTALL: 'Instalovat',
REINSTALL: 'Znovu instalovat',
ABORTED: 'přerušeno', ABORTED: 'přerušeno',
FAILED: 'neúspěšné', FAILED: 'neúspěšné',
SUCCESSFUL: 'úspěšné', SUCCESSFUL: 'úspěšné',

View File

@@ -162,6 +162,7 @@ const de: Translation = {
UPLOAD: 'Hochladen', UPLOAD: 'Hochladen',
DOWNLOAD: '{{Herunterladen|heruntergeladen|}}', DOWNLOAD: '{{Herunterladen|heruntergeladen|}}',
INSTALL: 'Installieren', INSTALL: 'Installieren',
REINSTALL: 'Neu installieren',
ABORTED: 'abgebrochen', ABORTED: 'abgebrochen',
FAILED: 'gescheitert', FAILED: 'gescheitert',
SUCCESSFUL: 'erfolgreich', SUCCESSFUL: 'erfolgreich',

View File

@@ -162,6 +162,7 @@ const en: Translation = {
UPLOAD: 'Upload', UPLOAD: 'Upload',
DOWNLOAD: '{{D|d|d}}ownload', DOWNLOAD: '{{D|d|d}}ownload',
INSTALL: 'Install', INSTALL: 'Install',
REINSTALL: 'Reinstall',
ABORTED: 'aborted', ABORTED: 'aborted',
FAILED: 'failed', FAILED: 'failed',
SUCCESSFUL: 'successful', SUCCESSFUL: 'successful',

View File

@@ -162,6 +162,7 @@ const fr: Translation = {
UPLOAD: 'Upload', UPLOAD: 'Upload',
DOWNLOAD: '{{D|d|d}}ownload', DOWNLOAD: '{{D|d|d}}ownload',
INSTALL: 'Installer', INSTALL: 'Installer',
REINSTALL: 'Réinstaller',
ABORTED: 'annulé', ABORTED: 'annulé',
FAILED: 'échoué', FAILED: 'échoué',
SUCCESSFUL: 'réussi', SUCCESSFUL: 'réussi',

View File

@@ -162,6 +162,7 @@ const it: Translation = {
UPLOAD: 'Carica', UPLOAD: 'Carica',
DOWNLOAD: 'Scarica', DOWNLOAD: 'Scarica',
INSTALL: 'Installare {0}', INSTALL: 'Installare {0}',
REINSTALL: 'Riavviare',
ABORTED: 'Annullato', ABORTED: 'Annullato',
FAILED: 'Fallito', FAILED: 'Fallito',
SUCCESSFUL: 'Riuscito', SUCCESSFUL: 'Riuscito',

View File

@@ -162,6 +162,7 @@ const nl: Translation = {
UPLOAD: 'Upload', UPLOAD: 'Upload',
DOWNLOAD: '{{D|d|d}}ownload', DOWNLOAD: '{{D|d|d}}ownload',
INSTALL: 'Installeren', INSTALL: 'Installeren',
REINSTALL: 'Opnieuw installeren',
ABORTED: 'afgebroken', ABORTED: 'afgebroken',
FAILED: 'mislukt', FAILED: 'mislukt',
SUCCESSFUL: 'successvol', SUCCESSFUL: 'successvol',

View File

@@ -162,6 +162,7 @@ const no: Translation = {
UPLOAD: 'Opplasning', UPLOAD: 'Opplasning',
DOWNLOAD: '{{N|n|n}}edlasting', DOWNLOAD: '{{N|n|n}}edlasting',
INSTALL: 'Installer', INSTALL: 'Installer',
REINSTALL: 'Ominstaller',
ABORTED: 'avbrutt', ABORTED: 'avbrutt',
FAILED: 'feilet', FAILED: 'feilet',
SUCCESSFUL: 'vellykket', SUCCESSFUL: 'vellykket',

View File

@@ -162,6 +162,7 @@ const pl: BaseTranslation = {
UPLOAD: 'Wysyłanie', UPLOAD: 'Wysyłanie',
DOWNLOAD: '{{P|p||P}}obier{{anie|z||z}}', DOWNLOAD: '{{P|p||P}}obier{{anie|z||z}}',
INSTALL: 'Zainstalować', INSTALL: 'Zainstalować',
REINSTALL: 'Zainstalować ponownie',
ABORTED: 'zostało przerwane!', ABORTED: 'zostało przerwane!',
FAILED: 'nie powiodł{{o|a|}} się!', FAILED: 'nie powiodł{{o|a|}} się!',
SUCCESSFUL: 'powiodło się.', SUCCESSFUL: 'powiodło się.',

View File

@@ -162,6 +162,7 @@ const sk: Translation = {
UPLOAD: 'Nahrať', UPLOAD: 'Nahrať',
DOWNLOAD: '{{S|s|s}}tiahnuť', DOWNLOAD: '{{S|s|s}}tiahnuť',
INSTALL: 'Inštalovať', INSTALL: 'Inštalovať',
REINSTALL: 'Inštalovať znova',
ABORTED: 'zrušené', ABORTED: 'zrušené',
FAILED: 'chybné', FAILED: 'chybné',
SUCCESSFUL: 'úspešné', SUCCESSFUL: 'úspešné',

View File

@@ -162,6 +162,7 @@ const sv: Translation = {
UPLOAD: 'Uppladdning', UPLOAD: 'Uppladdning',
DOWNLOAD: '{{N|n|n}}edladdning', DOWNLOAD: '{{N|n|n}}edladdning',
INSTALL: 'Installera', INSTALL: 'Installera',
REINSTALL: 'Återinstallera',
ABORTED: 'Avbruten', ABORTED: 'Avbruten',
FAILED: 'Misslyckades', FAILED: 'Misslyckades',
SUCCESSFUL: 'Lyckades', SUCCESSFUL: 'Lyckades',

View File

@@ -162,6 +162,7 @@ const tr: Translation = {
UPLOAD: 'Yükleme', UPLOAD: 'Yükleme',
DOWNLOAD: '{{İ|i|i}}İndirme', DOWNLOAD: '{{İ|i|i}}İndirme',
INSTALL: 'Düzenlemek', INSTALL: 'Düzenlemek',
REINSTALL: 'Yeniden düzenlemek',
ABORTED: 'iptal edildi', ABORTED: 'iptal edildi',
FAILED: 'başarısız', FAILED: 'başarısız',
SUCCESSFUL: 'başarılı', SUCCESSFUL: 'başarılı',

View File

@@ -13,7 +13,7 @@ const router = createBrowserRouter(
createRoutesFromElements(<Route path="/*" element={<App />} />) createRoutesFromElements(<Route path="/*" element={<App />} />)
); );
createRoot(document.getElementById('root') as HTMLElement).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<RouterProvider router={router} /> <RouterProvider router={router} />
</StrictMode> </StrictMode>

View File

@@ -29,10 +29,10 @@ export const updateValue =
export const updateValueDirty = export const updateValueDirty =
( (
origData, origData: unknown,
dirtyFlags: string[], dirtyFlags: string[],
setDirtyFlags: React.Dispatch<React.SetStateAction<string[]>>, setDirtyFlags: React.Dispatch<React.SetStateAction<string[]>>,
updateDataValue: (unknown) => void updateDataValue: (value: unknown) => void
) => ) =>
(event: React.ChangeEvent<HTMLInputElement>) => { (event: React.ChangeEvent<HTMLInputElement>) => {
const updated_value = extractEventValue(event); const updated_value = extractEventValue(event);

View File

@@ -1,31 +1,108 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", // Target modern browsers for better performance
"target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["node"], // Optimized library selection
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node", "vite/client"],
// JavaScript handling
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "checkJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true, // Module system optimized for Vite
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"composite": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
// Emit configuration
"noEmit": true, "noEmit": true,
"useUnknownInCatchVariables": false, "declaration": false,
"declarationMap": false,
"sourceMap": false,
// React/JSX configuration
"jsx": "react-jsx", "jsx": "react-jsx",
"noImplicitAny": false, "jsxImportSource": "react",
"baseUrl": "src",
// Strict type checking for better code quality
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
// Additional checks
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"useUnknownInCatchVariables": true,
// Performance optimizations
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
// Path mapping for cleaner imports
"baseUrl": ".",
"paths": { "paths": {
"@": ["src"], "@/*": ["src/*"],
"@/*": ["src/*"] "@/components/*": ["src/components/*"],
"@/utils/*": ["src/utils/*"],
"@/types/*": ["src/types/*"],
"@/hooks/*": ["src/hooks/*"],
"@/services/*": ["src/services/*"],
"@/assets/*": ["src/assets/*"],
// Support for bare imports from src directory
"App": ["src/App"],
"AppRouting": ["src/AppRouting"],
"CustomTheme": ["src/CustomTheme"],
"SignIn": ["src/SignIn"],
"AuthenticatedRouting": ["src/AuthenticatedRouting"],
"env": ["src/env"],
"components": ["src/components"],
"contexts": ["src/contexts"],
"i18n": ["src/i18n"],
"utils": ["src/utils"],
"validators": ["src/validators"],
"types": ["src/types"],
"api": ["src/api"],
"app": ["src/app"],
// Wildcard patterns for subdirectories
"components/*": ["src/components/*"],
"contexts/*": ["src/contexts/*"],
"i18n/*": ["src/i18n/*"],
"utils/*": ["src/utils/*"],
"validators/*": ["src/validators/*"],
"types/*": ["src/types/*"],
"api/*": ["src/api/*"],
"app/*": ["src/app/*"]
} }
}, },
"include": ["src/**/*", "vite.config.ts"], "include": ["src/**/*", "vite.config.ts", "progmem-generator.js"],
"exclude": ["node_modules", "dist"] "exclude": [
"node_modules",
"dist",
"build",
".tsbuildinfo",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx"
],
"ts-node": {
"esm": true
}
} }

View File

@@ -1,16 +1,106 @@
import preact from '@preact/preset-vite'; import preact from '@preact/preset-vite';
import fs from 'fs';
import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer'; import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
// import viteImagemin from 'vite-plugin-imagemin'; import { Plugin } from 'vite';
import viteImagemin from 'vite-plugin-imagemin';
import viteTsconfigPaths from 'vite-tsconfig-paths'; import viteTsconfigPaths from 'vite-tsconfig-paths';
import zlib from 'zlib';
// @ts-expect-error - mock server doesn't have type declarations
import mockServer from '../mock-api/mockServer.js'; import mockServer from '../mock-api/mockServer.js';
export default defineConfig(({ command, mode }) => { // Plugin to display bundle size information
const bundleSizeReporter = (): Plugin => {
return {
name: 'bundle-size-reporter',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
writeBundle(options: any, bundle: any) {
console.log('\n📦 Bundle Size Report:');
console.log('='.repeat(50));
let totalSize = 0;
const files: Array<{ name: string; size: number; gzipSize?: number }> = [];
for (const [fileName, chunk] of Object.entries(
bundle as Record<string, unknown>
)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((chunk as any).type === 'chunk' || (chunk as any).type === 'asset') {
const filePath = path.join((options.dir as string) || 'dist', fileName);
let size = 0;
let gzipSize = 0;
try {
const stats = fs.statSync(filePath);
size = stats.size;
totalSize += size;
// Calculate gzip size
const fileContent = fs.readFileSync(filePath);
gzipSize = zlib.gzipSync(fileContent).length;
files.push({
name: fileName,
size,
gzipSize
});
} catch (error) {
console.warn(`Could not read file ${fileName}:`, error);
}
}
}
// Sort files by size (largest first)
files.sort((a, b) => b.size - a.size);
// Display individual file sizes
files.forEach((file) => {
const sizeKB = (file.size / 1024).toFixed(2);
const gzipKB = file.gzipSize ? (file.gzipSize / 1024).toFixed(2) : 'N/A';
console.log(
`📄 ${file.name.padEnd(30)} ${sizeKB.padStart(8)} KB (${gzipKB} KB gzipped)`
);
});
console.log('='.repeat(50));
console.log(`📊 Total Bundle Size: ${(totalSize / 1024).toFixed(2)} KB`);
// Calculate and display gzip total
const totalGzipSize = files.reduce(
(sum, file) => sum + (file.gzipSize || 0),
0
);
console.log(`🗜️ Total Gzipped Size: ${(totalGzipSize / 1024).toFixed(2)} KB`);
// Show compression ratio
const compressionRatio = (
((totalSize - totalGzipSize) / totalSize) *
100
).toFixed(1);
console.log(`📈 Compression Ratio: ${compressionRatio}%`);
console.log('='.repeat(50));
}
};
};
export default defineConfig(
({ command, mode }: { command: string; mode: string }) => {
if (command === 'serve') { if (command === 'serve') {
console.log('Preparing for standalone build with server, mode=' + mode); console.log('Preparing for standalone build with server, mode=' + mode);
return { return {
plugins: [preact(), viteTsconfigPaths(), mockServer()], plugins: [
preact({
// Keep dev tools enabled for development
devToolsEnabled: true,
prefreshEnabled: true
}),
viteTsconfigPaths(),
bundleSizeReporter(), // Add bundle size reporting
mockServer()
],
server: { server: {
open: true, open: true,
port: mode == 'production' ? 4173 : 3000, port: mode == 'production' ? 4173 : 3000,
@@ -23,6 +113,12 @@ export default defineConfig(({ command, mode }) => {
'/rest': 'http://localhost:3080', '/rest': 'http://localhost:3080',
'/gh': 'http://localhost:3080' // mock for GitHub API '/gh': 'http://localhost:3080' // mock for GitHub API
} }
},
// Optimize development builds
build: {
target: 'es2020',
minify: false, // Disable minification for faster dev builds
sourcemap: true // Enable source maps for debugging
} }
}; };
} }
@@ -30,9 +126,50 @@ export default defineConfig(({ command, mode }) => {
if (mode === 'hosted') { if (mode === 'hosted') {
console.log('Preparing for hosted build'); console.log('Preparing for hosted build');
return { return {
plugins: [preact(), viteTsconfigPaths()], plugins: [
preact({
// Enable Preact optimizations for hosted build
devToolsEnabled: false,
prefreshEnabled: false
}),
viteTsconfigPaths(),
bundleSizeReporter() // Add bundle size reporting
],
build: { build: {
chunkSizeWarningLimit: 1024 target: 'es2020',
chunkSizeWarningLimit: 512,
minify: 'terser',
cssMinify: true,
assetsInlineLimit: 4096,
terserOptions: {
compress: {
passes: 3,
drop_console: true,
drop_debugger: true,
dead_code: true,
unused: true
},
mangle: {
toplevel: true
},
ecma: 2020
},
rollupOptions: {
treeshake: {
moduleSideEffects: false
},
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
if (id.includes('preact')) {
return '@preact';
}
return 'vendor';
}
return undefined;
}
}
}
} }
}; };
} }
@@ -41,90 +178,154 @@ export default defineConfig(({ command, mode }) => {
return { return {
plugins: [ plugins: [
preact(), preact({
// Enable Preact optimizations
devToolsEnabled: false,
prefreshEnabled: false
}),
viteTsconfigPaths(), viteTsconfigPaths(),
// { // Enable image optimization for size reduction
// ...viteImagemin({ {
// verbose: false, ...viteImagemin({
// gifsicle: { verbose: false,
// optimizationLevel: 7, gifsicle: {
// interlaced: false optimizationLevel: 7,
// }, interlaced: false
// optipng: { },
// optimizationLevel: 7 optipng: {
// }, optimizationLevel: 7
// mozjpeg: { },
// quality: 20 mozjpeg: {
// }, quality: 20
// pngquant: { },
// quality: [0.8, 0.9], pngquant: {
// speed: 4 quality: [0.8, 0.9],
// }, speed: 4
// svgo: { },
// plugins: [ svgo: {
// { plugins: [
// name: 'removeViewBox' {
// }, name: 'removeViewBox'
// { },
// name: 'removeEmptyAttrs', {
// active: false name: 'removeEmptyAttrs',
// } active: false
// ] }
// } ]
// }), }
// enforce: 'pre' }),
// }, enforce: 'pre'
},
visualizer({ visualizer({
template: 'treemap', // or sunburst template: 'treemap', // or sunburst
open: false, open: false,
gzipSize: true, gzipSize: true,
brotliSize: true, brotliSize: true,
filename: '../analyse.html' // will be saved in project's root filename: '../analyse.html' // will be saved in project's root
}) }),
bundleSizeReporter() // Add bundle size reporting
], ],
build: { build: {
// target: 'es2022', // Target modern browsers for smaller bundles
chunkSizeWarningLimit: 1024, target: 'es2020',
chunkSizeWarningLimit: 512,
minify: 'terser', minify: 'terser',
// Enable CSS minification
cssMinify: true,
// Optimize asset handling
assetsInlineLimit: 4096, // Inline small assets
terserOptions: { terserOptions: {
compress: { compress: {
passes: 4, passes: 6,
arrows: true, arrows: true,
drop_console: true, drop_console: true,
drop_debugger: true, drop_debugger: true,
sequences: true sequences: true
// Additional aggressive compression options
// dead_code: true,
// hoist_funs: true,
// hoist_vars: true,
// if_return: true,
// join_vars: true,
// loops: true,
// pure_getters: true,
// reduce_vars: true,
// side_effects: false,
// switches: true,
// unsafe: true,
// unsafe_arrows: true,
// unsafe_comps: true,
// unsafe_Function: true,
// unsafe_math: true,
// unsafe_proto: true,
// unsafe_regexp: true,
// unsafe_undefined: true,
// unused: true
}, },
mangle: { mangle: {
// toplevel: true toplevel: true, // Enable top-level mangling
// module: true module: true // Enable module mangling
// properties: { // properties: {
// regex: /^_/ // regex: /^_/ // Mangle properties starting with _
// } // }
}, },
ecma: 5, ecma: 2020, // Updated to modern ECMAScript
enclose: false, enclose: false,
keep_classnames: false, keep_classnames: false,
keep_fnames: false, keep_fnames: false,
ie8: false, ie8: false,
module: false, module: false,
safari10: false, safari10: false,
toplevel: false toplevel: true // Enable top-level optimization
}, },
rollupOptions: { rollupOptions: {
// Enable tree shaking
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
tryCatchDeoptimization: false
},
output: { output: {
// Optimize chunk naming for better caching
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks(id: string) { manualChunks(id: string) {
if (id.includes('node_modules')) { if (id.includes('node_modules')) {
// creating a chunk to react routes deps. Reducing the vendor chunk size // More granular chunk splitting for better caching
if (id.includes('react-router')) { if (id.includes('react-router')) {
return '@react-router'; return '@react-router';
} }
if (id.includes('preact')) {
return '@preact';
}
if (id.includes('uuid')) {
return '@uuid';
}
if (id.includes('axios') || id.includes('fetch')) {
return '@http';
}
if (id.includes('lodash') || id.includes('ramda')) {
return '@utils';
}
return 'vendor'; return 'vendor';
} }
// Split large application modules
if (id.includes('components/')) {
return 'components';
} }
if (id.includes('pages/') || id.includes('routes/')) {
return 'pages';
}
return undefined;
},
// Enable source maps for debugging (optional)
sourcemap: false // Disable for production to save space
} }
} }
} }
}; };
}); }
);

View File

@@ -1,33 +1,24 @@
// used to simulate // Mock server for development
// - file uploads // Simulates file uploads and EventSource (SSE) for log messages
// - EventSource (SSE) for log messages
import formidable from 'formidable'; import formidable from 'formidable';
function pad(number) { // Optimized padding function
let r = String(number); const pad = (number) => String(number).padStart(2, '0');
if (r.length === 1) {
r = '0' + r;
}
return r;
}
// e.g. 2024-03-29 07:02:37.856 // Cached date formatter to avoid prototype pollution
Date.prototype.toISOString = function () { const formatDate = (date) => {
return ( const year = date.getUTCFullYear();
this.getUTCFullYear() + const month = pad(date.getUTCMonth() + 1);
'-' + const day = pad(date.getUTCDate());
pad(this.getUTCMonth() + 1) + const hours = pad(date.getUTCHours());
'-' + const minutes = pad(date.getUTCMinutes());
pad(this.getUTCDate()) + const seconds = pad(date.getUTCSeconds());
' ' + const milliseconds = String((date.getUTCMilliseconds() / 1000).toFixed(3)).slice(
pad(this.getUTCHours()) + 2,
':' + 5
pad(this.getUTCMinutes()) +
':' +
pad(this.getUTCSeconds()) +
'.' +
String((this.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5)
); );
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
}; };
export default () => { export default () => {
@@ -35,97 +26,129 @@ export default () => {
name: 'vite:mockserver', name: 'vite:mockserver',
configureServer: async (server) => { configureServer: async (server) => {
server.middlewares.use(async (req, res, next) => { server.middlewares.use(async (req, res, next) => {
// catch any file uploads // Handle file uploads
if (req.url.startsWith('/rest/uploadFile')) { if (req.url.startsWith('/rest/uploadFile')) {
// show progress const fileSize = parseInt(req.headers['content-length'] || '0', 10);
let progress = 0; let progress = 0;
const file_size = req.headers['content-length'];
console.log('File size: ' + file_size); // Track upload progress
req.on('data', async (chunk) => { req.on('data', (chunk) => {
progress += chunk.length; progress += chunk.length;
const percentage = (progress / file_size) * 100; if (fileSize > 0) {
console.log(`Progress: ${Math.round(percentage)}%`); const percentage = Math.round((progress / fileSize) * 100);
// await new Promise((resolve) => setTimeout(() => resolve(), 3000)); // slow it down console.log(`Upload progress: ${percentage}%`);
}
}); });
const form = formidable({});
let fields;
let files;
try { try {
[fields, files] = await form.parse(req); const form = formidable({
} catch (err) { maxFileSize: 50 * 1024 * 1024, // 50MB limit
console.error('Not json form content'); keepExtensions: true
res.writeHead(err.httpCode || 400, {
'Content-Type': 'text/plain'
}); });
res.end(String(err));
const [fields, files] = await form.parse(req);
if (Object.keys(files).length === 0) {
res.statusCode = 400;
res.end('No file uploaded');
return; return;
} }
// only process when we have a file const uploadedFile = files.file[0];
if (Object.keys(files).length > 0) { const fileName = uploadedFile.originalFilename;
const uploaded_file = files.file[0]; const fileExtension = fileName
const file_name = uploaded_file.originalFilename; .substring(fileName.lastIndexOf('.') + 1)
const file_extension = file_name.substring( .toLowerCase();
file_name.lastIndexOf('.') + 1
console.log(
`File uploaded: ${fileName} (${fileExtension}, ${fileSize} bytes)`
); );
console.log('Filename: ' + file_name); // Validate file extension
console.log('Extension: ' + file_extension); const validExtensions = new Set(['bin', 'json', 'md5']);
console.log('File size: ' + file_size); if (!validExtensions.has(fileExtension)) {
res.statusCode = 406;
res.end('Invalid file extension');
return;
}
if (file_extension === 'bin' || file_extension === 'json') { // Handle different file types
console.log('File uploaded successfully!'); if (fileExtension === 'md5') {
} else if (file_extension === 'md5') { res.setHeader('Content-Type', 'application/json');
console.log('MD5 hash generated successfully!');
res.end( res.end(
JSON.stringify({ JSON.stringify({
md5: 'ef4304fc4d9025a58dcf25d71c882d2c' md5: 'ef4304fc4d9025a58dcf25d71c882d2c'
}) })
); );
} else { } else {
res.statusCode = 406; console.log('File uploaded successfully!');
console.log('Invalid file extension!');
}
}
res.end(); res.end();
} }
} catch (err) {
console.error('Upload error:', err.message);
res.statusCode = err.httpCode || 400;
res.setHeader('Content-Type', 'text/plain');
res.end(err.message);
}
}
// SSE Eventsource // Handle Server-Sent Events (SSE) for log streaming
else if (req.url.startsWith('/es/log')) { else if (req.url.startsWith('/es/log')) {
// Set SSE headers
res.writeHead(200, { res.writeHead(200, {
Connection: 'keep-alive', Connection: 'keep-alive',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream' 'Content-Type': 'text/event-stream',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
}); });
let count = 0; let messageCount = 0;
const interval = setInterval(() => { const logLevels = [3, 4, 5, 6, 7, 8]; // Different log levels
let message = 'message #' + count; const logNames = ['system', 'ems', 'wifi', 'mqtt', 'ntp', 'api'];
if (count % 6 === 1) {
const sendLogMessage = () => {
const level = logLevels[messageCount % logLevels.length];
const name = logNames[messageCount % logNames.length];
let message = `Log message #${messageCount}`;
// Add long message every 6th message
if (messageCount % 6 === 1) {
message += message +=
' that is a long message that will be wrapped, to test if it gets truncated'; ' - This is a longer message to test text wrapping and truncation behavior in the UI';
} }
const data = {
t: new Date().toISOString(), const logData = {
l: 3 + (count % 6), t: formatDate(new Date()),
i: count, l: level,
n: 'system', i: messageCount,
n: name,
m: message m: message
}; };
count++;
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
// if client closes connection res.write(`data: ${JSON.stringify(logData)}\n\n`);
res.on('close', () => { messageCount++;
console.log('Closing ES connection'); };
// Send initial message
sendLogMessage();
// Set up interval for periodic messages
const interval = setInterval(sendLogMessage, 1000);
// Clean up on connection close
const cleanup = () => {
console.log('SSE connection closed');
clearInterval(interval); clearInterval(interval);
if (!res.destroyed) {
res.end(); res.end();
}); }
};
res.on('close', cleanup);
res.on('error', cleanup);
} else { } else {
next(); // move on to the next middleware function in chain next(); // Continue to next middleware
} }
}); });
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "mock-api", "name": "mock-api",
"version": "3.7.2", "version": "3.7.3",
"description": "mock api for EMS-ESP", "description": "mock api for EMS-ESP",
"author": "proddy, emsesp.org", "author": "proddy, emsesp.org",
"license": "MIT", "license": "MIT",
@@ -15,5 +15,5 @@
"itty-router": "^5.0.22", "itty-router": "^5.0.22",
"prettier": "^3.6.2" "prettier": "^3.6.2"
}, },
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34" "packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8"
} }

View File

@@ -116,17 +116,15 @@ let system_status = {
let DEV_VERSION_IS_UPGRADEABLE: boolean; let DEV_VERSION_IS_UPGRADEABLE: boolean;
let STABLE_VERSION_IS_UPGRADEABLE: boolean; let STABLE_VERSION_IS_UPGRADEABLE: boolean;
let THIS_VERSION: string; let THIS_VERSION: string;
let version_test: number;
let LATEST_STABLE_VERSION = '3.7.2'; let LATEST_STABLE_VERSION = '3.7.2';
let LATEST_DEV_VERSION = '3.7.3-dev.6'; let LATEST_DEV_VERSION = '3.7.3-dev.6';
// scenarios for testing versioning // scenarios for testing versioning
version_test = 0; // on latest stable, or switch to dev let version_test = 0; // on latest stable, or switch to dev
// version_test = 1; // on latest dev, or switch back to stable // let version_test = 1; // on latest dev, or switch back to stable
// version_test = 2; // upgrade an older stable to latest stable or switch to latest dev // let version_test = 2; // upgrade an older stable to latest stable or switch to latest dev
// version_test = 3; // upgrade dev to latest, or switch to stable // let version_test = 3; // upgrade dev to latest, or switch to stable
// version_test = 4; // downgrade to an older dev, or switch back to stable // let version_test = 4; // downgrade to an older dev, or switch back to stable
switch (version_test as number) { switch (version_test as number) {
case 0: case 0:
@@ -4380,78 +4378,124 @@ router
function deviceData(id: number) { function deviceData(id: number) {
if (id == 1) { if (id == 1) {
return new Response(encoder.encode(emsesp_devicedata_1), { headers }); return new Response(encoder.encode(emsesp_devicedata_1) as BodyInit, {
headers
});
} }
if (id == 2) { if (id == 2) {
return new Response(encoder.encode(emsesp_devicedata_2), { headers }); return new Response(encoder.encode(emsesp_devicedata_2) as BodyInit, {
headers
});
} }
if (id == 3) { if (id == 3) {
return new Response(encoder.encode(emsesp_devicedata_3), { headers }); return new Response(encoder.encode(emsesp_devicedata_3) as BodyInit, {
headers
});
} }
if (id == 4) { if (id == 4) {
return new Response(encoder.encode(emsesp_devicedata_4), { headers }); return new Response(encoder.encode(emsesp_devicedata_4) as BodyInit, {
headers
});
} }
if (id == 5) { if (id == 5) {
return new Response(encoder.encode(emsesp_devicedata_5), { headers }); return new Response(encoder.encode(emsesp_devicedata_5) as BodyInit, {
headers
});
} }
if (id == 6) { if (id == 6) {
return new Response(encoder.encode(emsesp_devicedata_6), { headers }); return new Response(encoder.encode(emsesp_devicedata_6) as BodyInit, {
headers
});
} }
if (id == 7) { if (id == 7) {
return new Response(encoder.encode(emsesp_devicedata_7), { headers }); return new Response(encoder.encode(emsesp_devicedata_7) as BodyInit, {
headers
});
} }
if (id == 8) { if (id == 8) {
// test changing the selected flow temp on a Bosch Compress 7000i AW Heat Pump (Boiler/HP) // test changing the selected flow temp on a Bosch Compress 7000i AW Heat Pump (Boiler/HP)
emsesp_devicedata_8.nodes[4].v = Math.floor(Math.random() * 100); emsesp_devicedata_8.nodes[4].v = Math.floor(Math.random() * 100);
return new Response(encoder.encode(emsesp_devicedata_8), { headers }); return new Response(encoder.encode(emsesp_devicedata_8) as BodyInit, {
headers
});
} }
if (id == 9) { if (id == 9) {
return new Response(encoder.encode(emsesp_devicedata_9), { headers }); return new Response(encoder.encode(emsesp_devicedata_9) as BodyInit, {
headers
});
} }
if (id == 10) { if (id == 10) {
return new Response(encoder.encode(emsesp_devicedata_10), { headers }); return new Response(encoder.encode(emsesp_devicedata_10) as BodyInit, {
headers
});
} }
if (id == 11) { if (id == 11) {
return new Response(encoder.encode(emsesp_devicedata_11), { headers }); return new Response(encoder.encode(emsesp_devicedata_11) as BodyInit, {
headers
});
} }
if (id == 99) { if (id == 99) {
return new Response(encoder.encode(emsesp_devicedata_99), { headers }); return new Response(encoder.encode(emsesp_devicedata_99) as BodyInit, {
headers
});
} }
} }
function deviceEntities(id: number) { function deviceEntities(id: number) {
if (id == 1) { if (id == 1) {
return new Response(encoder.encode(emsesp_deviceentities_1), { headers }); return new Response(encoder.encode(emsesp_deviceentities_1) as BodyInit, {
headers
});
} }
if (id == 2) { if (id == 2) {
return new Response(encoder.encode(emsesp_deviceentities_2), { headers }); return new Response(encoder.encode(emsesp_deviceentities_2) as BodyInit, {
headers
});
} }
if (id == 3) { if (id == 3) {
return new Response(encoder.encode(emsesp_deviceentities_3), { headers }); return new Response(encoder.encode(emsesp_deviceentities_3) as BodyInit, {
headers
});
} }
if (id == 4) { if (id == 4) {
return new Response(encoder.encode(emsesp_deviceentities_4), { headers }); return new Response(encoder.encode(emsesp_deviceentities_4) as BodyInit, {
headers
});
} }
if (id == 5) { if (id == 5) {
return new Response(encoder.encode(emsesp_deviceentities_5), { headers }); return new Response(encoder.encode(emsesp_deviceentities_5) as BodyInit, {
headers
});
} }
if (id == 6) { if (id == 6) {
return new Response(encoder.encode(emsesp_deviceentities_6), { headers }); return new Response(encoder.encode(emsesp_deviceentities_6) as BodyInit, {
headers
});
} }
if (id == 7) { if (id == 7) {
return new Response(encoder.encode(emsesp_deviceentities_7), { headers }); return new Response(encoder.encode(emsesp_deviceentities_7) as BodyInit, {
headers
});
} }
if (id == 8) { if (id == 8) {
return new Response(encoder.encode(emsesp_deviceentities_8), { headers }); return new Response(encoder.encode(emsesp_deviceentities_8) as BodyInit, {
headers
});
} }
if (id == 9) { if (id == 9) {
return new Response(encoder.encode(emsesp_deviceentities_9), { headers }); return new Response(encoder.encode(emsesp_deviceentities_9) as BodyInit, {
headers
});
} }
if (id == 10) { if (id == 10) {
return new Response(encoder.encode(emsesp_deviceentities_10), { headers }); return new Response(encoder.encode(emsesp_deviceentities_10) as BodyInit, {
headers
});
} }
// not found, return empty // not found, return empty
return new Response(encoder.encode(emsesp_deviceentities_none), { headers }); return new Response(encoder.encode(emsesp_deviceentities_none) as BodyInit, {
headers
});
} }
// prepare dashboard data // prepare dashboard data
@@ -4558,8 +4602,8 @@ router
} }
// add temperature sensor data. no command c // add temperature sensor data. no command c
let sensor_data: any[] = []; if (emsesp_sensordata.ts.length > 0) {
sensor_data = emsesp_sensordata.ts.map((item, index) => ({ const sensor_data = emsesp_sensordata.ts.map((item, index) => ({
id: DeviceTypeUniqueID.TEMPERATURESENSOR_UID * 100 + index, id: DeviceTypeUniqueID.TEMPERATURESENSOR_UID * 100 + index,
dv: { dv: {
id: '00' + item.n, id: '00' + item.n,
@@ -4572,15 +4616,16 @@ router
t: DeviceType.TEMPERATURESENSOR, t: DeviceType.TEMPERATURESENSOR,
nodes: sensor_data nodes: sensor_data
}; };
// only add to dashboard if we have values
if ((dashboard_object.nodes ?? []).length > 0) {
dashboard_nodes.push(dashboard_object); dashboard_nodes.push(dashboard_object);
} }
// add analog sensor data. no command c // add analog sensor data. no command c
// remove disabled sensors first (t = 0) // remove disabled sensors first (t = 0) and create data in one pass
sensor_data = emsesp_sensordata.as.filter((item) => item.t !== 0); const enabledAnalogSensors = emsesp_sensordata.as.filter(
sensor_data = sensor_data.map((item, index) => ({ (item) => item.t !== 0
);
if (enabledAnalogSensors.length > 0) {
const sensor_data = enabledAnalogSensors.map((item, index) => ({
id: DeviceTypeUniqueID.ANALOGSENSOR_UID * 100 + index, id: DeviceTypeUniqueID.ANALOGSENSOR_UID * 100 + index,
dv: { dv: {
id: '00' + item.n, id: '00' + item.n,
@@ -4593,15 +4638,14 @@ router
t: DeviceType.ANALOGSENSOR, t: DeviceType.ANALOGSENSOR,
nodes: sensor_data nodes: sensor_data
}; };
// only add to dashboard if we have values
if ((dashboard_object.nodes ?? []).length > 0) {
dashboard_nodes.push(dashboard_object); dashboard_nodes.push(dashboard_object);
} }
// add the scheduler data // add the scheduler data
// filter emsesp_schedule with only if it has a name // filter emsesp_schedule with only if it has a name and create data in one pass
let scheduler_data = emsesp_schedule.schedule.filter((item) => item.name); const namedSchedules = emsesp_schedule.schedule.filter((item) => item.name);
let scheduler_data2 = scheduler_data.map((item, index) => ({ if (namedSchedules.length > 0) {
const scheduler_data = namedSchedules.map((item, index) => ({
id: DeviceTypeUniqueID.SCHEDULER_UID * 100 + index, id: DeviceTypeUniqueID.SCHEDULER_UID * 100 + index,
dv: { dv: {
id: '00' + item.name, id: '00' + item.name,
@@ -4613,10 +4657,8 @@ router
dashboard_object = { dashboard_object = {
id: DeviceTypeUniqueID.SCHEDULER_UID, id: DeviceTypeUniqueID.SCHEDULER_UID,
t: DeviceType.SCHEDULER, t: DeviceType.SCHEDULER,
nodes: scheduler_data2 nodes: scheduler_data
}; };
// only add to dashboard if we have values
if ((dashboard_object.nodes ?? []).length > 0) {
dashboard_nodes.push(dashboard_object); dashboard_nodes.push(dashboard_object);
} }
} else { } else {
@@ -4660,8 +4702,12 @@ router
}; };
// console.log('dashboardData: ', dashboardData); // console.log('dashboardData: ', dashboardData);
// Clear references to help with garbage collection
dashboard_nodes = [];
dashboard_object = {};
// return dashboard_data; // if not using msgpack // return dashboard_data; // if not using msgpack
return new Response(encoder.encode(dashboardData), { headers }); // msgpack it return new Response(encoder.encode(dashboardData) as BodyInit, { headers }); // msgpack it
}) })
// Customizations // Customizations
@@ -4852,10 +4898,12 @@ router
} else { } else {
if (as.deleted) { if (as.deleted) {
emsesp_sensordata.as[objIndex].d = true; emsesp_sensordata.as[objIndex].d = true;
var filtered = emsesp_sensordata.as.filter(function (value, index, arr) { // Remove deleted items in-place to avoid creating new arrays
return !value.d; for (let i = emsesp_sensordata.as.length - 1; i >= 0; i--) {
}); if (emsesp_sensordata.as[i].d) {
emsesp_sensordata.as = filtered; emsesp_sensordata.as.splice(i, 1);
}
}
} else { } else {
emsesp_sensordata.as[objIndex].n = as.name; emsesp_sensordata.as[objIndex].n = as.name;
emsesp_sensordata.as[objIndex].f = as.factor; emsesp_sensordata.as[objIndex].f = as.factor;

View File

@@ -197,7 +197,7 @@ extra_scripts =
build_flags = build_flags =
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_STANDALONE -DEMSESP_TEST
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.2-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\" -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
-std=gnu++17 -Og -ggdb -std=gnu++17 -Og -ggdb
build_unflags = -std=gnu++11 -std=gnu++14 build_unflags = -std=gnu++11 -std=gnu++14
build_type = debug build_type = debug

View File

@@ -1426,3 +1426,11 @@ Roomparams
Roomdata Roomdata
Roomschedule Roomschedule
dewtemp dewtemp
chefhat
sofasingle
bowlmix
bedsingle
beddouble
teddybear
washingmachine
switchprogram

View File

@@ -39,6 +39,8 @@ rm -f ./src/core/modbus_entity_parameters.hpp ./docs/dump_entities.csv
echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_entities.csv echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_entities.csv
cat ./docs/dump_entities.csv | python3 ./scripts/update_modbus_registers.py > ./src/core/modbus_entity_parameters.hpp cat ./docs/dump_entities.csv | python3 ./scripts/update_modbus_registers.py > ./src/core/modbus_entity_parameters.hpp
exit 0
# regenerate dump_entities.csv but without the Modbus entity parameters # regenerate dump_entities.csv but without the Modbus entity parameters
make clean make clean
make -s ARGS=-DEMSESP_STANDALONE make -s ARGS=-DEMSESP_STANDALONE

View File

@@ -211,7 +211,7 @@ for entity in entities:
if int(entity["modbus count"]) <= 0: if int(entity["modbus count"]) <= 0:
raise Exception('Entity "' + entity_dev_name + ' (' + entity_shortname + ')' + raise Exception('Entity "' + entity_dev_name + ' (' + entity_shortname + ')' +
'" does not have a size - string sizes need to be added manually to update_modbus_registers.py/string_sizes') '" does not have a size - string sizes need to be added manually to update_modbus_registers.py/string_sizes[]')
# if entity["modbus count"] == "0": # if entity["modbus count"] == "0":
# print("ignoring " + entity_dev_name + " - it has a register length of zero") # print("ignoring " + entity_dev_name + " - it has a register length of zero")

View File

@@ -203,7 +203,7 @@
{163, DeviceType::WATER, "SM100, MS100", DeviceFlags::EMS_DEVICE_FLAG_SM100}, {163, DeviceType::WATER, "SM100, MS100", DeviceFlags::EMS_DEVICE_FLAG_SM100},
{164, DeviceType::WATER, "SM200, MS200", DeviceFlags::EMS_DEVICE_FLAG_SM100}, {164, DeviceType::WATER, "SM200, MS200", DeviceFlags::EMS_DEVICE_FLAG_SM100},
{248, DeviceType::MIXER, "HM210", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS}, {248, DeviceType::MIXER, "HM210", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{17, DeviceType::CONNECT, "MX400", DeviceFlags::EMS_DEVICE_FLAG_NONE} // 0x50 Wirelss Base {17, DeviceType::CONNECT, "MX400", DeviceFlags::EMS_DEVICE_FLAG_NONE} // 0x50 Wireless Base
// {157, DeviceType::THERMOSTAT, "RC120", DeviceFlags::EMS_DEVICE_FLAG_CR120} // {157, DeviceType::THERMOSTAT, "RC120", DeviceFlags::EMS_DEVICE_FLAG_CR120}
#endif #endif

View File

@@ -29,7 +29,7 @@ static_assert(uuid::console::thread_safe, "uuid-console must be thread-safe");
namespace emsesp { namespace emsesp {
// Static member definitions // Static member definitions
std::deque<std::unique_ptr<EMSdevice>> EMSESP::emsdevices{}; std::vector<std::unique_ptr<EMSdevice>> EMSESP::emsdevices{};
std::vector<EMSESP::Device_record> EMSESP::device_library_; std::vector<EMSESP::Device_record> EMSESP::device_library_;
uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN}; uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN};
uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; uint16_t EMSESP::watch_id_ = WATCH_ID_NONE;

View File

@@ -222,7 +222,7 @@ class EMSESP {
static void scan_devices(); static void scan_devices();
static void clear_all_devices(); static void clear_all_devices();
static std::deque<std::unique_ptr<EMSdevice>> emsdevices; static std::vector<std::unique_ptr<EMSdevice>> emsdevices;
// services // services
static Mqtt mqtt_; static Mqtt mqtt_;

View File

@@ -698,8 +698,8 @@ std::string Helpers::toUpper(std::string const & s) {
} }
// capitalizes one UTF-8 character in char array // capitalizes one UTF-8 character in char array
// works with Latin1 (1 byte), Polish amd some other (2 bytes) characters // works with Latin1 (1 byte), Polish and other (2 bytes) characters
// TODO add special characters that occur in other supported languages // supports special characters for all 11 supported languages: EN, DE, NL, SV, PL, NO, FR, TR, IT, SK, CZ
#if defined(EMSESP_STANDALONE) #if defined(EMSESP_STANDALONE)
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtype-limits" #pragma GCC diagnostic ignored "-Wtype-limits"
@@ -714,23 +714,77 @@ void Helpers::CharToUpperUTF8(char * c) {
if ((p_v >= (char)0xA0) && (p_v <= (char)0xBE)) { if ((p_v >= (char)0xA0) && (p_v <= (char)0xBE)) {
*p -= 0x20; *p -= 0x20;
} }
// Additional special characters for supported languages
switch (p_v) {
case (char)0xA0: // à -> À
case (char)0xA1: // á -> Á
case (char)0xA2: // â -> Â
case (char)0xA3: // ã -> Ã
case (char)0xA4: // ä -> Ä (German, Swedish)
case (char)0xA5: // å -> Å (Swedish, Norwegian)
case (char)0xA6: // æ -> Æ (Norwegian)
case (char)0xA7: // ç -> Ç (French, Turkish)
case (char)0xA8: // è -> È (French, Italian)
case (char)0xA9: // é -> É (French, Italian)
case (char)0xAA: // ê -> Ê (French)
case (char)0xAB: // ë -> Ë (French)
case (char)0xAC: // ì -> Ì (Italian)
case (char)0xAD: // í -> Í (Slovak, Czech)
case (char)0xAE: // î -> Î (French)
case (char)0xAF: // ï -> Ï (French)
case (char)0xB0: // ð -> Ð (Icelandic)
case (char)0xB1: // ñ -> Ñ (Spanish)
case (char)0xB2: // ò -> Ò (Italian)
case (char)0xB3: // ó -> Ó (Slovak, Czech)
case (char)0xB4: // ô -> Ô (French, Slovak)
case (char)0xB5: // õ -> Õ (Portuguese)
case (char)0xB6: // ö -> Ö (German, Swedish, Turkish)
case (char)0xB8: // ø -> Ø (Norwegian)
case (char)0xB9: // ù -> Ù (French, Italian)
case (char)0xBA: // ú -> Ú (Slovak, Czech)
case (char)0xBB: // û -> Û (French)
case (char)0xBC: // ü -> Ü (German, French, Turkish)
case (char)0xBD: // ý -> Ý (Slovak, Czech)
case (char)0xBE: // þ -> Þ (Icelandic)
case (char)0xBF: // ÿ -> Ÿ (French)
*p -= 0x20;
break;
}
break; break;
case (char)0xC4: case (char)0xC4:
switch (p_v) { switch (p_v) {
case (char)0x85: //ą (0xC4,0x85) -> Ą (0xC4,0x84) case (char)0x85: //ą (0xC4,0x85) -> Ą (0xC4,0x84) (Polish)
case (char)0x87: //ć (0xC4,0x87) -> Ć (0xC4,0x86) case (char)0x87: //ć (0xC4,0x87) -> Ć (0xC4,0x86) (Polish)
case (char)0x99: //ę (0xC4,0x99) -> Ę (0xC4,0x98) case (char)0x8D: //č (0xC4,0x8D) -> Č (0xC4,0x8C) (Slovak, Czech)
case (char)0x8F: //ď (0xC4,0x8F) -> Ď (0xC4,0x8E) (Slovak, Czech)
case (char)0x9F: //ğ (0xC4,0x9F) -> Ğ (0xC4,0x9E) (Turkish)
case (char)0x99: //ę (0xC4,0x99) -> Ę (0xC4,0x98) (Polish)
case (char)0x9B: //ě (0xC4,0x9B) -> Ě (0xC4,0x9A) (Czech)
case (char)0xAF: //ı (0xC4,0xAF) -> I (0xC4,0xAE) (Turkish)
case (char)0xB1: //ı (0xC4,0xB1) -> I (0xC4,0xB0) (Turkish)
case (char)0xB3: //ij (0xC4,0xB3) -> IJ (0xC4,0xB2) (Dutch)
*p -= 1; *p -= 1;
break; break;
} }
break; break;
case (char)0xC5: case (char)0xC5:
switch (p_v) { switch (p_v) {
case (char)0x82: //ł (0xC5,0x82) -> Ł (0xC5,0x81) case (char)0x81: //ł (0xC5,0x81) -> Ł (0xC5,0x80) (Polish)
case (char)0x84: //ń (0xC5,0x84) -> Ń (0xC5,0x83) case (char)0x82: //ł (0xC5,0x82) -> Ł (0xC5,0x81) (Polish)
case (char)0x9B: //ś (0xC5,0x9B) -> Ś (0xC5,0x9A) case (char)0x83: //ń (0xC5,0x83) -> Ń (0xC5,0x82) (Polish)
case (char)0xBA: //ź (0xC5,0xBA) -> Ź (0xC5,0xB9) case (char)0x84: //ń (0xC5,0x84) -> Ń (0xC5,0x83) (Polish)
case (char)0xBC: //ż (0xC5,0xBC) -> Ż (0xC5,0xBB) case (char)0x88: //ň (0xC5,0x88) -> Ň (0xC5,0x87) (Slovak, Czech)
case (char)0x95: //ŕ (0xC5,0x95) -> Ŕ (0xC5,0x94) (Slovak)
case (char)0x99: //ř (0xC5,0x99) -> Ř (0xC5,0x98) (Czech)
case (char)0x9A: //ś (0xC5,0x9A) -> Ś (0xC5,0x99) (Polish)
case (char)0x9B: //ś (0xC5,0x9B) -> Ś (0xC5,0x9A) (Polish)
case (char)0x9F: //ş (0xC5,0x9F) -> Ş (0xC5,0x9E) (Turkish)
case (char)0xA1: //š (0xC5,0xA1) -> Š (0xC5,0xA0) (Slovak, Czech)
case (char)0xA5: //ť (0xC5,0xA5) -> Ť (0xC5,0xA4) (Slovak, Czech)
case (char)0xAF: //ů (0xC5,0xAF) -> Ů (0xC5,0xAE) (Czech)
case (char)0xBA: //ź (0xC5,0xBA) -> Ź (0xC5,0xB9) (Polish)
case (char)0xBC: //ż (0xC5,0xBC) -> Ż (0xC5,0xBB) (Polish)
case (char)0xBE: //ž (0xC5,0xBE) -> Ž (0xC5,0xBD) (Slovak, Czech)
*p -= 1; *p -= 1;
break; break;
} }

View File

@@ -378,27 +378,28 @@ const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_ma
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomsensor), 199, 1), // roomsensor REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomsensor), 199, 1), // roomsensor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatup), 200, 1), // heatup REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatup), 200, 1), // heatup
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(mode), 0, 1), // mode REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(mode), 0, 1), // mode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTemp), 1, 1), // settemp REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(modetype), 1, 1), // modetype
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTempLow), 2, 1), // settemplow REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTemp), 2, 1), // settemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircMode), 3, 1), // circmode REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTempLow), 3, 1), // settemplow
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwChargeDuration), 4, 1), // chargeduration REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircMode), 4, 1), // circmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCharge), 5, 1), // charge REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwChargeDuration), 5, 1), // chargeduration
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwExtra), 6, 1), // extra REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCharge), 6, 1), // charge
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfecting), 7, 1), // disinfecting REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwExtra), 7, 1), // extra
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectDay), 8, 1), // disinfectday REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfecting), 8, 1), // disinfecting
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectTime), 9, 1), // disinfecttime REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectDay), 9, 1), // disinfectday
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeating), 10, 1), // dailyheating REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectTime), 10, 1), // disinfecttime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeatTime), 11, 1), // dailyheattime REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeating), 11, 1), // dailyheating
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwProgMode), 12, 1), // progmode REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeatTime), 12, 1), // dailyheattime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircProg), 13, 1), // circprog REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwProgMode), 13, 1), // progmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectHour), 14, 1), // disinfecthour REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircProg), 14, 1), // circprog
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwMaxTemp), 15, 1), // maxtemp REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectHour), 15, 1), // disinfecthour
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwOneTimeKey), 16, 1), // onetimekey REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwMaxTemp), 16, 1), // maxtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(switchtime), 17, 8), // switchtime REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwOneTimeKey), 17, 1), // onetimekey
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwcircswitchtime), 25, 8), // circswitchtime REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(switchtime), 18, 8), // switchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(holidays), 33, 13), // holidays REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwcircswitchtime), 26, 8), // circswitchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(vacations), 46, 13), // vacations REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(holidays), 34, 13), // holidays
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwWhenModeOff), 59, 1), // whenmodeoff REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(vacations), 47, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwWhenModeOff), 60, 1), // whenmodeoff
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowTempHc), 0, 1), // flowtemphc REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowTempHc), 0, 1), // flowtemphc
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(valveStatus), 1, 1), // valvestatus REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(valveStatus), 1, 1), // valvestatus
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowSetTemp), 2, 1), // flowsettemp REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowSetTemp), 2, 1), // flowsettemp
@@ -523,6 +524,8 @@ const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_ma
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(flowTempHc), 1, 1), // flowtemphc REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(flowTempHc), 1, 1), // flowtemphc
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(status), 2, 1), // status REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(status), 2, 1), // status
REGISTER_MAPPING(dt::CONTROLLER, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 0, 13), // datetime REGISTER_MAPPING(dt::CONTROLLER, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 0, 13), // datetime
REGISTER_MAPPING(dt::CONNECT, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 0, 13), // datetime
REGISTER_MAPPING(dt::CONNECT, TAG_TYPE_DEVICE_DATA, FL_(outdoorTemp), 13, 1), // outdoortemp
REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setFlowTemp), 0, 1), // setflowtemp REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setFlowTemp), 0, 1), // setflowtemp
REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setBurnPow), 1, 1), // setburnpow REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setBurnPow), 1, 1), // setburnpow
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(flowTempVf), 0, 1), // flowtempvf REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(flowTempVf), 0, 1), // flowtempvf

View File

@@ -151,7 +151,7 @@ void Shower::loop() {
// turn off hot water to send a shot of cold // turn off hot water to send a shot of cold
void Shower::shower_alert_start() { void Shower::shower_alert_start() {
LOG_DEBUG("Shower Alert started"); LOG_DEBUG("Shower Alert started");
(void)Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "false", 9); (void)Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "false", DeviceValueTAG::TAG_DHW1);
doing_cold_shot_ = true; doing_cold_shot_ = true;
force_coldshot = false; force_coldshot = false;
alert_timer_start_ = uuid::get_uptime_sec(); // timer starts now alert_timer_start_ = uuid::get_uptime_sec(); // timer starts now
@@ -161,7 +161,7 @@ void Shower::shower_alert_start() {
void Shower::shower_alert_stop() { void Shower::shower_alert_stop() {
if (doing_cold_shot_) { if (doing_cold_shot_) {
LOG_DEBUG("Shower Alert stopped"); LOG_DEBUG("Shower Alert stopped");
(void)Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "true", 9); (void)Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "true", DeviceValueTAG::TAG_DHW1);
doing_cold_shot_ = false; doing_cold_shot_ = false;
force_coldshot = false; force_coldshot = false;
next_alert_ += shower_alert_trigger_; next_alert_ += shower_alert_trigger_;

View File

@@ -22,7 +22,7 @@
#include "shuntingYard.h" #include "shuntingYard.h"
// find tokens // find tokens - optimized to reduce string allocations
std::deque<Token> exprToTokens(const std::string & expr) { std::deque<Token> exprToTokens(const std::string & expr) {
std::deque<Token> tokens; std::deque<Token> tokens;
@@ -40,13 +40,14 @@ std::deque<Token> exprToTokens(const std::string & expr) {
if (*p) { if (*p) {
++p; ++p;
} }
auto s = std::string(b, p); // Use string_view to avoid unnecessary string copies
std::string_view s(b, p - b);
auto n = s.find("\"\""); auto n = s.find("\"\"");
while (n != std::string::npos) { while (n != std::string_view::npos) {
s.erase(n, 2); s.remove_prefix(n + 2);
n = s.find("\"\""); n = s.find("\"\"");
} }
tokens.emplace_back(Token::Type::String, s, -3); tokens.emplace_back(Token::Type::String, std::string(s), -3);
if (*p == '\0') { if (*p == '\0') {
--p; --p;
} }
@@ -225,11 +226,14 @@ std::deque<Token> exprToTokens(const std::string & expr) {
return tokens; return tokens;
} }
// sort tokens to RPN form // sort tokens to RPN form - optimized for memory usage
std::deque<Token> shuntingYard(const std::deque<Token> & tokens) { std::deque<Token> shuntingYard(const std::deque<Token> & tokens) {
std::deque<Token> queue; std::deque<Token> queue;
std::vector<Token> stack; std::vector<Token> stack;
// Reserve space for vector to reduce reallocations
stack.reserve(tokens.size() / 2);
// While there are tokens to be read: // While there are tokens to be read:
for (auto const & token : tokens) { for (auto const & token : tokens) {
// Read a token // Read a token

View File

@@ -135,7 +135,7 @@ void Connect::process_roomThermostat(std::shared_ptr<const Telegram> telegram) {
has_update(rc->dewtemp_, dt); has_update(rc->dewtemp_, dt);
} }
// gateway(0x48) W gateway(0x50), ?(0x0B42), data: 01 // icon in ofset 0 // gateway(0x48) W gateway(0x50), ?(0x0B42), data: 01 // icon in offset 0
// gateway(0x48) W gateway(0x50), ?(0x0B42), data: 00 4B 00 FC 00 63 00 68 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (offset 1) // gateway(0x48) W gateway(0x50), ?(0x0B42), data: 00 4B 00 FC 00 63 00 68 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (offset 1)
void Connect::process_roomThermostatName(std::shared_ptr<const Telegram> telegram) { void Connect::process_roomThermostatName(std::shared_ptr<const Telegram> telegram) {
auto rc = room_circuit(telegram->type_id - 0xB3D); auto rc = room_circuit(telegram->type_id - 0xB3D);

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.7.3-dev.20" #define EMSESP_APP_VERSION "3.7.3-dev.21"

View File

@@ -764,7 +764,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
test("boiler"); test("boiler");
// device type, command, data // device type, command, data
Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "false", 9); Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "false", DeviceValueTAG::TAG_DHW1);
ok = true; ok = true;
} }