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

View File

@@ -35,7 +35,7 @@
[![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 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)
**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",
"sdkconfig.*",
"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: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
project: true
}
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "EMS-ESP",
"version": "3.7.2",
"version": "3.7.3",
"description": "EMS-ESP WebUI",
"homepage": "https://emsesp.org",
"author": "proddy, emsesp.org",
@@ -45,23 +45,23 @@
},
"devDependencies": {
"@babel/core": "^7.28.4",
"@eslint/js": "^9.37.0",
"@eslint/js": "^9.38.0",
"@preact/compat": "^18.3.1",
"@preact/preset-vite": "^2.10.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-dom": "^19.2.2",
"concurrently": "^9.2.1",
"eslint": "^9.37.0",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.6.2",
"rollup-plugin-visualizer": "^6.0.5",
"terser": "^5.44.0",
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.11",
"vite-plugin-imagemin": "^0.6.1",
"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,
readFileSync,
readdirSync,
statSync,
unlinkSync
} from 'fs';
import mime from 'mime-types';
@@ -15,67 +16,79 @@ const INDENT = ' ';
const outputPath = '../src/ESP32React/WWWData.h';
const sourcePath = './dist';
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 = () =>
`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
const generateWWWClass =
() => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
// Bundle Statistics:
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB
// - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB
// - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}%
// - Generated on: ${new Date().toISOString()}
class WWWData {
${indent}public:
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`).join('\n')}
${indent.repeat(2)}}
${INDENT}public:
${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "${f.hash}");`).join('\n')}
${INDENT.repeat(2)}}
};
`;
function getFilesSync(dir, files = []) {
const getFilesSync = (dir, files = []) => {
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
const entryPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
getFilesSync(entryPath, files);
} else {
files.push(entryPath);
}
entry.isDirectory() ? getFilesSync(entryPath, files) : files.push(entryPath);
});
return files;
}
};
function cleanAndOpen(path) {
if (existsSync(path)) {
unlinkSync(path);
}
const cleanAndOpen = (path) => {
existsSync(path) && unlinkSync(path);
return createWriteStream(path, { flags: 'w+' });
}
};
const getFileType = (filePath) => {
const ext = filePath.split('.').pop().toLowerCase();
if (ext === 'js') return 'js';
if (ext === 'css') return 'css';
if (ext === 'html') return 'html';
if (ext === 'svg') return 'svg';
return 'other';
};
const writeFile = (relativeFilePath, buffer) => {
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
const variable = `ESP_REACT_DATA_${fileInfo.length}`;
const mimeType = mime.lookup(relativeFilePath);
var size = 0;
writeStream.write('const uint8_t ' + variable + '[] = {');
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
const fileType = getFileType(relativeFilePath);
let size = 0;
writeStream.write(`const uint8_t ${variable}[] = {`);
// create sha
const hashSum = crypto.createHash('sha256');
hashSum.update(zipBuffer);
const hash = hashSum.digest('hex');
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) {
writeStream.write('\n');
writeStream.write(indent);
writeStream.write('\n' + INDENT);
}
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ',');
writeStream.write('0x' + b.toString(16).toUpperCase().padStart(2, '0') + ',');
size++;
});
if (size % bytesPerLine) {
writeStream.write('\n');
}
size % bytesPerLine && writeStream.write('\n');
writeStream.write('};\n\n');
// Update bundle statistics
bundleStats[fileType].count++;
bundleStats[fileType].uncompressed += buffer.length;
bundleStats[fileType].compressed += zipBuffer.length;
fileInfo.push({
uri: '/' + relativeFilePath.replace(sep, '/'),
mimeType,
@@ -84,32 +97,52 @@ const writeFile = (relativeFilePath, buffer) => {
hash
});
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
totalSize += size;
};
// start
console.log('Generating ' + outputPath + ' from ' + sourcePath);
const includes = ARDUINO_INCLUDES;
const indent = INDENT;
console.log(`Generating ${outputPath} from ${sourcePath}`);
const fileInfo = [];
const writeStream = cleanAndOpen(resolve(outputPath));
// includes
writeStream.write(includes);
writeStream.write(ARDUINO_INCLUDES);
// process static files
const buildPath = resolve(sourcePath);
for (const filePath of getFilesSync(buildPath)) {
const readStream = readFileSync(filePath);
const relativeFilePath = relative(buildPath, filePath);
writeFile(relativeFilePath, readStream);
writeFile(relative(buildPath, filePath), readFileSync(filePath));
}
// add class
writeStream.write(generateWWWClass());
// 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="/devices/*" element={<Devices />} />
<Route path="/sensors/*" element={<Sensors />} />
<Route path="/status/*" element={<Status />} />
<Route path="/help/*" element={<Help />} />
<Route path="/*" element={<Navigate to="/" />} />
<Route path="/status/*" element={<Status />} />
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
<Route path="/status/activity" element={<Activity />} />
<Route path="/status/log" element={<SystemLog />} />
@@ -68,6 +67,8 @@ const AuthenticatedRouting = () => {
<Route path="/customentities" element={<CustomEntities />} />
</>
)}
<Route path="/*" element={<Navigate to="/" />} />
</Routes>
</Layout>
);

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ export const getDevVersion = () =>
cacheFor: 60 * 10 * 1000,
transform(response: { data: { name: string; published_at: string } }) {
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
};
}

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 {
decoder = new TextDecoder();
} catch (error) {}
let src;
let srcEnd;
let position = 0;
const EMPTY_ARRAY = [];
let strings = EMPTY_ARRAY;
let stringPosition = 0;
let currentUnpackr = {};
let currentStructures;
let srcString;
let srcStringStart = 0;
let srcStringEnd = 0;
let bundledStrings;
let referenceMap;
const currentExtensions = [];
let dataView;
const defaultOptions = {
useRecords: false,
mapsAsObjects: true
};
export class C1Type {}
export const C1 = new C1Type();
class C1Type {}
const C1 = new C1Type();
C1.name = 'MessagePack 0xC1';
let sequentialMode = false;
let inlineObjectReadThreshold = 2;
let readStruct, onLoadedStructures, onSaveState;
// no-eval build
let sequentialMode = false,
inlineObjectReadThreshold = 2,
readStruct,
onLoadedStructures,
onSaveState;
try {
new Function('');
} catch (error) {
// if eval variants are not supported, do not create inline object readers ever
inlineObjectReadThreshold = Infinity;
}
export class Unpackr {
constructor(options) {
if (options) {
@@ -50,19 +47,15 @@ export class Unpackr {
if (options.structures)
options.structures.sharedLength = options.structures.length;
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;
}
if (options.int64AsNumber) {
options.int64AsType = 'number';
}
if (options.int64AsNumber) options.int64AsType = 'number';
}
Object.assign(this, options);
}
unpack(source, options?: any) {
if (src) {
// re-entrant execution, save the state and restore it after we do this unpack
return saveState(() => {
clearSource();
return this
@@ -86,9 +79,6 @@ export class Unpackr {
strings = EMPTY_ARRAY;
bundledStrings = null;
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 {
dataView =
source.dataView ||
@@ -191,10 +181,10 @@ export class Unpackr {
return this.unpack(source, end);
}
}
export function getPosition() {
function getPosition() {
return position;
}
export function checkedRead(options: any) {
function checkedRead(options: any) {
try {
if (!currentUnpackr.trusted && !sequentialMode) {
const sharedLength = currentStructures.sharedLength || 0;
@@ -264,7 +254,7 @@ function restoreStructures() {
currentStructures.restoreStructures = null;
}
export function read() {
function read() {
let token = src[position++];
if (token < 0xa0) {
if (token < 0x80) {
@@ -589,7 +579,7 @@ const createSecondByteReader = (firstId, read0) =>
return structure.read();
};
export function loadStructures() {
function loadStructures() {
const loadedStructures = saveState(() => {
// save the state in case getStructures modifies our buffer
src = null;
@@ -605,9 +595,8 @@ var readFixedString = readStringJS;
var readString8 = readStringJS;
var readString16 = readStringJS;
var readString32 = readStringJS;
export let isNativeAccelerationEnabled = false;
export function setExtractor(extractStrings) {
let isNativeAccelerationEnabled = false;
function setExtractor(extractStrings) {
isNativeAccelerationEnabled = true;
readFixedString = readString(1);
readString8 = readString(2);
@@ -701,7 +690,7 @@ function readStringJS(length) {
return result;
}
export function readString(source, start, length) {
function readString(source, start, length) {
const existingSrc = src;
src = source;
position = start;
@@ -1065,7 +1054,7 @@ currentExtensions[0x70] = (data) => {
currentExtensions[0x73] = () => new Set(read());
export const typedArrays = [
const typedArrays = [
'Int8',
'Uint8',
'Uint8Clamped',
@@ -1177,44 +1166,20 @@ function saveState(callback) {
dataView = new DataView(src.buffer, src.byteOffset, src.byteLength);
return value;
}
export function clearSource() {
function clearSource() {
src = null;
referenceMap = null;
currentStructures = null;
}
export function addExtension(extension) {
function addExtension(extension) {
if (extension.unpack) currentExtensions[extension.type] = extension.unpack;
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++) {
mult10[i] = +('1e' + Math.floor(45.15 - i * 0.30103));
}
export const Decoder = Unpackr;
var defaultUnpackr = new Unpackr({ useRecords: false });
const defaultUnpackr = new Unpackr({ useRecords: false });
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 () => {
await writeEntities({
entities: entities
.filter((ei) => !ei.deleted)
.map((condensed_ei) => ({
.filter((ei: EntityItem) => !ei.deleted)
.map((condensed_ei: EntityItem) => ({
id: condensed_ei.id,
ram: condensed_ei.ram,
name: condensed_ei.name,
@@ -231,6 +231,7 @@ const CustomEntities = () => {
value_type: 0,
writeable: false,
deleted: false,
hide: false,
value: ''
});
setDialogOpen(true);
@@ -251,15 +252,17 @@ const CustomEntities = () => {
const renderEntity = () => {
if (!entities) {
return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />;
return (
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
);
}
return (
<Table
data={{
nodes: entities
.filter((ei) => !ei.deleted)
.sort((a, b) => a.name.localeCompare(b.name))
.filter((ei: EntityItem) => !ei.deleted)
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name))
}}
theme={entity_theme}
layout={{ custom: true }}

View File

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

View File

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

View File

@@ -54,7 +54,10 @@ const CustomizationsDialog = ({
}
}, [open, selectedItem]);
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') {
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 { Link } from 'react-router';
import { toast } from 'react-toastify';
@@ -76,35 +76,40 @@ const Dashboard = () => {
}
);
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
if (!selectedDashboardItem) {
return;
}
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
})
.catch((error: Error) => {
toast.error(error.message);
})
.finally(() => {
setDeviceValueDialogOpen(false);
setSelectedDashboardItem(undefined);
});
};
const deviceValueDialogSave = useCallback(
async (devicevalue: DeviceValue) => {
if (!selectedDashboardItem) {
return;
}
const id = selectedDashboardItem.id; // this is the parent ID
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
})
.catch((error: Error) => {
toast.error(error.message);
})
.finally(() => {
setDeviceValueDialogOpen(false);
setSelectedDashboardItem(undefined);
});
},
[selectedDashboardItem, sendDeviceValue, LL]
);
const dashboard_theme = useTheme({
Table: `
const dashboard_theme = useMemo(
() =>
useTheme({
Table: `
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
`,
BaseRow: `
BaseRow: `
font-size: 14px;
.td {
height: 28px;
}
`,
Row: `
Row: `
cursor: pointer;
background-color: #1e1e1e;
&:nth-of-type(odd) .td {
@@ -114,7 +119,7 @@ const Dashboard = () => {
background-color: #177ac9;
},
`,
BaseCell: `
BaseCell: `
&:nth-of-type(2) {
text-align: right;
}
@@ -122,12 +127,14 @@ const Dashboard = () => {
text-align: right;
}
`
});
}),
[]
);
const tree = useTree(
{ nodes: data.nodes },
{
onChange: undefined // not used but needed
onChange: () => {} // not used but needed
},
{
treeIcon: {
@@ -162,28 +169,31 @@ const Dashboard = () => {
: tree.fns.onRemoveAll(); // collapse tree
}, [parentNodes]);
const showType = (n?: string, t?: number) => {
// if we have a name show it
if (n) {
return n;
}
if (t) {
// otherwise pick translation based on type
switch (t) {
case DeviceType.CUSTOM:
return LL.CUSTOM_ENTITIES(0);
case DeviceType.ANALOGSENSOR:
return LL.ANALOG_SENSORS();
case DeviceType.TEMPERATURESENSOR:
return LL.TEMP_SENSORS();
case DeviceType.SCHEDULER:
return LL.SCHEDULER();
default:
break;
const showType = useCallback(
(n?: string, t?: number) => {
// if we have a name show it
if (n) {
return n;
}
}
return '';
};
if (t) {
// otherwise pick translation based on type
switch (t) {
case DeviceType.CUSTOM:
return LL.CUSTOM_ENTITIES(0);
case DeviceType.ANALOGSENSOR:
return LL.ANALOG_SENSORS();
case DeviceType.TEMPERATURESENSOR:
return LL.TEMP_SENSORS();
case DeviceType.SCHEDULER:
return LL.SCHEDULER();
default:
break;
}
}
return '';
},
[LL]
);
const showName = (di: DashboardItem) => {
if (di.id < 100) {
@@ -201,20 +211,24 @@ const Dashboard = () => {
if (di.dv) {
return <span>{di.dv.id.slice(2)}</span>;
}
return null;
};
const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask;
const editDashboardValue = (di: DashboardItem) => {
if (me.admin && di.dv?.c) {
setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true);
}
};
const editDashboardValue = useCallback(
(di: DashboardItem) => {
if (me.admin && di.dv?.c) {
setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true);
}
},
[me.admin]
);
const handleShowAll = (
event: React.MouseEvent<HTMLElement>,
_event: React.MouseEvent<HTMLElement>,
toggle: boolean | null
) => {
if (toggle !== null) {
@@ -225,7 +239,9 @@ const Dashboard = () => {
const renderContent = () => {
if (!data) {
return <FormLoader onRetry={fetchDashboard} errorMessage={error?.message} />;
return (
<FormLoader onRetry={fetchDashboard} errorMessage={error?.message || ''} />
);
}
const hasFavEntities = data.nodes.filter(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -67,7 +67,8 @@ const SystemActivity = () => {
});
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]();
};
@@ -87,7 +88,7 @@ const SystemActivity = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -110,7 +110,7 @@ const Version = () => {
}, [latestVersion, latestDevVersion]);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const DIVISIONS = [
const DIVISIONS: Array<{ amount: number; name: string }> = [
{ amount: 60, name: 'seconds' },
{ amount: 60, name: 'minutes' },
{ amount: 24, name: 'hours' },
@@ -119,18 +119,21 @@ const Version = () => {
{ amount: 12, name: 'months' },
{ amount: Number.POSITIVE_INFINITY, name: 'years' }
];
function formatTimeAgo(date) {
function formatTimeAgo(date: Date) {
let duration = (date.getTime() - new Date().getTime()) / 1000;
for (let i = 0; i < DIVISIONS.length; i++) {
const division = DIVISIONS[i];
if (Math.abs(duration) < division.amount) {
if (division && Math.abs(duration) < division.amount) {
return rtf.format(
Math.round(duration),
division.name as Intl.RelativeTimeFormatUnit
);
}
duration /= division.amount;
if (division) {
duration /= division.amount;
}
}
return rtf.format(0, 'seconds');
}
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
@@ -270,6 +273,14 @@ const Version = () => {
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
</span>
<Button
sx={{ ml: 2 }}
variant="outlined"
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{LL.REINSTALL()}
</Button>
</>
);
}
@@ -293,7 +304,7 @@ const Version = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
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 type { BoxProps } from '@mui/material';
const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => (
const ButtonRow = memo<BoxProps>(({ children, ...rest }) => (
<Box
sx={{
'& button, & a, & .MuiCard-root': {
mt: 2,
mx: 0.6,
'&:last-child': {
mr: 0
},
'&:first-of-type': {
ml: 0
}
'&:last-child': { mr: 0 },
'&:first-of-type': { ml: 0 }
}
}}
{...rest}
>
{children}
</Box>
);
));
ButtonRow.displayName = 'ButtonRow';
export default ButtonRow;

View File

@@ -1,7 +1,12 @@
import { Tooltip, type TooltipProps, styled, tooltipClasses } from '@mui/material';
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 }) => ({
[`& .${tooltipClasses.arrow}`]: {
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 './layout';
export * from './loading';
export * from './routing';
export * from './upload';
export { default as SectionContent } from './SectionContent';
export { default as ButtonRow } from './ButtonRow';
export { default as MessageBox } from './MessageBox';
// Specific routing exports
export { default as BlockNavigation } from './routing/BlockNavigation';
export { default as ButtonTooltip } from './ButtonTooltip';

View File

@@ -16,14 +16,14 @@ const ValidatedTextField: FC<ValidatedTextFieldProps> = ({
fieldErrors,
...rest
}) => {
const errors = fieldErrors && fieldErrors[rest.name];
const renderErrors = () =>
errors &&
errors.map((e) => <FormHelperText key={e.message}>{e.message}</FormHelperText>);
const errors = fieldErrors?.[rest.name];
return (
<>
<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);
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' }}>
<Icon />
</ListItemIcon>

View File

@@ -58,12 +58,22 @@ const LayoutMenuItem = ({
}
>
<ListItemButton component={Link} to={to}>
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} />
<RenderIcon
icon={icon}
{...(bgcolor && { bgcolor })}
label={label}
text={text}
/>
</ListItemButton>
</ListItem>
) : (
<ListItem>
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} />
<RenderIcon
icon={icon}
{...(bgcolor && { bgcolor })}
label={label}
text={text}
/>
</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 FormLoader } from './FormLoader';
export { default as LazyLoader } from './LazyLoader';

View File

@@ -1,6 +1,5 @@
import type { Path } from 'react-router';
import type * as H from 'history';
import { jwtDecode } from 'jwt-decode';
import type { Me, SignInRequest, SignInResponse } from 'types';
@@ -18,10 +17,10 @@ export function getStorage() {
return localStorage || sessionStorage;
}
export function storeLoginRedirect(location?: H.Location) {
export function storeLoginRedirect(location?: { pathname: string; search: string }) {
if (location) {
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname as string);
getStorage().setItem(SIGN_IN_SEARCH, location.search as string);
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
getStorage().setItem(SIGN_IN_SEARCH, location.search);
}
}
@@ -36,7 +35,7 @@ export function fetchLoginRedirect(): Partial<Path> {
clearLoginRedirect();
return {
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
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 CloudUploadIcon from '@mui/icons-material/CloudUpload';
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 './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 [dragged, setDragged] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
@@ -28,14 +71,17 @@ const DragNdrop = ({ text, onFileSelected }) => {
};
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) {
if (!e.target.files || e.target.files.length === 0) {
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
};
const handleDrop = (event) => {
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
const droppedFiles = event.dataTransfer.files;
if (droppedFiles.length > 0) {
@@ -43,38 +89,40 @@ const DragNdrop = ({ text, onFileSelected }) => {
}
};
const handleRemoveFile = (event) => {
const handleRemoveFile = (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
setFile(undefined);
setDragged(false);
};
const handleUploadClick = (event) => {
const handleUploadClick = (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
onFileSelected(file);
if (file) {
onFileSelected(file);
}
};
const handleBrowseClick = () => {
inputRef.current?.click();
};
const handleDragOver = (event) => {
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); // prevent file from being opened
setDragged(true);
};
return (
<div
className={`document-uploader ${file || dragged ? 'active' : ''}`}
<DocumentUploader
active={!!(file || dragged)}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={() => setDragged(false)}
onClick={handleBrowseClick}
>
<div className="upload-info">
<UploadInfo>
<CloudUploadIcon sx={{ marginRight: 4 }} color="primary" fontSize="large" />
<p>{text}</p>
</div>
<Typography>{text}</Typography>
</UploadInfo>
<input
type="file"
@@ -88,9 +136,9 @@ const DragNdrop = ({ text, onFileSelected }) => {
{file && (
<>
<div className="file-info">
<p>{file.name}</p>
</div>
<FileInfo>
<FileName>{file.name}</FileName>
</FileInfo>
<Box>
<Button
startIcon={<CancelIcon />}
@@ -112,7 +160,7 @@ const DragNdrop = ({ text, onFileSelected }) => {
</Box>
</>
)}
</div>
</DocumentUploader>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,108 @@
{
"compilerOptions": {
"target": "ESNext",
// Target modern browsers for better performance
"target": "ES2022",
"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,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"composite": true,
"checkJs": false,
// Module system optimized for Vite
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
// Emit configuration
"noEmit": true,
"useUnknownInCatchVariables": false,
"declaration": false,
"declarationMap": false,
"sourceMap": false,
// React/JSX configuration
"jsx": "react-jsx",
"noImplicitAny": false,
"baseUrl": "src",
"jsxImportSource": "react",
// 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": {
"@": ["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"],
"exclude": ["node_modules", "dist"]
"include": ["src/**/*", "vite.config.ts", "progmem-generator.js"],
"exclude": [
"node_modules",
"dist",
"build",
".tsbuildinfo",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx"
],
"ts-node": {
"esm": true
}
}

View File

@@ -1,130 +1,331 @@
import preact from '@preact/preset-vite';
import fs from 'fs';
import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
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 zlib from 'zlib';
// @ts-expect-error - mock server doesn't have type declarations
import mockServer from '../mock-api/mockServer.js';
export default defineConfig(({ command, mode }) => {
if (command === 'serve') {
console.log('Preparing for standalone build with server, mode=' + mode);
return {
plugins: [preact(), viteTsconfigPaths(), mockServer()],
server: {
open: true,
port: mode == 'production' ? 4173 : 3000,
proxy: {
'/api': {
target: 'http://localhost:3080',
changeOrigin: true,
secure: false
},
'/rest': 'http://localhost:3080',
'/gh': 'http://localhost:3080' // mock for GitHub API
}
}
};
}
if (mode === 'hosted') {
console.log('Preparing for hosted build');
return {
plugins: [preact(), viteTsconfigPaths()],
build: {
chunkSizeWarningLimit: 1024
}
};
}
console.log('Preparing for production, optimized build');
// Plugin to display bundle size information
const bundleSizeReporter = (): Plugin => {
return {
plugins: [
preact(),
viteTsconfigPaths(),
// {
// ...viteImagemin({
// verbose: false,
// gifsicle: {
// optimizationLevel: 7,
// interlaced: false
// },
// optipng: {
// optimizationLevel: 7
// },
// mozjpeg: {
// quality: 20
// },
// pngquant: {
// quality: [0.8, 0.9],
// speed: 4
// },
// svgo: {
// plugins: [
// {
// name: 'removeViewBox'
// },
// {
// name: 'removeEmptyAttrs',
// active: false
// }
// ]
// }
// }),
// enforce: 'pre'
// },
visualizer({
template: 'treemap', // or sunburst
open: false,
gzipSize: true,
brotliSize: true,
filename: '../analyse.html' // will be saved in project's root
})
],
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));
build: {
// target: 'es2022',
chunkSizeWarningLimit: 1024,
minify: 'terser',
terserOptions: {
compress: {
passes: 4,
arrows: true,
drop_console: true,
drop_debugger: true,
sequences: true
},
mangle: {
// toplevel: true
// module: true
// properties: {
// regex: /^_/
// }
},
ecma: 5,
enclose: false,
keep_classnames: false,
keep_fnames: false,
ie8: false,
module: false,
safari10: false,
toplevel: false
},
let totalSize = 0;
const files: Array<{ name: string; size: number; gzipSize?: number }> = [];
rollupOptions: {
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
// creating a chunk to react routes deps. Reducing the vendor chunk size
if (id.includes('react-router')) {
return '@react-router';
}
return 'vendor';
}
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') {
console.log('Preparing for standalone build with server, mode=' + mode);
return {
plugins: [
preact({
// Keep dev tools enabled for development
devToolsEnabled: true,
prefreshEnabled: true
}),
viteTsconfigPaths(),
bundleSizeReporter(), // Add bundle size reporting
mockServer()
],
server: {
open: true,
port: mode == 'production' ? 4173 : 3000,
proxy: {
'/api': {
target: 'http://localhost:3080',
changeOrigin: true,
secure: false
},
'/rest': 'http://localhost:3080',
'/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
}
};
}
if (mode === 'hosted') {
console.log('Preparing for hosted build');
return {
plugins: [
preact({
// Enable Preact optimizations for hosted build
devToolsEnabled: false,
prefreshEnabled: false
}),
viteTsconfigPaths(),
bundleSizeReporter() // Add bundle size reporting
],
build: {
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;
}
}
}
}
};
}
console.log('Preparing for production, optimized build');
return {
plugins: [
preact({
// Enable Preact optimizations
devToolsEnabled: false,
prefreshEnabled: false
}),
viteTsconfigPaths(),
// Enable image optimization for size reduction
{
...viteImagemin({
verbose: false,
gifsicle: {
optimizationLevel: 7,
interlaced: false
},
optipng: {
optimizationLevel: 7
},
mozjpeg: {
quality: 20
},
pngquant: {
quality: [0.8, 0.9],
speed: 4
},
svgo: {
plugins: [
{
name: 'removeViewBox'
},
{
name: 'removeEmptyAttrs',
active: false
}
]
}
}),
enforce: 'pre'
},
visualizer({
template: 'treemap', // or sunburst
open: false,
gzipSize: true,
brotliSize: true,
filename: '../analyse.html' // will be saved in project's root
}),
bundleSizeReporter() // Add bundle size reporting
],
build: {
// Target modern browsers for smaller bundles
target: 'es2020',
chunkSizeWarningLimit: 512,
minify: 'terser',
// Enable CSS minification
cssMinify: true,
// Optimize asset handling
assetsInlineLimit: 4096, // Inline small assets
terserOptions: {
compress: {
passes: 6,
arrows: true,
drop_console: true,
drop_debugger: 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: {
toplevel: true, // Enable top-level mangling
module: true // Enable module mangling
// properties: {
// regex: /^_/ // Mangle properties starting with _
// }
},
ecma: 2020, // Updated to modern ECMAScript
enclose: false,
keep_classnames: false,
keep_fnames: false,
ie8: false,
module: false,
safari10: false,
toplevel: true // Enable top-level optimization
},
rollupOptions: {
// Enable tree shaking
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
tryCatchDeoptimization: false
},
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) {
if (id.includes('node_modules')) {
// More granular chunk splitting for better caching
if (id.includes('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';
}
// 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
// - file uploads
// - EventSource (SSE) for log messages
// Mock server for development
// Simulates file uploads and EventSource (SSE) for log messages
import formidable from 'formidable';
function pad(number) {
let r = String(number);
if (r.length === 1) {
r = '0' + r;
}
return r;
}
// Optimized padding function
const pad = (number) => String(number).padStart(2, '0');
// e.g. 2024-03-29 07:02:37.856
Date.prototype.toISOString = function () {
return (
this.getUTCFullYear() +
'-' +
pad(this.getUTCMonth() + 1) +
'-' +
pad(this.getUTCDate()) +
' ' +
pad(this.getUTCHours()) +
':' +
pad(this.getUTCMinutes()) +
':' +
pad(this.getUTCSeconds()) +
'.' +
String((this.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5)
// Cached date formatter to avoid prototype pollution
const formatDate = (date) => {
const year = date.getUTCFullYear();
const month = pad(date.getUTCMonth() + 1);
const day = pad(date.getUTCDate());
const hours = pad(date.getUTCHours());
const minutes = pad(date.getUTCMinutes());
const seconds = pad(date.getUTCSeconds());
const milliseconds = String((date.getUTCMilliseconds() / 1000).toFixed(3)).slice(
2,
5
);
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
};
export default () => {
@@ -35,97 +26,129 @@ export default () => {
name: 'vite:mockserver',
configureServer: async (server) => {
server.middlewares.use(async (req, res, next) => {
// catch any file uploads
// Handle file uploads
if (req.url.startsWith('/rest/uploadFile')) {
// show progress
const fileSize = parseInt(req.headers['content-length'] || '0', 10);
let progress = 0;
const file_size = req.headers['content-length'];
console.log('File size: ' + file_size);
req.on('data', async (chunk) => {
// Track upload progress
req.on('data', (chunk) => {
progress += chunk.length;
const percentage = (progress / file_size) * 100;
console.log(`Progress: ${Math.round(percentage)}%`);
// await new Promise((resolve) => setTimeout(() => resolve(), 3000)); // slow it down
if (fileSize > 0) {
const percentage = Math.round((progress / fileSize) * 100);
console.log(`Upload progress: ${percentage}%`);
}
});
const form = formidable({});
let fields;
let files;
try {
[fields, files] = await form.parse(req);
} catch (err) {
console.error('Not json form content');
res.writeHead(err.httpCode || 400, {
'Content-Type': 'text/plain'
const form = formidable({
maxFileSize: 50 * 1024 * 1024, // 50MB limit
keepExtensions: true
});
res.end(String(err));
return;
}
// only process when we have a file
if (Object.keys(files).length > 0) {
const uploaded_file = files.file[0];
const file_name = uploaded_file.originalFilename;
const file_extension = file_name.substring(
file_name.lastIndexOf('.') + 1
const [fields, files] = await form.parse(req);
if (Object.keys(files).length === 0) {
res.statusCode = 400;
res.end('No file uploaded');
return;
}
const uploadedFile = files.file[0];
const fileName = uploadedFile.originalFilename;
const fileExtension = fileName
.substring(fileName.lastIndexOf('.') + 1)
.toLowerCase();
console.log(
`File uploaded: ${fileName} (${fileExtension}, ${fileSize} bytes)`
);
console.log('Filename: ' + file_name);
console.log('Extension: ' + file_extension);
console.log('File size: ' + file_size);
// Validate file extension
const validExtensions = new Set(['bin', 'json', 'md5']);
if (!validExtensions.has(fileExtension)) {
res.statusCode = 406;
res.end('Invalid file extension');
return;
}
if (file_extension === 'bin' || file_extension === 'json') {
console.log('File uploaded successfully!');
} else if (file_extension === 'md5') {
console.log('MD5 hash generated successfully!');
// Handle different file types
if (fileExtension === 'md5') {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
md5: 'ef4304fc4d9025a58dcf25d71c882d2c'
})
);
} else {
res.statusCode = 406;
console.log('Invalid file extension!');
console.log('File uploaded successfully!');
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);
}
res.end();
}
// SSE Eventsource
// Handle Server-Sent Events (SSE) for log streaming
else if (req.url.startsWith('/es/log')) {
// Set SSE headers
res.writeHead(200, {
Connection: 'keep-alive',
'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;
const interval = setInterval(() => {
let message = 'message #' + count;
if (count % 6 === 1) {
let messageCount = 0;
const logLevels = [3, 4, 5, 6, 7, 8]; // Different log levels
const logNames = ['system', 'ems', 'wifi', 'mqtt', 'ntp', 'api'];
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 +=
' 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(),
l: 3 + (count % 6),
i: count,
n: 'system',
const logData = {
t: formatDate(new Date()),
l: level,
i: messageCount,
n: name,
m: message
};
count++;
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
// if client closes connection
res.on('close', () => {
console.log('Closing ES connection');
res.write(`data: ${JSON.stringify(logData)}\n\n`);
messageCount++;
};
// 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);
res.end();
});
if (!res.destroyed) {
res.end();
}
};
res.on('close', cleanup);
res.on('error', cleanup);
} 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",
"version": "3.7.2",
"version": "3.7.3",
"description": "mock api for EMS-ESP",
"author": "proddy, emsesp.org",
"license": "MIT",
@@ -15,5 +15,5 @@
"itty-router": "^5.0.22",
"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 STABLE_VERSION_IS_UPGRADEABLE: boolean;
let THIS_VERSION: string;
let version_test: number;
let LATEST_STABLE_VERSION = '3.7.2';
let LATEST_DEV_VERSION = '3.7.3-dev.6';
// scenarios for testing versioning
version_test = 0; // on latest stable, or switch to dev
// 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
// 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 = 0; // on latest stable, or switch to dev
// let version_test = 1; // on latest dev, or switch back to stable
// let version_test = 2; // upgrade an older stable to latest stable or switch to latest dev
// let version_test = 3; // upgrade dev to latest, or switch to stable
// let version_test = 4; // downgrade to an older dev, or switch back to stable
switch (version_test as number) {
case 0:
@@ -4380,78 +4378,124 @@ router
function deviceData(id: number) {
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) {
return new Response(encoder.encode(emsesp_devicedata_2), { headers });
return new Response(encoder.encode(emsesp_devicedata_2) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_devicedata_4), { headers });
return new Response(encoder.encode(emsesp_devicedata_4) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_devicedata_6), { headers });
return new Response(encoder.encode(emsesp_devicedata_6) as BodyInit, {
headers
});
}
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) {
// 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);
return new Response(encoder.encode(emsesp_devicedata_8), { headers });
return new Response(encoder.encode(emsesp_devicedata_8) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_devicedata_10), { headers });
return new Response(encoder.encode(emsesp_devicedata_10) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_devicedata_99), { headers });
return new Response(encoder.encode(emsesp_devicedata_99) as BodyInit, {
headers
});
}
}
function deviceEntities(id: number) {
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) {
return new Response(encoder.encode(emsesp_deviceentities_2), { headers });
return new Response(encoder.encode(emsesp_deviceentities_2) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_deviceentities_4), { headers });
return new Response(encoder.encode(emsesp_deviceentities_4) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_deviceentities_6), { headers });
return new Response(encoder.encode(emsesp_deviceentities_6) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_deviceentities_8), { headers });
return new Response(encoder.encode(emsesp_deviceentities_8) as BodyInit, {
headers
});
}
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) {
return new Response(encoder.encode(emsesp_deviceentities_10), { headers });
return new Response(encoder.encode(emsesp_deviceentities_10) as BodyInit, {
headers
});
}
// 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
@@ -4558,65 +4602,63 @@ router
}
// add temperature sensor data. no command c
let sensor_data: any[] = [];
sensor_data = emsesp_sensordata.ts.map((item, index) => ({
id: DeviceTypeUniqueID.TEMPERATURESENSOR_UID * 100 + index,
dv: {
id: '00' + item.n,
v: item.t, // value is called t in ts (temperature)
u: item.u
}
}));
dashboard_object = {
id: DeviceTypeUniqueID.TEMPERATURESENSOR_UID,
t: DeviceType.TEMPERATURESENSOR,
nodes: sensor_data
};
// only add to dashboard if we have values
if ((dashboard_object.nodes ?? []).length > 0) {
if (emsesp_sensordata.ts.length > 0) {
const sensor_data = emsesp_sensordata.ts.map((item, index) => ({
id: DeviceTypeUniqueID.TEMPERATURESENSOR_UID * 100 + index,
dv: {
id: '00' + item.n,
v: item.t, // value is called t in ts (temperature)
u: item.u
}
}));
dashboard_object = {
id: DeviceTypeUniqueID.TEMPERATURESENSOR_UID,
t: DeviceType.TEMPERATURESENSOR,
nodes: sensor_data
};
dashboard_nodes.push(dashboard_object);
}
// add analog sensor data. no command c
// remove disabled sensors first (t = 0)
sensor_data = emsesp_sensordata.as.filter((item) => item.t !== 0);
sensor_data = sensor_data.map((item, index) => ({
id: DeviceTypeUniqueID.ANALOGSENSOR_UID * 100 + index,
dv: {
id: '00' + item.n,
v: item.v,
u: item.u
}
}));
dashboard_object = {
id: DeviceTypeUniqueID.ANALOGSENSOR_UID,
t: DeviceType.ANALOGSENSOR,
nodes: sensor_data
};
// only add to dashboard if we have values
if ((dashboard_object.nodes ?? []).length > 0) {
// remove disabled sensors first (t = 0) and create data in one pass
const enabledAnalogSensors = emsesp_sensordata.as.filter(
(item) => item.t !== 0
);
if (enabledAnalogSensors.length > 0) {
const sensor_data = enabledAnalogSensors.map((item, index) => ({
id: DeviceTypeUniqueID.ANALOGSENSOR_UID * 100 + index,
dv: {
id: '00' + item.n,
v: item.v,
u: item.u
}
}));
dashboard_object = {
id: DeviceTypeUniqueID.ANALOGSENSOR_UID,
t: DeviceType.ANALOGSENSOR,
nodes: sensor_data
};
dashboard_nodes.push(dashboard_object);
}
// add the scheduler data
// filter emsesp_schedule with only if it has a name
let scheduler_data = emsesp_schedule.schedule.filter((item) => item.name);
let scheduler_data2 = scheduler_data.map((item, index) => ({
id: DeviceTypeUniqueID.SCHEDULER_UID * 100 + index,
dv: {
id: '00' + item.name,
v: item.active ? 'on' : 'off',
c: item.name,
l: ['off', 'on']
}
}));
dashboard_object = {
id: DeviceTypeUniqueID.SCHEDULER_UID,
t: DeviceType.SCHEDULER,
nodes: scheduler_data2
};
// only add to dashboard if we have values
if ((dashboard_object.nodes ?? []).length > 0) {
// filter emsesp_schedule with only if it has a name and create data in one pass
const namedSchedules = emsesp_schedule.schedule.filter((item) => item.name);
if (namedSchedules.length > 0) {
const scheduler_data = namedSchedules.map((item, index) => ({
id: DeviceTypeUniqueID.SCHEDULER_UID * 100 + index,
dv: {
id: '00' + item.name,
v: item.active ? 'on' : 'off',
c: item.name,
l: ['off', 'on']
}
}));
dashboard_object = {
id: DeviceTypeUniqueID.SCHEDULER_UID,
t: DeviceType.SCHEDULER,
nodes: scheduler_data
};
dashboard_nodes.push(dashboard_object);
}
} else {
@@ -4660,8 +4702,12 @@ router
};
// console.log('dashboardData: ', dashboardData);
// Clear references to help with garbage collection
dashboard_nodes = [];
dashboard_object = {};
// 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
@@ -4852,10 +4898,12 @@ router
} else {
if (as.deleted) {
emsesp_sensordata.as[objIndex].d = true;
var filtered = emsesp_sensordata.as.filter(function (value, index, arr) {
return !value.d;
});
emsesp_sensordata.as = filtered;
// Remove deleted items in-place to avoid creating new arrays
for (let i = emsesp_sensordata.as.length - 1; i >= 0; i--) {
if (emsesp_sensordata.as[i].d) {
emsesp_sensordata.as.splice(i, 1);
}
}
} else {
emsesp_sensordata.as[objIndex].n = as.name;
emsesp_sensordata.as[objIndex].f = as.factor;

View File

@@ -197,7 +197,7 @@ extra_scripts =
build_flags =
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-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
build_unflags = -std=gnu++11 -std=gnu++14
build_type = debug

View File

@@ -1426,3 +1426,11 @@ Roomparams
Roomdata
Roomschedule
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
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
make clean
make -s ARGS=-DEMSESP_STANDALONE

View File

@@ -211,7 +211,7 @@ for entity in entities:
if int(entity["modbus count"]) <= 0:
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":
# 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},
{164, DeviceType::WATER, "SM200, MS200", DeviceFlags::EMS_DEVICE_FLAG_SM100},
{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}
#endif

View File

@@ -29,22 +29,22 @@ static_assert(uuid::console::thread_safe, "uuid-console must be thread-safe");
namespace emsesp {
// Static member definitions
std::deque<std::unique_ptr<EMSdevice>> EMSESP::emsdevices{};
std::vector<EMSESP::Device_record> EMSESP::device_library_;
uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN};
uint16_t EMSESP::watch_id_ = WATCH_ID_NONE;
uint8_t EMSESP::watch_ = 0;
uint16_t EMSESP::read_id_ = WATCH_ID_NONE;
bool EMSESP::read_next_ = false;
uint16_t EMSESP::publish_id_ = 0;
uint16_t EMSESP::response_id_ = 0;
bool EMSESP::tap_water_active_ = false;
uint8_t EMSESP::publish_all_idx_ = 0;
uint8_t EMSESP::unique_id_count_ = 0;
bool EMSESP::trace_raw_ = false;
uint16_t EMSESP::wait_validate_ = 0;
bool EMSESP::wait_km_ = false;
uint32_t EMSESP::last_fetch_ = 0;
std::vector<std::unique_ptr<EMSdevice>> EMSESP::emsdevices{};
std::vector<EMSESP::Device_record> EMSESP::device_library_;
uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN};
uint16_t EMSESP::watch_id_ = WATCH_ID_NONE;
uint8_t EMSESP::watch_ = 0;
uint16_t EMSESP::read_id_ = WATCH_ID_NONE;
bool EMSESP::read_next_ = false;
uint16_t EMSESP::publish_id_ = 0;
uint16_t EMSESP::response_id_ = 0;
bool EMSESP::tap_water_active_ = false;
uint8_t EMSESP::publish_all_idx_ = 0;
uint8_t EMSESP::unique_id_count_ = 0;
bool EMSESP::trace_raw_ = false;
uint16_t EMSESP::wait_validate_ = 0;
bool EMSESP::wait_km_ = false;
uint32_t EMSESP::last_fetch_ = 0;
AsyncWebServer webServer(80);

View File

@@ -222,7 +222,7 @@ class EMSESP {
static void scan_devices();
static void clear_all_devices();
static std::deque<std::unique_ptr<EMSdevice>> emsdevices;
static std::vector<std::unique_ptr<EMSdevice>> emsdevices;
// services
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
// works with Latin1 (1 byte), Polish amd some other (2 bytes) characters
// TODO add special characters that occur in other supported languages
// works with Latin1 (1 byte), Polish and other (2 bytes) characters
// supports special characters for all 11 supported languages: EN, DE, NL, SV, PL, NO, FR, TR, IT, SK, CZ
#if defined(EMSESP_STANDALONE)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtype-limits"
@@ -714,23 +714,77 @@ void Helpers::CharToUpperUTF8(char * c) {
if ((p_v >= (char)0xA0) && (p_v <= (char)0xBE)) {
*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;
case (char)0xC4:
switch (p_v) {
case (char)0x85: //ą (0xC4,0x85) -> Ą (0xC4,0x84)
case (char)0x87: //ć (0xC4,0x87) -> Ć (0xC4,0x86)
case (char)0x99: //ę (0xC4,0x99) -> Ę (0xC4,0x98)
case (char)0x85: //ą (0xC4,0x85) -> Ą (0xC4,0x84) (Polish)
case (char)0x87: //ć (0xC4,0x87) -> Ć (0xC4,0x86) (Polish)
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;
break;
}
break;
case (char)0xC5:
switch (p_v) {
case (char)0x82: //ł (0xC5,0x82) -> Ł (0xC5,0x81)
case (char)0x84: //ń (0xC5,0x84) -> Ń (0xC5,0x83)
case (char)0x9B: //ś (0xC5,0x9B) -> Ś (0xC5,0x9A)
case (char)0xBA: //ź (0xC5,0xBA) -> Ź (0xC5,0xB9)
case (char)0xBC: //ż (0xC5,0xBC) -> Ż (0xC5,0xBB)
case (char)0x81: //ł (0xC5,0x81) -> Ł (0xC5,0x80) (Polish)
case (char)0x82: //ł (0xC5,0x82) -> Ł (0xC5,0x81) (Polish)
case (char)0x83: //ń (0xC5,0x83) -> Ń (0xC5,0x82) (Polish)
case (char)0x84: //ń (0xC5,0x84) -> Ń (0xC5,0x83) (Polish)
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;
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_(heatup), 200, 1), // heatup
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_(wwSetTempLow), 2, 1), // settemplow
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircMode), 3, 1), // circmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwChargeDuration), 4, 1), // chargeduration
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCharge), 5, 1), // charge
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwExtra), 6, 1), // extra
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfecting), 7, 1), // disinfecting
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectDay), 8, 1), // disinfectday
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectTime), 9, 1), // disinfecttime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeating), 10, 1), // dailyheating
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeatTime), 11, 1), // dailyheattime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwProgMode), 12, 1), // progmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircProg), 13, 1), // circprog
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectHour), 14, 1), // disinfecthour
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwMaxTemp), 15, 1), // maxtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwOneTimeKey), 16, 1), // onetimekey
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(switchtime), 17, 8), // switchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwcircswitchtime), 25, 8), // circswitchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(holidays), 33, 13), // holidays
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(vacations), 46, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwWhenModeOff), 59, 1), // whenmodeoff
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(modetype), 1, 1), // modetype
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTemp), 2, 1), // settemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTempLow), 3, 1), // settemplow
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircMode), 4, 1), // circmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwChargeDuration), 5, 1), // chargeduration
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCharge), 6, 1), // charge
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwExtra), 7, 1), // extra
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfecting), 8, 1), // disinfecting
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectDay), 9, 1), // disinfectday
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectTime), 10, 1), // disinfecttime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeating), 11, 1), // dailyheating
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeatTime), 12, 1), // dailyheattime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwProgMode), 13, 1), // progmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircProg), 14, 1), // circprog
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectHour), 15, 1), // disinfecthour
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwMaxTemp), 16, 1), // maxtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwOneTimeKey), 17, 1), // onetimekey
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(switchtime), 18, 8), // switchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwcircswitchtime), 26, 8), // circswitchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(holidays), 34, 13), // holidays
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_(valveStatus), 1, 1), // valvestatus
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_(status), 2, 1), // status
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_(setBurnPow), 1, 1), // setburnpow
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
void Shower::shower_alert_start() {
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;
force_coldshot = false;
alert_timer_start_ = uuid::get_uptime_sec(); // timer starts now
@@ -161,7 +161,7 @@ void Shower::shower_alert_start() {
void Shower::shower_alert_stop() {
if (doing_cold_shot_) {
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;
force_coldshot = false;
next_alert_ += shower_alert_trigger_;

View File

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

View File

@@ -135,7 +135,7 @@ void Connect::process_roomThermostat(std::shared_ptr<const Telegram> telegram) {
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)
void Connect::process_roomThermostatName(std::shared_ptr<const Telegram> telegram) {
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

@@ -263,8 +263,8 @@ bool Test::test(const std::string & cmd, int8_t id1, int8_t id2) {
if (cmd == "src") {
EMSESP::logger().notice("Adding SRC plus thermostat...");
add_device(0x50, 17); // MX400 module
uart_telegram("50 00 FF 00 0A DD 00 E6 36 2A"); // monitor, temperatures
add_device(0x50, 17); // MX400 module
uart_telegram("50 00 FF 00 0A DD 00 E6 36 2A"); // monitor, temperatures
uart_telegram("50 00 FF 00 0A B5 00 FF 00 24 01 FF 24 00"); // mode, childlock
// switchprogram
uart_telegram("50 00 FF 00 0A 65 2A 00 3C 2A FF FF 2A FF FF 2A FF FF 2A FF FF 2A FF FF");
@@ -764,7 +764,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
test("boiler");
// device type, command, data
Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "false", 9);
Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "false", DeviceValueTAG::TAG_DHW1);
ok = true;
}