diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index eadb7a48b..d4f73ac66 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -20,6 +20,8 @@ This release is based on the latest Espressif/Arduino core version 3. It brings - LED stayed off on a healthy system when "Disable LED" was unchecked - Ethernet MAC address changed with the new SDK, breaking DHCP reservations (currently disabled) - "IPv4 nameserver" showed an IPv6 address when IPv6 was in use +- HA Discovery warning on Uptime after EMS-ESP boot due to NTP not ready +- show MQTT connection error only once on the first connect failure ## Changed @@ -30,4 +32,5 @@ This release is based on the latest Espressif/Arduino core version 3. It brings - Gateway and Connect devices are shown in the Devices page, but disabled [3126](https://github.com/emsesp/EMS-ESP32/discussions/3126) - show control setting only for master thermostats (0x10) [#3173](https://github.com/emsesp/EMS-ESP32/issues/3173) - remove devices without entities not listed in 0x07 telegram +- changes to the Dialogs in Dashboard page, removed the run icon in the Commands page. Renamed Execute to Run. diff --git a/interface/package.json b/interface/package.json index 9e8fdc7ed..fb974e5e9 100644 --- a/interface/package.json +++ b/interface/package.json @@ -26,8 +26,8 @@ "@alova/adapter-xhr": "2.3.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^9.3.0", - "@mui/material": "^9.3.0", + "@mui/icons-material": "^9.3.1", + "@mui/material": "^9.3.1", "@table-library/react-table-library": "4.1.15", "alova": "^3.5.3", "jwt-decode": "^4.0.0", @@ -42,21 +42,21 @@ "@eslint/js": "^10.0.1", "@preact/preset-vite": "^2.10.6", "@trivago/prettier-plugin-sort-imports": "^6.0.2", - "@types/node": "^26.1.2", + "@types/node": "^26.2.0", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "concurrently": "^10.0.4", - "eslint": "^10.8.0", + "eslint": "^10.8.1", "eslint-config-prettier": "^10.1.8", "etag": "^1.8.1", "mime-types": "^3.0.2", "prettier": "^3.9.6", "rollup-plugin-visualizer": "^7.0.1", - "terser": "^5.49.1", + "terser": "^5.49.2", "typescript": "^6.0.3", "typescript-eslint": "^8.66.0", - "vite": "^8.2.0", + "vite": "^8.2.1", "vite-plugin-imagemin": "^0.6.1" }, - "packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee" + "packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1" } diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 12dfc57fb..509697c99 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: specifier: ^11.14.1 version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2) '@mui/icons-material': - specifier: ^9.3.0 - version: 9.3.0(@mui/material@9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + specifier: ^9.3.1 + version: 9.3.1(@mui/material@9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) '@mui/material': - specifier: ^9.3.0 - version: 9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^9.3.1 + version: 9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@table-library/react-table-library': specifier: 4.1.15 version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -53,16 +53,16 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.8.0(supports-color@10.2.2)) + version: 10.0.1(eslint@10.8.1(supports-color@10.2.2)) '@preact/preset-vite': specifier: ^2.10.6 - version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1)) + version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2)) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2) '@types/node': - specifier: ^26.1.2 - version: 26.1.2 + specifier: ^26.2.0 + version: 26.2.0 '@types/react': specifier: ^19.2.18 version: 19.2.18 @@ -73,11 +73,11 @@ importers: specifier: ^10.0.4 version: 10.0.4 eslint: - specifier: ^10.8.0 - version: 10.8.0(supports-color@10.2.2) + specifier: ^10.8.1 + version: 10.8.1(supports-color@10.2.2) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.8.0(supports-color@10.2.2)) + version: 10.1.8(eslint@10.8.1(supports-color@10.2.2)) etag: specifier: ^1.8.1 version: 1.8.1 @@ -89,22 +89,22 @@ importers: version: 3.9.6 rollup-plugin-visualizer: specifier: ^7.0.1 - version: 7.0.1(rolldown@1.2.2) + version: 7.0.1(rolldown@1.2.3) terser: - specifier: ^5.49.1 - version: 5.49.1 + specifier: ^5.49.2 + version: 5.49.2 typescript: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: specifier: ^8.66.0 - version: 8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + version: 8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) vite: - specifier: ^8.2.0 - version: 8.2.0(@types/node@26.1.2)(terser@5.49.1) + specifier: ^8.2.1 + version: 8.2.1(@types/node@26.2.0)(terser@5.49.2) vite-plugin-imagemin: specifier: ^0.6.1 - version: 0.6.1(supports-color@10.2.2)(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1)) + version: 0.6.1(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2)) packages: @@ -354,22 +354,22 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@mui/core-downloads-tracker@9.3.0': - resolution: {integrity: sha512-CrjNKhozfI6kSbD4bvfS8Nx+eFOVX/I5Tb3L73YBXSG0e4PyYbEwu03lp7AOM/ERdceQQrpBnQeSgL4FMZO5mg==} + '@mui/core-downloads-tracker@9.3.1': + resolution: {integrity: sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==} - '@mui/icons-material@9.3.0': - resolution: {integrity: sha512-6kk+cbt7fyehTRIrt3LyeoD4C36lX0biR1NMnwy0Wz7Sj6lwOh6ghY5BZzd1UncyOZGCmTZROBQxSPawcKES7A==} + '@mui/icons-material@9.3.1': + resolution: {integrity: sha512-rZj5ccG7vkpV38o/l4ys+chfE9GFypmvZr9dSNBoYVCNBD9yC6KfKq1TYpEMshWbjic7GKQ8MA1LQlcpGgcq9Q==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^9.3.0 + '@mui/material': ^9.3.1 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/material@9.3.0': - resolution: {integrity: sha512-H9WJkuaT7YthTOnz8ecygQoKZNVqvgZsTv/oJl6ttalfg6F1MkEi4fx/4j8f5+ju4U9mPX+lALLEZGVT9Jb3Gg==} + '@mui/material@9.3.1': + resolution: {integrity: sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -457,8 +457,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.142.0': - resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -486,92 +486,92 @@ packages: preact: ^10.4.0 || ^11.0.0-0 vite: '>=2.0.0' - '@rolldown/binding-android-arm64@1.2.2': - resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.2': - resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.2': - resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.2': - resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': - resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.2': - resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.2': - resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.2': - resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.2': - resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.2': - resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.2': - resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.2': - resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.2': - resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.2': - resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -665,8 +665,8 @@ packages: resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. - '@types/node@26.1.2': - resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1344,8 +1344,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.8.0: - resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} + eslint@10.8.1: + resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2124,8 +2124,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.17: - resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2343,8 +2343,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} powershell-utils@0.1.0: @@ -2498,8 +2498,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rolldown@1.2.2: - resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2739,8 +2739,8 @@ packages: resolution: {integrity: sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==} engines: {node: '>=4'} - terser@5.49.1: - resolution: {integrity: sha512-7A2xlQ5EnGT8KPA92dUh6RbRYTVw8hEaEN9L1K68l4UOXFuV511NnAqObGoRqGOQofQcMypisu1s3xawCEHrvA==} + terser@5.49.2: + resolution: {integrity: sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==} engines: {node: '>=10'} hasBin: true @@ -2869,8 +2869,8 @@ packages: peerDependencies: vite: 5.x || 6.x || 7.x || 8.x - vite@8.2.0: - resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3208,9 +3208,9 @@ snapshots: '@esbuild/linux-loong64@0.14.54': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(supports-color@10.2.2))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1(supports-color@10.2.2))': dependencies: - eslint: 10.8.0(supports-color@10.2.2) + eslint: 10.8.1(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3231,9 +3231,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.8.0(supports-color@10.2.2))': + '@eslint/js@10.0.1(eslint@10.8.1(supports-color@10.2.2))': optionalDependencies: - eslint: 10.8.0(supports-color@10.2.2) + eslint: 10.8.1(supports-color@10.2.2) '@eslint/object-schema@3.0.5': {} @@ -3282,20 +3282,20 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mui/core-downloads-tracker@9.3.0': {} + '@mui/core-downloads-tracker@9.3.1': {} - '@mui/icons-material@9.3.0(@mui/material@9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8)': + '@mui/icons-material@9.3.1(@mui/material@9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 - '@mui/material': 9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@mui/material': 9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 optionalDependencies: '@types/react': 19.2.18 - '@mui/material@9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@mui/material@9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 - '@mui/core-downloads-tracker': 9.3.0 + '@mui/core-downloads-tracker': 9.3.1 '@mui/system': 9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8) '@mui/types': 9.3.0(@types/react@19.2.18) '@mui/utils': 9.3.0(@types/react@19.2.18)(react@19.2.8) @@ -3381,23 +3381,23 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.142.0': {} + '@oxc-project/types@0.143.0': {} '@popperjs/core@2.11.8': {} - '@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1))': + '@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2))': dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) - '@prefresh/vite': 2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1)) + '@prefresh/vite': 2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2)) '@rollup/pluginutils': 5.4.0 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.7(supports-color@10.2.2)) debug: 4.4.3(supports-color@10.2.2) magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.2.0(@types/node@26.1.2)(terser@5.49.1) - vite-prerender-plugin: 0.5.13(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1)) + vite: 8.2.1(@types/node@26.2.0)(terser@5.49.2) + vite-prerender-plugin: 0.5.13(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact @@ -3412,7 +3412,7 @@ snapshots: '@prefresh/utils@1.2.1': {} - '@prefresh/vite@2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1))': + '@prefresh/vite@2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2))': dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@prefresh/babel-plugin': 0.5.3 @@ -3420,50 +3420,50 @@ snapshots: '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 preact: 10.29.8 - vite: 8.2.0(@types/node@26.1.2)(terser@5.49.1) + vite: 8.2.1(@types/node@26.2.0)(terser@5.49.2) transitivePeerDependencies: - supports-color - '@rolldown/binding-android-arm64@1.2.2': + '@rolldown/binding-android-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-arm64@1.2.2': + '@rolldown/binding-darwin-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-x64@1.2.2': + '@rolldown/binding-darwin-x64@1.2.3': optional: true - '@rolldown/binding-freebsd-x64@1.2.2': + '@rolldown/binding-freebsd-x64@1.2.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.2': + '@rolldown/binding-linux-arm64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.2': + '@rolldown/binding-linux-arm64-musl@1.2.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.2': + '@rolldown/binding-linux-ppc64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.2': + '@rolldown/binding-linux-s390x-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.2': + '@rolldown/binding-linux-x64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-musl@1.2.2': + '@rolldown/binding-linux-x64-musl@1.2.3': optional: true - '@rolldown/binding-openharmony-arm64@1.2.2': + '@rolldown/binding-openharmony-arm64@1.2.3': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.2': + '@rolldown/binding-win32-arm64-msvc@1.2.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.2': + '@rolldown/binding-win32-x64-msvc@1.2.3': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -3511,7 +3511,7 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 6.0.0 - '@types/node': 26.1.2 + '@types/node': 26.2.0 '@types/imagemin-gifsicle@7.0.4': dependencies: @@ -3540,23 +3540,23 @@ snapshots: '@types/imagemin@7.0.1': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 '@types/imagemin@9.0.1': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 '@types/json-schema@7.0.15': {} '@types/keyv@3.1.4': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 '@types/minimatch@6.0.0': dependencies: minimatch: 10.2.6 - '@types/node@26.1.2': + '@types/node@26.2.0': dependencies: undici-types: 8.3.0 @@ -3578,21 +3578,21 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 '@types/svgo@2.6.4': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 - '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.66.0 - eslint: 10.8.0(supports-color@10.2.2) + eslint: 10.8.1(supports-color@10.2.2) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3600,14 +3600,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/parser@8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.66.0 '@typescript-eslint/types': 8.66.0 '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3(supports-color@10.2.2) - eslint: 10.8.0(supports-color@10.2.2) + eslint: 10.8.1(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3630,13 +3630,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.66.0 '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) debug: 4.4.3(supports-color@10.2.2) - eslint: 10.8.0(supports-color@10.2.2) + eslint: 10.8.1(supports-color@10.2.2) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -3659,13 +3659,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/utils@8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.66.0 '@typescript-eslint/types': 8.66.0 '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.8.0(supports-color@10.2.2) + eslint: 10.8.1(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4269,9 +4269,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.8.0(supports-color@10.2.2)): + eslint-config-prettier@10.1.8(eslint@10.8.1(supports-color@10.2.2)): dependencies: - eslint: 10.8.0(supports-color@10.2.2) + eslint: 10.8.1(supports-color@10.2.2) eslint-scope@9.1.2: dependencies: @@ -4284,9 +4284,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.8.0(supports-color@10.2.2): + eslint@10.8.1(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@10.2.2) '@eslint/config-helpers': 0.7.0 @@ -5080,7 +5080,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.17: {} + nanoid@3.3.18: {} natural-compare@1.4.0: {} @@ -5277,9 +5277,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.25: + postcss@8.5.26: dependencies: - nanoid: 3.3.17 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -5418,34 +5418,34 @@ snapshots: dependencies: glob: 7.2.3 - rolldown@1.2.2: + rolldown@1.2.3: dependencies: - '@oxc-project/types': 0.142.0 + '@oxc-project/types': 0.143.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.2 - '@rolldown/binding-darwin-arm64': 1.2.2 - '@rolldown/binding-darwin-x64': 1.2.2 - '@rolldown/binding-freebsd-x64': 1.2.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.2 - '@rolldown/binding-linux-arm64-gnu': 1.2.2 - '@rolldown/binding-linux-arm64-musl': 1.2.2 - '@rolldown/binding-linux-ppc64-gnu': 1.2.2 - '@rolldown/binding-linux-s390x-gnu': 1.2.2 - '@rolldown/binding-linux-x64-gnu': 1.2.2 - '@rolldown/binding-linux-x64-musl': 1.2.2 - '@rolldown/binding-openharmony-arm64': 1.2.2 - '@rolldown/binding-win32-arm64-msvc': 1.2.2 - '@rolldown/binding-win32-x64-msvc': 1.2.2 + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 - rollup-plugin-visualizer@7.0.1(rolldown@1.2.2): + rollup-plugin-visualizer@7.0.1(rolldown@1.2.3): dependencies: open: 11.0.0 picomatch: 4.0.5 source-map: 0.7.6 yargs: 18.1.0 optionalDependencies: - rolldown: 1.2.2 + rolldown: 1.2.3 run-applescript@7.1.0: {} @@ -5647,7 +5647,7 @@ snapshots: temp-dir: 1.0.0 uuid: 3.4.0 - terser@5.49.1: + terser@5.49.2: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.18.0 @@ -5707,13 +5707,13 @@ snapshots: dependencies: typescript: 6.0.3 - typescript-eslint@8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3): + typescript-eslint@8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.8.0(supports-color@10.2.2) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + eslint: 10.8.1(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -5758,7 +5758,7 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1)): + vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2)): dependencies: '@types/imagemin': 7.0.1 '@types/imagemin-gifsicle': 7.0.4 @@ -5783,11 +5783,11 @@ snapshots: imagemin-webp: 6.1.0 jpegtran-bin: 6.0.1 pathe: 0.2.0 - vite: 8.2.0(@types/node@26.1.2)(terser@5.49.1) + vite: 8.2.1(@types/node@26.2.0)(terser@5.49.2) transitivePeerDependencies: - supports-color - vite-prerender-plugin@0.5.13(vite@8.2.0(@types/node@26.1.2)(terser@5.49.1)): + vite-prerender-plugin@0.5.13(vite@8.2.1(@types/node@26.2.0)(terser@5.49.2)): dependencies: kolorist: 1.8.0 magic-string: 0.30.21 @@ -5795,19 +5795,19 @@ snapshots: simple-code-frame: 1.3.0 source-map: 0.7.6 stack-trace: 1.0.0 - vite: 8.2.0(@types/node@26.1.2)(terser@5.49.1) + vite: 8.2.1(@types/node@26.2.0)(terser@5.49.2) - vite@8.2.0(@types/node@26.1.2)(terser@5.49.1): + vite@8.2.1(@types/node@26.2.0)(terser@5.49.2): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.2.2 + postcss: 8.5.26 + rolldown: 1.2.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 fsevents: 2.3.3 - terser: 5.49.1 + terser: 5.49.2 which-typed-array@1.1.22: dependencies: diff --git a/interface/src/app/main/Commands.tsx b/interface/src/app/main/Commands.tsx index 43cf2e380..bb3771d16 100644 --- a/interface/src/app/main/Commands.tsx +++ b/interface/src/app/main/Commands.tsx @@ -3,11 +3,9 @@ import { useBlocker } from 'react-router'; import AddIcon from '@mui/icons-material/Add'; import CancelIcon from '@mui/icons-material/Cancel'; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import WarningIcon from '@mui/icons-material/Warning'; -import { Box, Button, IconButton, Typography } from '@mui/material'; +import { Box, Button, Typography } from '@mui/material'; -import { callAction } from '@/api/app'; import { Body, Cell, @@ -48,7 +46,7 @@ const DEFAULT_COMMAND_ITEM: Omit = { const commandsTheme = { Table: ` - --data-table-library_grid-template-columns: repeat(1, minmax(100px, 1fr)) repeat(1, minmax(100px, 1fr)) 160px 32px; + --data-table-library_grid-template-columns: repeat(1, minmax(100px, 1fr)) repeat(1, minmax(100px, 1fr)) 160px; `, BaseRow: ` font-size: 14px; @@ -143,21 +141,6 @@ const Commands = () => { } }; - const { send: executeCommand } = useRequest( - (id: string) => callAction({ action: 'executeCommand', param: id }), - { immediate: false } - ) - .onSuccess(() => { - toast.success(LL.EXECUTE_COMMAND_SENT()); - }) - .onError((error) => { - toast.error(String(error.error?.message || 'An error occurred')); - }); - - const execute = async (name: string) => { - await executeCommand(name); - }; - const editItem = (ci: CommandItem) => { setCreating(false); setSelectedItem(ci); @@ -222,7 +205,6 @@ const Commands = () => { {LL.COMMAND(0)} {LL.VALUE(0)} {LL.NAME(0)} - @@ -231,16 +213,6 @@ const Commands = () => { {ci.cmd} {ci.value} {ci.name} - - {numChanges === 0 && ( - execute(ci.name)} - style={{ backgroundColor: 'transparent' }} - > - - - )} - ))} diff --git a/interface/src/app/main/CommandsDialog.tsx b/interface/src/app/main/CommandsDialog.tsx index 5e7c7bc80..cd4c93faf 100644 --- a/interface/src/app/main/CommandsDialog.tsx +++ b/interface/src/app/main/CommandsDialog.tsx @@ -95,7 +95,7 @@ const CommandsDialog = ({ { immediate: false } ) .onSuccess(() => { - toast.success(LL.EXECUTE_COMMAND_SENT()); + toast.success(LL.COMMAND_SENT()); }) .onError((error) => { toast.error(String(error.error?.message || 'An error occurred')); @@ -121,8 +121,8 @@ const CommandsDialog = ({ return ( - {creating ? `${LL.ADD(1)} ${LL.NEW(0)}` : LL.EDIT()}  - {LL.COMMAND(1)} + {creating ? `${LL.ADD(1)} ` : LL.UPDATE() + '/' + LL.RUN()}  + {LL.COMMAND(0)} - {LL.EXECUTE()} + {LL.RUN()} )} diff --git a/interface/src/app/main/Dashboard.tsx b/interface/src/app/main/Dashboard.tsx index 2bbcebcea..7c531c15e 100644 --- a/interface/src/app/main/Dashboard.tsx +++ b/interface/src/app/main/Dashboard.tsx @@ -334,7 +334,7 @@ const Dashboard = memo(() => { (di.dv.v === '' || di.dv.v === undefined ? ( editDashboardValue(di)} style={{ backgroundColor: 'transparent' }} > @@ -402,7 +402,10 @@ const Dashboard = memo(() => { {selectedDashboardItem && selectedDashboardItem.dv && ( setDeviceValueDialogOpen(false)} + onClose={() => { + setDeviceValueDialogOpen(false); + setSelectedDashboardItem(undefined); + }} onSave={deviceValueDialogSave} selectedItem={selectedDashboardItem.dv} writeable={true} diff --git a/interface/src/app/main/Devices.tsx b/interface/src/app/main/Devices.tsx index 9916d2ea7..9d53d0429 100644 --- a/interface/src/app/main/Devices.tsx +++ b/interface/src/app/main/Devices.tsx @@ -592,6 +592,7 @@ const Devices = memo(() => { const deviceValueDialogClose = () => { setDeviceValueDialogOpen(false); + setSelectedDeviceValue(undefined); if (selectedDevice !== undefined) { void sendDeviceData(selectedDevice); } diff --git a/interface/src/app/main/DevicesDialog.tsx b/interface/src/app/main/DevicesDialog.tsx index 96d095ad6..1feb0ee30 100644 --- a/interface/src/app/main/DevicesDialog.tsx +++ b/interface/src/app/main/DevicesDialog.tsx @@ -30,7 +30,7 @@ import { ValidationError, validate } from 'validators'; import type Schema from 'validators/schema'; import type { ValidateFieldsError } from 'validators/schema'; -import { DeviceValueUOM, DeviceValueUOM_s } from './types'; +import { DeviceValueUOM, DeviceValueUOM_s, getDeviceValueKind } from './types'; import type { DeviceValue } from './types'; interface DevicesDialogProps { @@ -70,7 +70,7 @@ const DevicesDialog = ({ { immediate: false } ) .onSuccess(() => { - toast.success(LL.EXECUTE_COMMAND_SENT()); + toast.success(LL.COMMAND_SENT()); }) .onError((error) => { toast.error(String(error.error?.message || 'An error occurred')); @@ -119,18 +119,20 @@ const DevicesDialog = ({ return undefined; }; - const isCommand = - (selectedItem.v === '' || selectedItem.v === undefined) && - Boolean(selectedItem.c); - const isSchedulerImmediate = selectedItem.v === undefined; - const dialogTitle = isCommand - ? isSchedulerImmediate - ? LL.EXECUTE() + ' ' + LL.SCHEDULE(0) - : LL.RUN_COMMAND() - : writeable - ? LL.CHANGE_VALUE() - : LL.VALUE(0); - const buttonLabel = isCommand ? LL.EXECUTE() : LL.UPDATE(); + const kind = getDeviceValueKind(selectedItem); + + const dialogTitle = + kind === 'command' || kind === 'deviceCommand' + ? LL.RUN() + ' ' + LL.COMMAND(0) + : kind === 'scheduler' + ? LL.UPDATE() + ' ' + LL.SCHEDULE(0) + : writeable + ? LL.CHANGE_VALUE() + : LL.VALUE(0); + + const buttonLabel = + kind === 'command' || kind === 'deviceCommand' ? LL.RUN() : LL.UPDATE(); + const helperText = showHelperText(editItem); const valueLabel = LL.VALUE(0); @@ -142,7 +144,7 @@ const DevicesDialog = ({ {editItem.id.slice(2)} - {!isSchedulerImmediate && ( + {kind !== 'command' && ( {editItem.l ? ( @@ -227,11 +229,15 @@ const DevicesDialog = ({ diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index 1281ee90a..70d91b29d 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -210,6 +210,18 @@ export enum DeviceValueUOM { HERTZ } +export type DeviceValueKind = + 'value' | 'scheduler' | 'command' | 'deviceCommand' | 'deviceEntity'; + +export function getDeviceValueKind(item: DeviceValue): DeviceValueKind { + if (!item.c) return 'value'; + if (item.v !== undefined && item.l !== undefined && item.u === undefined) + return 'scheduler'; + if (item.v === undefined && item.l === undefined) return 'command'; + if (item.u === DeviceValueUOM.NONE) return 'deviceCommand'; + return 'deviceEntity'; +} + export const DeviceValueUOM_s = [ '', '°C', diff --git a/interface/src/i18n/cs/index.ts b/interface/src/i18n/cs/index.ts index 7879115bd..cb79ed473 100644 --- a/interface/src/i18n/cs/index.ts +++ b/interface/src/i18n/cs/index.ts @@ -37,13 +37,11 @@ const cs: Translation = { VALUE: '{{value|Value}}', DEVICES: 'Zařízení', SENSORS: 'Senzory', - RUN_COMMAND: 'Zavolat příkaz', CHANGE_VALUE: 'Změnit hodnotu', CANCEL: 'Zrušit', REMOVE_ALL: 'Odebrat vše', APPLY_CHANGES: 'Použít změny ({0})', UPDATE: 'Aktualizovat', - EXECUTE: 'Provést', REMOVE: 'Odebrat', PROBLEM_UPDATING: 'Problém s aktualizací', PROBLEM_LOADING: 'Problém s načítáním', @@ -368,7 +366,8 @@ const cs: Translation = { COMMANDS: 'Příkazy', COMMANDS_UPDATED: 'Příkazy byly aktualizovány', COMMANDS_HELP_1: 'Definujte vlastní příkazy pro magistrali EMS', - EXECUTE_COMMAND_SENT: 'Příkaz odeslán', + COMMAND_SENT: 'Příkaz odeslán', + RUN: 'Spustit příkaz' }; export default cs; diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index 27e58a5ce..223afbf59 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -37,13 +37,11 @@ const de: Translation = { VALUE: '{{wert|Wert}}', DEVICES: 'Geräte', SENSORS: 'Sensoren', - RUN_COMMAND: 'Befehl ausführen', CHANGE_VALUE: 'Wert ändern', CANCEL: 'Abbrechen', REMOVE_ALL: 'Alles entfernen', APPLY_CHANGES: 'Änderungen anwenden ({0})', UPDATE: 'Aktualisieren', - EXECUTE: 'Ausführen', REMOVE: 'Entfernen', PROBLEM_UPDATING: 'Problem beim Aktualisieren', PROBLEM_LOADING: 'Problem beim Laden', @@ -368,7 +366,8 @@ const de: Translation = { COMMANDS: 'Befehle', COMMANDS_UPDATED: 'Befehle wurden aktualisiert', COMMANDS_HELP_1: 'Definieren Sie eigene Befehle für die EMS-Magistral', - EXECUTE_COMMAND_SENT: 'Befehl gesendet', + COMMAND_SENT: 'Befehl gesendet', + RUN: 'Befehl ausführen' }; export default de; diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index a8860affb..d140dea12 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -37,13 +37,11 @@ const en: Translation = { VALUE: '{{value|Value}}', DEVICES: 'Devices', SENSORS: 'Sensors', - RUN_COMMAND: 'Call Command', CHANGE_VALUE: 'Change Value', CANCEL: 'Cancel', REMOVE_ALL: 'Remove All', APPLY_CHANGES: 'Apply Changes ({0})', UPDATE: 'Update', - EXECUTE: 'Execute', REMOVE: 'Remove', PROBLEM_UPDATING: 'Problem updating', PROBLEM_LOADING: 'Problem loading', @@ -368,7 +366,8 @@ const en: Translation = { COMMANDS: 'Commands', COMMANDS_UPDATED: 'Commands updated', COMMANDS_HELP_1: 'Define reusable named commands below. These can be executed from the console, API/MQTT, or referenced by the Scheduler', - EXECUTE_COMMAND_SENT: 'Command sent', + COMMAND_SENT: 'Command sent', + RUN: 'Run' }; export default en; diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index 29209ec39..b61ac6828 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -37,13 +37,11 @@ const fr: Translation = { VALUE: 'Valeur', DEVICES: 'Appareils', SENSORS: 'Capteurs', - RUN_COMMAND: 'Lancer une commande', CHANGE_VALUE: 'Changer la valeur', CANCEL: 'Annuler', REMOVE_ALL: 'Supprimer tout', APPLY_CHANGES: 'Appliquer les changements ({0})', UPDATE: 'Update', - EXECUTE: 'Execute', REMOVE: 'Enlever', PROBLEM_UPDATING: 'Problème lors de la mise à jour', PROBLEM_LOADING: 'Problème lors du chargement', @@ -368,7 +366,8 @@ const fr: Translation = { COMMANDS: 'Commandes', COMMANDS_UPDATED: 'Commandes mises à jour', COMMANDS_HELP_1: 'Définir des commandes personnalisées pour la magistral EMS', - EXECUTE_COMMAND_SENT: 'Commande envoyée', + COMMAND_SENT: 'Commande envoyée', + RUN: 'Exécuter une commande' }; export default fr; diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts index 0e63ea8b1..34d510e0d 100644 --- a/interface/src/i18n/it/index.ts +++ b/interface/src/i18n/it/index.ts @@ -37,13 +37,11 @@ const it: Translation = { VALUE: '{{valore|Valore}}', DEVICES: 'Dispositivi', SENSORS: 'Sensori', - RUN_COMMAND: 'Esegui', CHANGE_VALUE: 'Cambia Valore', CANCEL: 'Annulla', REMOVE_ALL: 'Rimuovi tutto', APPLY_CHANGES: 'Applica Cambiamenti ({0})', UPDATE: 'Update', - EXECUTE: 'Execute', REMOVE: 'Elimina', PROBLEM_UPDATING: 'Problema aggiornamento', PROBLEM_LOADING: 'Problema caricamento', @@ -368,7 +366,8 @@ const it: Translation = { COMMANDS: 'Comandi', COMMANDS_UPDATED: 'Comandi aggiornati', COMMANDS_HELP_1: 'Definisci comandi personalizzati per la magistrali EMS', - EXECUTE_COMMAND_SENT: 'Comando inviato', + COMMAND_SENT: 'Comando inviato', + RUN: 'Esegui comando' }; export default it; diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 27f2fad10..35db7369b 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -37,13 +37,11 @@ const nl: Translation = { VALUE: '{{waarde|Waarde}}', DEVICES: 'Apparaten', SENSORS: 'Sensoren', - RUN_COMMAND: 'Call commando', CHANGE_VALUE: 'Wijzig waarde', CANCEL: 'Annuleren', REMOVE_ALL: 'Verwijderen alles', APPLY_CHANGES: 'Aanpassen ({0})', UPDATE: 'Update', - EXECUTE: 'Uitvoeren', REMOVE: 'Verwijderen', PROBLEM_UPDATING: 'Probleem met updaten', PROBLEM_LOADING: 'Probleem met laden', @@ -368,7 +366,8 @@ const nl: Translation = { COMMANDS: 'Commando\'s', COMMANDS_UPDATED: 'Commando\'s bijgewerkt', COMMANDS_HELP_1: 'Definieer hergebruikbare benoemde commando\'s hieronder. Deze kunnen worden uitgevoerd vanuit de console, API/MQTT, of worden aangeroepen door de Scheduler', - EXECUTE_COMMAND_SENT: 'Commando verzonden', + COMMAND_SENT: 'Commando verzonden', + RUN: 'Commando uitvoeren' }; export default nl; diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index 8c9dbc723..506781527 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -37,13 +37,11 @@ const no: Translation = { VALUE: '{{verdi|Verdi}}', DEVICES: 'Enheter', SENSORS: 'Sensorer', - RUN_COMMAND: 'Kjør kommando', CHANGE_VALUE: 'Endre Verdi', CANCEL: 'Avbryt', REMOVE_ALL: 'Fjern alles', APPLY_CHANGES: 'Utfør endringer({0})', UPDATE: 'Oppdater', - EXECUTE: 'Utfør', REMOVE: 'Fjern', PROBLEM_UPDATING: 'Problem med oppdatering', PROBLEM_LOADING: 'Problem med opplasting', @@ -368,7 +366,8 @@ const no: Translation = { COMMANDS: 'Kommandoer', COMMANDS_UPDATED: 'Kommandoer oppdatert', COMMANDS_HELP_1: 'Definer egne kommandoer for EMS-Magistral', - EXECUTE_COMMAND_SENT: 'Kommando sendt', + COMMAND_SENT: 'Kommando sendt', + RUN: 'Kjør' }; export default no; diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index fa434ef57..bdb22f3fe 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -37,13 +37,11 @@ const pl: BaseTranslation = { VALUE: '{{W|w|w}}artość', DEVICES: 'Urządzenia', SENSORS: 'Czujniki', - RUN_COMMAND: 'Wykonaj komendę', CHANGE_VALUE: 'Zmiana wartości', CANCEL: 'Anuluj', REMOVE_ALL: 'Usuń wszystko', APPLY_CHANGES: 'Zapisz zmiany ({0})', UPDATE: 'Zmień', - EXECUTE: 'Wykonaj', REMOVE: 'Usuń', PROBLEM_UPDATING: 'Problem z uaktualnieniem!', PROBLEM_LOADING: 'Problem z załadowaniem!', @@ -368,7 +366,8 @@ const pl: BaseTranslation = { COMMANDS: 'Komendy', COMMANDS_UPDATED: 'Komendy zostały zaktualizowane', COMMANDS_HELP_1: 'Zdefiniuj niestandardowe komendy dla magistrali EMS', - EXECUTE_COMMAND_SENT: 'Komenda wysłana', + COMMAND_SENT: 'Komenda wysłana', + RUN: 'Wykonaj' }; export default pl; diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts index fc8ead5a2..5a65edf9f 100644 --- a/interface/src/i18n/sk/index.ts +++ b/interface/src/i18n/sk/index.ts @@ -37,13 +37,11 @@ const sk: Translation = { VALUE: '{{hodnota|Hodnota}}', DEVICES: 'Zariadenia', SENSORS: 'Snímače', - RUN_COMMAND: 'Volať príkaz', CHANGE_VALUE: 'Zmena hodnoty', CANCEL: 'Zrušiť', REMOVE_ALL: 'Odstrániť všetko', APPLY_CHANGES: 'Aplikovať zmeny ({0})', UPDATE: 'Aktualizovať', - EXECUTE: 'Spustiť', REMOVE: 'Odstrániť', PROBLEM_UPDATING: 'Problém s aktualizáciou', PROBLEM_LOADING: 'Problém s načítaním', @@ -368,7 +366,8 @@ const sk: Translation = { COMMANDS: 'Príkazy', COMMANDS_UPDATED: 'Príkazy aktualizované', COMMANDS_HELP_1: 'Definujte vlastné príkazy pre magistrali EMS', - EXECUTE_COMMAND_SENT: 'Príkaz odoslaný', + COMMAND_SENT: 'Príkaz odoslaný', + RUN: 'Volať príkaz' }; export default sk; diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index d67f7328e..c933d48cd 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -37,13 +37,11 @@ const sv: Translation = { VALUE: '{{värde|Värde}}', DEVICES: 'Enheter', SENSORS: 'Sensorer', - RUN_COMMAND: 'Kör kommando', CHANGE_VALUE: 'Ändra värde', CANCEL: 'Avbryt', REMOVE_ALL: 'Ta bort allt', APPLY_CHANGES: 'Utför ändringar ({0})', UPDATE: 'Uppdatera', - EXECUTE: 'Utför', REMOVE: 'Ta bort', PROBLEM_UPDATING: 'Problem vid uppdatering', PROBLEM_LOADING: 'Problem vid hämtning', @@ -368,7 +366,8 @@ const sv: Translation = { COMMANDS: 'Kommandon', COMMANDS_UPDATED: 'Kommandon uppdaterade', COMMANDS_HELP_1: 'Definiera egna kommandon för EMS-Magistral', - EXECUTE_COMMAND_SENT: 'Kommando skickat', + COMMAND_SENT: 'Kommando skickat', + RUN: 'Kör kommando' }; export default sv; diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index 0a9000979..bfa15079e 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -37,13 +37,11 @@ const tr: Translation = { VALUE: '{{değer|Değer}}', DEVICES: 'Cihazlar', SENSORS: 'Sensörler', - RUN_COMMAND: 'Çalıştırma Komutu', CHANGE_VALUE: 'Değeri Değiştir', CANCEL: 'İptal', REMOVE_ALL: 'Tümünü Kaldır', APPLY_CHANGES: 'Değişiklikleri Uygula ({0})', UPDATE: 'Update', - EXECUTE: 'Uygulamak', REMOVE: 'Kaldır', PROBLEM_UPDATING: 'Güncelleme Sorunu', PROBLEM_LOADING: 'Yükleme Sorunu', @@ -368,7 +366,8 @@ const tr: Translation = { COMMANDS: 'Komutlar', COMMANDS_UPDATED: 'Komutlar güncellendi', COMMANDS_HELP_1: 'Özel komutları EMS hattına tanımlayın', - EXECUTE_COMMAND_SENT: 'Komut gönderildi', + COMMAND_SENT: 'Komut gönderildi', + RUN: 'Komutu çalıştır' }; export default tr; diff --git a/lib_standalone/ESP32React.h b/lib_standalone/ESP32React.h index 0d499c783..d3bd07d92 100644 --- a/lib_standalone/ESP32React.h +++ b/lib_standalone/ESP32React.h @@ -22,6 +22,10 @@ #define NTP_SETTINGS_FILE "/config/ntpSettings.json" #define EMSESP_SETTINGS_FILE "/config/emsespSettings.json" +// AP provision modes, mirrors src/ESP32React/APSettingsService.h +#define AP_MODE_DISCONNECTED 1 +#define AP_MODE_NEVER 2 + class DummySettings { public: // SYSTEM @@ -81,7 +85,7 @@ class DummySettings { String password = "ems-esp"; // AP - uint8_t provisionMode = 0; + uint8_t provisionMode = AP_MODE_DISCONNECTED; // always // NTP String server = "pool.ntp.org"; diff --git a/mock-api/package.json b/mock-api/package.json index 241cd644c..e830ca9b9 100644 --- a/mock-api/package.json +++ b/mock-api/package.json @@ -18,5 +18,5 @@ "@trivago/prettier-plugin-sort-imports": "^6.0.2", "prettier": "^3.9.6" }, - "packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee" + "packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1" } diff --git a/project-words.txt b/project-words.txt index 152abaa8d..70eb94d0a 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1343,4 +1343,5 @@ optimisations IILE Sumr eraseap -roomtempoffset \ No newline at end of file +roomtempoffset +dhcpc \ No newline at end of file diff --git a/scripts/generate_test_api.py b/scripts/generate_test_api.py index c66cbcc85..1b7228dd8 100644 --- a/scripts/generate_test_api.py +++ b/scripts/generate_test_api.py @@ -7,7 +7,7 @@ Workflow: 3. Write that block to test/test_api/test_api.h. 4. Run `pio run -e native-test -t exec`. -run with `python3 scripts/generate_test_api.py` +run with `python3 ./scripts/generate_test_api.py` """ import subprocess diff --git a/src/ESP32React/APSettingsService.cpp b/src/ESP32React/APSettingsService.cpp index 05d02a110..d53017b9c 100644 --- a/src/ESP32React/APSettingsService.cpp +++ b/src/ESP32React/APSettingsService.cpp @@ -31,7 +31,8 @@ StateUpdateResult APSettings::update(JsonObject root, APSettings & settings) { APSettings newSettings{}; newSettings.provisionMode = static_cast(root["provision_mode"] | FACTORY_AP_PROVISION_MODE); - switch (settings.provisionMode) { + // coerce removed/invalid modes (e.g. legacy AP_MODE_ALWAYS = 0) to the current default + switch (newSettings.provisionMode) { case AP_MODE_DISCONNECTED: case AP_MODE_NEVER: break; @@ -53,11 +54,17 @@ StateUpdateResult APSettings::update(JsonObject root, APSettings & settings) { return StateUpdateResult::UNCHANGED; } - settings = newSettings; + const uint8_t previousProvisionMode = settings.provisionMode; + settings = newSettings; - // if the AP mode has changed, force a disconnect and reconnect - if (settings.provisionMode != newSettings.provisionMode) { - emsesp::EMSESP::network_.reconnect(); + // FS load at boot uses this updater before Network::begin(); skip reconnect on that first apply. + // Subsequent provision-mode changes (UI/API) must reload Network's cached AP settings. + static bool appliedOnce = false; + const bool modeChanged = previousProvisionMode != settings.provisionMode; + if (appliedOnce && modeChanged) { + emsesp::EMSESP::network_.schedule_reconnect(); } + appliedOnce = true; + return StateUpdateResult::CHANGED; } diff --git a/src/ESP32React/APSettingsService.h b/src/ESP32React/APSettingsService.h index 6c280875b..3d8e389e9 100644 --- a/src/ESP32React/APSettingsService.h +++ b/src/ESP32React/APSettingsService.h @@ -51,7 +51,7 @@ enum APNetworkStatus { ACTIVE = 0, INACTIVE }; class APSettings { public: - uint8_t provisionMode = FACTORY_AP_PROVISION_MODE; // 0 = on, 2 = off + uint8_t provisionMode = FACTORY_AP_PROVISION_MODE; // 1 = on disconnect, 2 = off String ssid = FACTORY_AP_SSID; String password = FACTORY_AP_PASSWORD; uint8_t channel = FACTORY_AP_CHANNEL; diff --git a/src/ESP32React/UploadFileService.cpp b/src/ESP32React/UploadFileService.cpp index 766d45946..d369d874c 100644 --- a/src/ESP32React/UploadFileService.cpp +++ b/src/ESP32React/UploadFileService.cpp @@ -94,7 +94,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri } #endif // it's firmware - initialize the ArduinoOTA updater - emsesp::EMSESP::logger().info("Uploading firmware file %s (size: %d KB). Please wait...", filename.c_str(), filesize / 1024); + emsesp::EMSESP::logger().info("Uploading firmware file %s (size: %dKB). Please wait...", filename.c_str(), filesize / 1024); // turn off UART to prevent interference with the upload emsesp::EMSuart::stop(); @@ -111,7 +111,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri } } else if (_is_filesystem) { // LittleFS filesystem image - flash directly to the spiffs/littlefs partition - emsesp::EMSESP::logger().info("Uploading filesystem image %s (size: %u KB). Please wait...", filename.c_str(), static_cast(filesize / 1024)); + emsesp::EMSESP::logger().info("Uploading filesystem image %s (size: %uKB). Please wait...", filename.c_str(), static_cast(filesize / 1024)); emsesp::EMSuart::stop(); LittleFS.end(); // unmount LittleFS before we overwrite the partition under it diff --git a/src/core/analogsensor.cpp b/src/core/analogsensor.cpp index 8007cc507..6c604d083 100644 --- a/src/core/analogsensor.cpp +++ b/src/core/analogsensor.cpp @@ -102,7 +102,7 @@ void AnalogSensor::start(const bool factory_settings) { Command::add( EMSdevice::DeviceType::ANALOGSENSOR, F_(setvalue), - [&](const char * value, const int8_t id, JsonObject) { return command_setvalue(value, id); }, + [this](const char * value, const int8_t id, JsonObject) { return command_setvalue(value, id); }, FL_(setiovalue_cmd), CommandFlag::ADMIN_ONLY); @@ -195,7 +195,7 @@ void AnalogSensor::reload(bool get_nvs) { Command::add( EMSdevice::DeviceType::ANALOGSENSOR, sensor.name, - [&](const char * value, const int8_t, JsonObject) { return command_setvalue(value, sensor.gpio); }, + [this, gpio = sensor.gpio](const char * value, const int8_t, JsonObject) { return command_setvalue(value, gpio); }, sensor.type == AnalogType::COUNTER || (sensor.type >= AnalogType::CNT_0 && sensor.type <= AnalogType::CNT_2) ? FL_(counter) : sensor.type == AnalogType::DIGITAL_OUT ? FL_(digital_out) : sensor.type == AnalogType::RGB ? FL_(RGB) diff --git a/src/core/command.cpp b/src/core/command.cpp index cd5d9a791..7f106f107 100644 --- a/src/core/command.cpp +++ b/src/core/command.cpp @@ -492,7 +492,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha } // add a command to the list, which does not return json -void Command::add(const uint8_t device_type, const uint8_t device_id, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags) { +void Command::add(const uint8_t device_type, const uint8_t device_id, const char * cmd, const cmd_function_p & cb, const char * const * description, uint8_t flags) { // if the command already exists for that device type don't add it if (find_command(device_type, device_id, cmd, flags) != nullptr) { return; @@ -508,13 +508,13 @@ void Command::add(const uint8_t device_type, const uint8_t device_id, const char // add a command with no json output // system/temperature/analog devices uses device_id 0 -void Command::add(const uint8_t device_type, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags) { +void Command::add(const uint8_t device_type, const char * cmd, const cmd_function_p & cb, const char * const * description, uint8_t flags) { add(device_type, 0, cmd, cb, description, flags); } // add a command to the list, which does return a json object as output // these commands bypass the readonly check (they are actions, not entity setters) -void Command::add_json(const uint8_t device_type, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags) { +void Command::add_json(const uint8_t device_type, const char * cmd, const cmd_function_p & cb, const char * const * description, uint8_t flags) { // if the command already exists for that device type don't add it if (find_command(device_type, 0, cmd, flags) != nullptr) { return; @@ -828,7 +828,7 @@ uint8_t Command::json_message(uint8_t error_code, const char * message, const Js // e.g. //one/two////three/// becomes /one/two/three std::string SUrlParser::path() { std::string s = "/"; // set up the beginning slash - for (const std::string & f : m_folders) { + for (const auto & f : m_folders) { s += f; s += "/"; } diff --git a/src/core/command.h b/src/core/command.h index 9c7cb50e1..b135a59db 100644 --- a/src/core/command.h +++ b/src/core/command.h @@ -67,13 +67,13 @@ class Command { cmd_function_p cmdfunction_; const char * const * description_; - CmdFunction(const uint8_t device_type, - const uint8_t device_id, - const uint8_t flags, - const bool has_json_output, - const char * cmd, - const cmd_function_p cmdfunction, - const char * const * description) + CmdFunction(const uint8_t device_type, + const uint8_t device_id, + const uint8_t flags, + const bool has_json_output, + const char * cmd, + const cmd_function_p & cmdfunction, + const char * const * description) : device_type_(device_type) , device_id_(device_id) , flags_(flags) @@ -105,22 +105,22 @@ class Command { static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id = -1); // with normal call back function taking a value and id - static void add(const uint8_t device_type, - const uint8_t device_id, - const char * cmd, - const cmd_function_p cb, - const char * const * description, - uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT); + static void add(const uint8_t device_type, + const uint8_t device_id, + const char * cmd, + const cmd_function_p & cb, + const char * const * description, + uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT); // same for system/temperature/analog devices static void - add(const uint8_t device_type, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT); + add(const uint8_t device_type, const char * cmd, const cmd_function_p & cb, const char * const * description, uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT); // command that writes a JSON object as its output; bypasses the readonly check - static void add_json(const uint8_t device_type, - const char * cmd, - const cmd_function_p cb, - const char * const * description, - uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT); + static void add_json(const uint8_t device_type, + const char * cmd, + const cmd_function_p & cb, + const char * const * description, + uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT); static void reserve(size_t num) { cmdfunctions_.reserve(num); diff --git a/src/core/console.cpp b/src/core/console.cpp index 20ebbd420..7a5e3de4e 100644 --- a/src/core/console.cpp +++ b/src/core/console.cpp @@ -199,10 +199,9 @@ static void setup_commands(std::shared_ptr const & commands) { } }); - commands->add_command(ShellContext::MAIN, - CommandFlags::ADMIN, - string_vector{F_(wifi), F_(reconnect)}, - [](Shell &, const std::vector &) { EMSESP::network_.reconnect(); }); + commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, string_vector{F_(wifi), F_(reconnect)}, [](Shell &, const std::vector &) { + EMSESP::network_.reconnect(); + }); // // SET commands @@ -367,9 +366,7 @@ static void setup_commands(std::shared_ptr const & commands) { // EMS device commands // - commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, {F_(scan)}, [](Shell &, const std::vector &) { - EMSESP::scan_devices(); - }); + commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, {F_(scan)}, [](Shell &, const std::vector &) { EMSESP::scan_devices(); }); /* removed scan deep commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, {F_(scan)}, {F_(deep_optional)}, [](Shell & shell, const std::vector & arguments) { diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index 409d26bf5..5fa829408 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -52,7 +52,7 @@ bool EMSdevice::has_entities() const { } bool found = false; EMSESP::webCustomizationService.read([&](WebCustomization & settings) { - for (EntityCustomization entityCustomization : settings.entityCustomizations) { + for (const auto & entityCustomization : settings.entityCustomizations) { if (entityCustomization.device_id == device_id() && entityCustomization.entity_ids.size()) { found = true; break; @@ -564,17 +564,17 @@ void EMSdevice::register_telegram_type(const uint16_t telegram_type_id, const ch // add to device value library, also know now as a "device entity" // this function will also apply any customizations to the entity -void EMSdevice::add_device_value(int8_t tag, // to be used to group mqtt together, either as separate topics as a nested object - void * value_p, // pointer to the value from the .h file - uint8_t type, // one of DeviceValueType - const char * const ** options, // options for enum, which are translated as a list of lists - const char * const * options_single, // list of names - int8_t numeric_operator, // to divide or multiply, see DeviceValueNumOps:: - const char * const * name, // list of names, including shortname and translations - uint8_t uom, // unit of measure from DeviceValueUOM - const cmd_function_p f, // command function pointer - int16_t min, // min allowed value - uint32_t max // max allowed value +void EMSdevice::add_device_value(int8_t tag, // to be used to group mqtt together, either as separate topics as a nested object + void * value_p, // pointer to the value from the .h file + uint8_t type, // one of DeviceValueType + const char * const ** options, // options for enum, which are translated as a list of lists + const char * const * options_single, // list of names + int8_t numeric_operator, // to divide or multiply, see DeviceValueNumOps:: + const char * const * name, // list of names, including shortname and translations + uint8_t uom, // unit of measure from DeviceValueUOM + const cmd_function_p & f, // command function pointer + int16_t min, // min allowed value + uint32_t max // max allowed value ) { uint8_t state = DeviceValueState::DV_DEFAULT; // determine state auto custom_fullname = std::string(""); // custom fullname @@ -636,7 +636,7 @@ void EMSdevice::add_device_value(int8_t tag, // to b // scan through customizations to see if it's on the exclusion list by matching the productID and deviceID EMSESP::webCustomizationService.read([&](WebCustomization & settings) { - for (EntityCustomization entityCustomization : settings.entityCustomizations) { + for (const auto & entityCustomization : settings.entityCustomizations) { if ((entityCustomization.product_id == product_id()) && (entityCustomization.device_id == device_id())) { char entity[70]; if (tag < DeviceValueTAG::TAG_HC1) { @@ -645,7 +645,7 @@ void EMSdevice::add_device_value(int8_t tag, // to b snprintf(entity, sizeof(entity), "%s/%s", tag_to_mqtt(tag), short_name); } - for (const std::string & entity_id : entityCustomization.entity_ids) { + for (const auto & entity_id : entityCustomization.entity_ids) { // if there is an appended custom name, strip it to get the true entity name // and extract the new custom name auto custom_name_pos = entity_id.find('|'); @@ -700,93 +700,93 @@ void EMSdevice::erase_device_values() { } // single list of options -void EMSdevice::register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const * options_single, - const char * const * name, - uint8_t uom, - const cmd_function_p f) { +void EMSdevice::register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const * options_single, + const char * const * name, + uint8_t uom, + const cmd_function_p & f) { // create a multi-list from the options add_device_value(tag, value_p, type, nullptr, options_single, 0, name, uom, f, 0, 0); }; // single list of options, with no translations, with min and max -void EMSdevice::register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const * options_single, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max) { +void EMSdevice::register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const * options_single, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max) { // create a multi-list from the options add_device_value(tag, value_p, type, nullptr, options_single, 0, name, uom, f, min, max); }; -void EMSdevice::register_device_value(int8_t tag, - void * value_p, - uint8_t type, - int8_t numeric_operator, - const char * const * name, - uint8_t uom, - const cmd_function_p f) { +void EMSdevice::register_device_value(int8_t tag, + void * value_p, + uint8_t type, + int8_t numeric_operator, + const char * const * name, + uint8_t uom, + const cmd_function_p & f) { add_device_value(tag, value_p, type, nullptr, nullptr, numeric_operator, name, uom, f, 0, 0); } -void EMSdevice::register_device_value(int8_t tag, - void * value_p, - uint8_t type, - int8_t numeric_operator, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max) { +void EMSdevice::register_device_value(int8_t tag, + void * value_p, + uint8_t type, + int8_t numeric_operator, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max) { add_device_value(tag, value_p, type, nullptr, nullptr, numeric_operator, name, uom, f, min, max); } // no options, no function -void EMSdevice::register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p f) { +void EMSdevice::register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p & f) { add_device_value(tag, value_p, type, nullptr, nullptr, 0, name, uom, f, 0, 0); }; // no options, with min/max -void EMSdevice::register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max) { +void EMSdevice::register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max) { add_device_value(tag, value_p, type, nullptr, nullptr, 0, name, uom, f, min, max); }; // function with min and max values // adds a new command to the command list // in this function we separate out the short and long names and take any translations -void EMSdevice::register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const ** options, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max) { +void EMSdevice::register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const ** options, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max) { add_device_value(tag, value_p, type, options, nullptr, 0, name, uom, f, min, max); } // function with no min and max values (set to 0) -void EMSdevice::register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const ** options, - const char * const * name, - uint8_t uom, - const cmd_function_p f) { +void EMSdevice::register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const ** options, + const char * const * name, + uint8_t uom, + const cmd_function_p & f) { add_device_value(tag, value_p, type, options, nullptr, 0, name, uom, f, 0, 0); } @@ -1210,10 +1210,10 @@ void EMSdevice::generate_values_web_customization(JsonArray output) { // this is when the mask has it's high bit (0x80) set // https://github.com/emsesp/EMS-ESP32/issues/891 EMSESP::webCustomizationService.read([&](WebCustomization & settings) { - for (EntityCustomization entityCustomization : settings.entityCustomizations) { + for (const auto & entityCustomization : settings.entityCustomizations) { if (entityCustomization.device_id == device_id()) { // entity_ids is a list of all entities with the mask prefixed in the string - for (const std::string & entity_id : entityCustomization.entity_ids) { + for (const auto & entity_id : entityCustomization.entity_ids) { uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str()); if (mask & 0x80) { JsonObject obj = output.add(); diff --git a/src/core/emsdevice.h b/src/core/emsdevice.h index ec136b938..fa21a8744 100644 --- a/src/core/emsdevice.h +++ b/src/core/emsdevice.h @@ -290,78 +290,79 @@ class EMSdevice { void generate_values_web(JsonObject output, const bool is_dashboard = false); void generate_values_web_customization(JsonArray output); - void add_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const ** options, - const char * const * options_single, - int8_t numeric_operator, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max); + void add_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const ** options, + const char * const * options_single, + int8_t numeric_operator, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max); - void register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const ** options, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max); + void register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const ** options, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max); void erase_device_values(); void - register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom, const cmd_function_p f); + register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom, const cmd_function_p & f); void register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom); - void register_device_value(int8_t tag, - void * value_p, - uint8_t type, - int8_t numeric_operator, - const char * const * name, - uint8_t uom, - const cmd_function_p f = nullptr); + void register_device_value(int8_t tag, + void * value_p, + uint8_t type, + int8_t numeric_operator, + const char * const * name, + uint8_t uom, + const cmd_function_p & f = nullptr); - void register_device_value(int8_t tag, - void * value_p, - uint8_t type, - int8_t numeric_operator, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max); + void register_device_value(int8_t tag, + void * value_p, + uint8_t type, + int8_t numeric_operator, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max); // single list of options - void register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const * options_single, - const char * const * name, - uint8_t uom, - const cmd_function_p f = nullptr); + void register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const * options_single, + const char * const * name, + uint8_t uom, + const cmd_function_p & f = nullptr); // single list of options, with no translations, with min and max - void register_device_value(int8_t tag, - void * value_p, - uint8_t type, - const char * const * options_single, - const char * const * name, - uint8_t uom, - const cmd_function_p f, - int16_t min, - uint32_t max); + void register_device_value(int8_t tag, + void * value_p, + uint8_t type, + const char * const * options_single, + const char * const * name, + uint8_t uom, + const cmd_function_p & f, + int16_t min, + uint32_t max); // no options, optional function f - void register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p f = nullptr); + void register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p & f = nullptr); // no options, with min/max - void register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p f, int16_t min, uint32_t max); + void + register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p & f, int16_t min, uint32_t max); void write_command(const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid) const; void write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value, const uint16_t validate_typeid) const; diff --git a/src/core/emsdevicevalue.h b/src/core/emsdevicevalue.h index 58154771d..dbc037e3e 100644 --- a/src/core/emsdevicevalue.h +++ b/src/core/emsdevicevalue.h @@ -192,6 +192,11 @@ class DeviceValue { int16_t min; // min range uint32_t max; // max range + // optional custom name from customization. Allocated on heap only when actually set, + // so unnamed entities (the vast majority) don't pay for an inline std::string. + // Prefer custom_fullname() / set_custom_fullname() / has_custom_fullname() over direct access. + std::unique_ptr custom_fullname_; + DeviceValue(uint8_t device_type, // EMSdevice::DeviceType int8_t tag, // DeviceValueTAG::* void * value_p, // pointer to variable of any type @@ -245,11 +250,6 @@ class DeviceValue { static const char * const * DeviceValueTAG_s[]; static const char * const DeviceValueTAG_mqtt[]; static uint8_t NUM_TAGS; // # tags - - private: - // optional custom name from customization. Allocated on heap only when actually set, - // so unnamed entities (the vast majority) don't pay for an inline std::string. - std::unique_ptr custom_fullname_; }; }; // namespace emsesp diff --git a/src/core/emsesp.cpp b/src/core/emsesp.cpp index 7e7e2461c..a3efbcf5b 100644 --- a/src/core/emsesp.cpp +++ b/src/core/emsesp.cpp @@ -54,19 +54,19 @@ bool EMSESP::entity_compaction_pending_ = false; AsyncWebServer webServer(80); #if defined(EMSESP_STANDALONE) -FS dummyFS; -auto & fsRef = dummyFS; +FS dummyFS; +FS * const fsRef = &dummyFS; #else -auto & fsRef = LittleFS; +FS * const fsRef = &LittleFS; #endif -ESP32React EMSESP::esp32React(&webServer, &fsRef); -WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &fsRef, EMSESP::esp32React.getSecurityManager()); -WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, &fsRef, EMSESP::esp32React.getSecurityManager()); -WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, &fsRef, EMSESP::esp32React.getSecurityManager()); -WebCommandService EMSESP::webCommandService = WebCommandService(&webServer, &fsRef, EMSESP::esp32React.getSecurityManager()); -WebCustomEntityService EMSESP::webCustomEntityService = WebCustomEntityService(&webServer, &fsRef, EMSESP::esp32React.getSecurityManager()); -WebModulesService EMSESP::webModulesService = WebModulesService(&webServer, &fsRef, EMSESP::esp32React.getSecurityManager()); +ESP32React EMSESP::esp32React(&webServer, fsRef); +WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, fsRef, EMSESP::esp32React.getSecurityManager()); +WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, fsRef, EMSESP::esp32React.getSecurityManager()); +WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, fsRef, EMSESP::esp32React.getSecurityManager()); +WebCommandService EMSESP::webCommandService = WebCommandService(&webServer, fsRef, EMSESP::esp32React.getSecurityManager()); +WebCustomEntityService EMSESP::webCustomEntityService = WebCustomEntityService(&webServer, fsRef, EMSESP::esp32React.getSecurityManager()); +WebModulesService EMSESP::webModulesService = WebModulesService(&webServer, fsRef, EMSESP::esp32React.getSecurityManager()); WebActivityService EMSESP::webActivityService = WebActivityService(&webServer, EMSESP::esp32React.getSecurityManager()); WebStatusService EMSESP::webStatusService = WebStatusService(&webServer, EMSESP::esp32React.getSecurityManager()); @@ -184,13 +184,13 @@ void EMSESP::erase_device(const uint8_t type_id) { } } - // clears list of recognized devices, entities and telegrams - void EMSESP::clear_all_devices() { - for (auto & emsdevice : emsdevices) { +// clears list of recognized devices, entities and telegrams +void EMSESP::clear_all_devices() { + for (const auto & emsdevice : emsdevices) { emsdevice->erase_device_values(); } emsdevices.clear(); - } +} // called from EMSdevice/Command whenever an entity or telegram handler is registered. // Devices reserve their value/telegram vectors generously (to avoid realloc storms while @@ -1046,7 +1046,7 @@ void EMSESP::process_UBADevices(const std::shared_ptr & telegram } } else if (device_exists(device_id) && !device_hasEntities(device_id)) { LOG_DEBUG("Remove EMS device with ID 0x%02X", device_id); - erase_device(device_id); // + erase_device(device_id); // } next_byte = next_byte >> 1; // advance 1 bit } @@ -1484,7 +1484,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const // see if we have a custom device name in our Customizations list, and if so set it webCustomizationService.read([&](WebCustomization const & settings) { - for (EntityCustomization e : settings.entityCustomizations) { + for (const auto & e : settings.entityCustomizations) { if ((e.device_id == device_id) && (e.product_id == product_id)) { LOG_DEBUG("Have customizations for %s with deviceID 0x%02X productID %d", e.custom_name.c_str(), device_id, product_id); emsdevices.back()->custom_name(e.custom_name); diff --git a/src/core/emsesp.h b/src/core/emsesp.h index f8ada23c5..3111395ff 100644 --- a/src/core/emsesp.h +++ b/src/core/emsesp.h @@ -101,7 +101,7 @@ class Module {}; // forward declaration // for Command Function callbacks (Command::cmd_function_p). The unified callback takes a JsonObject // output which entity/setter commands ignore. -#define MAKE_CF_CB(__f) [&](const char * value, const int8_t id, JsonObject output) { return __f(value, id); } +#define MAKE_CF_CB(__f) [this](const char * value, const int8_t id, JsonObject output) { return __f(value, id); } namespace emsesp { diff --git a/src/core/led.cpp b/src/core/led.cpp index e56a41143..958ac9ac3 100644 --- a/src/core/led.cpp +++ b/src/core/led.cpp @@ -67,7 +67,6 @@ bool LED::loop(uint8_t healthcheck, bool button_busy) { set_led(Color::YELLOW); // Yellow } else { reset_led(); - previous_healthcheck_ = System::HEALTHCHECK_RESET; } return false; } @@ -75,39 +74,39 @@ bool LED::loop(uint8_t healthcheck, bool button_busy) { // check the system health. // Set the sequence accordingly, only if the healthcheck is not 0 and has changed if (healthcheck != previous_healthcheck_) { - previous_healthcheck_ = healthcheck; - color_steps_[0] = Color::OFF; - color_steps_[1] = Color::OFF; - color_steps_[2] = Color::OFF; + color_steps_[0] = Color::OFF; + color_steps_[1] = Color::OFF; + color_steps_[2] = Color::OFF; // if the healthcheck is 0, i.e. system is healthy, reset the LED if (healthcheck == 0) { reset_led(); - return false; + } else { + // 1 flash (blue) is the EMS bus is not connected + // 2 flashes (red, red) if the network (wifi or ethernet) is not connected + // 3 flashes (red, red, blue) is both the bus and the network are not connected + bool no_network = (healthcheck & System::HEALTHCHECK_NO_NETWORK) == System::HEALTHCHECK_NO_NETWORK; + bool no_bus = (healthcheck & System::HEALTHCHECK_NO_BUS) == System::HEALTHCHECK_NO_BUS; + + // set step 1 + if (no_network) { + color_steps_[0] = Color::RED; // red, no network + } else if (no_bus) { + color_steps_[0] = Color::BLUE; // blue, no bus + } + + // set step 2 + if (no_network) { + color_steps_[1] = Color::RED; // red, no network + } + + // set step 3 + if (no_network && no_bus) { + color_steps_[2] = Color::BLUE; // blue, no network and no bus + } } - // 1 flash (blue) is the EMS bus is not connected - // 2 flashes (red, red) if the network (wifi or ethernet) is not connected - // 3 flashes (red, red, blue) is both the bus and the network are not connected - bool no_network = (healthcheck & System::HEALTHCHECK_NO_NETWORK) == System::HEALTHCHECK_NO_NETWORK; - bool no_bus = (healthcheck & System::HEALTHCHECK_NO_BUS) == System::HEALTHCHECK_NO_BUS; - - // set step 1 - if (no_network) { - color_steps_[0] = Color::RED; // red, no network - } else if (no_bus) { - color_steps_[0] = Color::BLUE; // blue, no bus - } - - // set step 2 - if (no_network) { - color_steps_[1] = Color::RED; // red, no network - } - - // set step 3 - if (no_network && no_bus) { - color_steps_[2] = Color::BLUE; // blue, no network and no bus - } + previous_healthcheck_ = healthcheck; // must be set after reset_led(), which invalidates it } // there's nothing to flash, so leave the LED in the state reset_led() gave it. @@ -131,6 +130,10 @@ void LED::reset_led() { color_steps_[1] = Color::OFF; color_steps_[2] = Color::OFF; + // invalidate the last health state so the sequence is rebuilt on the next check, + // needed because the color steps above have been cleared + previous_healthcheck_ = System::HEALTHCHECK_RESET; + // rewind the sequence so it starts from the top if the health degrades again led_long_timer_ = 1; // 1 will kick it off immediately led_short_timer_ = 0; diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index 529d0e7c0..2ac2349f3 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -313,6 +313,31 @@ void Mqtt::on_publish(uint16_t packetId) const { LOG_DEBUG("Packet %d sent successful", packetId); } +// gracefully disconnect from the broker by sending a proper MQTT DISCONNECT packet. +// this is called just before a restart so the broker ends our session cleanly +// (avoids a lingering session that shows up as "session taken over" on reconnect). +// +// the disconnect is deferred inside espMqttClient - the DISCONNECT packet is only +// queued and actually sent when loop() runs the state machine. On PSRAM boards the +// client has its own task that keeps pumping loop(), so it flushes on its own. On +// non-PSRAM boards (UseInternalTask::NO) nothing drives loop() once we're mid-restart, +// so we pump it here until the client is fully disconnected, bounded by a timeout. +void Mqtt::disconnect() { + if (!mqttClient_) { + return; + } + + mqttClient_->disconnect(); // graceful - queues the DISCONNECT packet + + if (EMSESP::system_.PSram() == 0) { + uint32_t start = uuid::get_uptime(); + while (!mqttClient_->disconnected() && (uuid::get_uptime() - start) < MQTT_DISCONNECT_TIMEOUT) { + mqttClient_->loop(); + delay(2); + } + } +} + // called when MQTT settings have changed via the MQTT Settings or Application Settings Web pages void Mqtt::reset_mqtt() { if (!enabled()) { @@ -447,8 +472,8 @@ bool Mqtt::get_publish_onchange(uint8_t device_type) { return false; } void Mqtt::on_disconnect(espMqttClientTypes::DisconnectReason reason) { - // only show the error once, not every 2 seconds - if (!connecting_) { + // only show the error once on the first connect failure + if (connectcount_) { return; } connecting_ = false; @@ -587,9 +612,11 @@ void Mqtt::ha_status() { // These come from the info MQTT topic - and handled in the publish_ha_sensor_config function publish_system_ha_sensor_config(DeviceValueType::STRING, "Version", "version", DeviceValueUOM::NONE); - publish_system_ha_sensor_config(DeviceValueType::STRING, "Boot time", "bootTime", DeviceValueUOM::UPTIME); + publish_system_ha_sensor_config(DeviceValueType::STRING, + "Uptime", + "bootTime", + DeviceValueUOM::UPTIME); // called Uptime in HA and derives value from bootTime element in info topic } - // add sub or pub task to the queue. // the base is not included in the topic bool Mqtt::queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain) { @@ -988,8 +1015,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // set the entity_id. This is breaking change in HA 2025.10.0 - see https://github.com/home-assistant/core/pull/151775 // extract the string from topic up to the / using std::string + // it must be lowercase (although HA will slugify it and not reject it) std::string topic_str(topic); - doc["def_ent_id"] = topic_str.substr(0, topic_str.find("/")) + "." + uniq_id; + doc["def_ent_id"] = Helpers::toLower(topic_str.substr(0, topic_str.find("/"))) + "." + Helpers::toLower(uniq_id); char sample_val[30] = "0"; // sample, correct(!) entity value, used only to prevent warning/error in HA if real value is not published yet @@ -1067,7 +1095,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str()); // Override - there are exceptions for DeviceType::SYSTEM, which uses the heartbeat topic - // and when fetching the version and bootTime we want to take this from the info topic instead + // and when fetching the version and uptime we want to take this from the info topic instead if (device_type == EMSdevice::DeviceType::SYSTEM) { // handle the exceptions if (strncmp(entity, "version", 7) == 0) { @@ -1100,12 +1128,17 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery if (discovery_type() == discoveryType::HOMEASSISTANT) { + // handle special cases for value_template depending on the uom if (uom == DeviceValueUOM::TIMESTAMP) { - // special case for timestamp, using "value_template": "{{ (value_json.bootTime | as_datetime).isoformat() }}", + // uptime char val_tpl[100]; snprintf(val_tpl, sizeof(val_tpl), "{{ (value_json.%s | as_datetime).isoformat() }}", entity); doc["val_tpl"] = val_tpl; + } else if (uom == DeviceValueUOM::UPTIME) { + // uptime + doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else '' }}"; } else { + // default doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else " + sample_val + "}}"; } add_ha_avty_section(doc.as(), stat_t, val_cond); // adds availability section @@ -1141,7 +1174,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // https://developers.home-assistant.io/docs/core/entity/sensor/ // Home Assistant tracks the min, max and mean value during the statistics period // For statistics, the sensor must have state_class set to either MEASUREMENT, TOTAL or TOTAL_INCREASING -// When set to MEASUREMENT the device_class must cannot be DATE, ENUM, ENERGY, GAS, MONETARY, TIMESTAMP, VOLUME or WATER +// When set to MEASUREMENT the device_class must cannot be DATE, ENUM, ENERGY, GAS, MONETARY, TIMESTAMP, UPTIME, VOLUME or WATER // void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8_t type, const uint8_t uom, const char * entity, bool display_only) { if (device_type == EMSdevice::DeviceType::SYSTEM) { @@ -1280,7 +1313,7 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 doc[sc_ha] = sc_ha_measurement; doc[dc_ha] = "timestamp"; break; - case DeviceValueUOM::UPTIME: + case DeviceValueUOM::UPTIME: // no state class for uptime doc[dc_ha] = "uptime"; break; case DeviceValueUOM::MV: @@ -1602,9 +1635,6 @@ void Mqtt::add_ha_avty_section(JsonObject doc, const char * state_t, const char avty_json["t"] = state; snprintf(tpl, sizeof(tpl), "{{'online' if %s else 'offline'}}", cond1); avty_json["val_tpl"] = tpl; - if (!avty.add(avty_json)) { - LOG_WARNING("Failed to add availability condition 1 (low memory)"); - } } // condition 2 @@ -1613,9 +1643,6 @@ void Mqtt::add_ha_avty_section(JsonObject doc, const char * state_t, const char avty_json["t"] = state; snprintf(tpl, sizeof(tpl), "{{'online' if %s else 'offline'}}", cond2); avty_json["val_tpl"] = tpl; - if (!avty.add(avty_json)) { - LOG_WARNING("Failed to add availability condition 2 (low memory)"); - } } // negative condition @@ -1624,9 +1651,6 @@ void Mqtt::add_ha_avty_section(JsonObject doc, const char * state_t, const char avty_json["t"] = state; snprintf(tpl, sizeof(tpl), "{{'offline' if %s else 'online'}}", negcond); avty_json["val_tpl"] = tpl; - if (!avty.add(avty_json)) { - LOG_WARNING("Failed to add negative availability condition (low memory)"); - } } } diff --git a/src/core/mqtt.h b/src/core/mqtt.h index 21a1cfcc7..46e69478c 100644 --- a/src/core/mqtt.h +++ b/src/core/mqtt.h @@ -61,8 +61,9 @@ class Mqtt { enum Operation : uint8_t { PUBLISH, SUBSCRIBE, UNSUBSCRIBE }; enum NestedFormat : uint8_t { NESTED = 1, SINGLE }; - static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 128; // fixed, not a user setting anymore - static constexpr uint16_t MQTT_QUEUE_MAX_SIZE = 300; + static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 128; // fixed, not a user setting anymore + static constexpr uint16_t MQTT_QUEUE_MAX_SIZE = 300; + static constexpr uint32_t MQTT_DISCONNECT_TIMEOUT = 1000; // max ms to flush a graceful disconnect on non-PSRAM boards static void on_connect(); static void on_disconnect(espMqttClientTypes::DisconnectReason reason); @@ -132,11 +133,7 @@ class Mqtt { return mqttClient_ ? mqttClient_->connected() : false; } - static void disconnect() { - if (mqttClient_) { - mqttClient_->disconnect(); - }; - } + static void disconnect(); static bool enabled() { return mqtt_enabled_; diff --git a/src/core/network.cpp b/src/core/network.cpp index 467171626..ebeacb6d5 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -249,11 +249,12 @@ void Network::reconnect() { // pick the first phase that has the hardware/config to even be attempted on this device. // boards without an Ethernet PHY skip straight to WIFI; without a configured SSID, straight to AP +// (unless AP provision mode is Never — then stay on WIFI and keep waiting) NetPhase Network::initialPhase() const { if (phy_type_ != PHY_type::PHY_TYPE_NONE) { return NetPhase::ETHERNET; } - if (!ssid_.isEmpty()) { + if (!ssid_.isEmpty() || ap_provisionMode_ == AP_MODE_NEVER) { return NetPhase::WIFI; } return NetPhase::AP; @@ -262,6 +263,12 @@ NetPhase Network::initialPhase() const { // network loop, looking for new and disconnecting networks void Network::loop() { #ifndef EMSESP_STANDALONE + // a reconnect was requested from another task, run it here where nothing is mid-request + if (reconnect_pending_) { + reconnect_pending_ = false; + reconnect(); + } + // if we already have a Wifi or Ethernet connection then re-check every NETWORK_RECONNECTION_DELAY_LONG, otherwise NETWORK_RECONNECTION_DELAY_SHORT const unsigned long currentMillis = uuid::get_uptime_ms(); const uint32_t reconnectDelay = @@ -522,6 +529,22 @@ void Network::startWIFI() { #endif } +#if CONFIG_IDF_TARGET_ESP32 && !defined(EMSESP_STANDALONE) +// Unlike WiFi, which stamps the hostname onto the STA netif as it is created, the Arduino ETH class +// leaves it unset, so lwIP falls back to CONFIG_LWIP_LOCAL_HOSTNAME ("tasmota" in the SDK we build +// against). esp_eth_start() polls the PHY synchronously, so on a warm reboot where the link never +// dropped the DHCP DISCOVER can go out before ETH.begin() has even returned. This handler is +// registered ahead of the esp_netif glue and therefore runs first, applying the hostname before the +// netif is handed to lwIP. +static char eth_hostname[33] = {0}; + +static void ethApplyHostname(void * /*arg*/, esp_event_base_t /*base*/, int32_t /*event_id*/, void * /*event_data*/) { + if (eth_hostname[0] != '\0' && ETH.netif() != nullptr) { + esp_netif_set_hostname(ETH.netif(), eth_hostname); + } +} +#endif + // Ethernet management // Brings up the ETH driver / netif exactly once. After ETH.begin() returns true the driver // continues to run autonomously (link negotiation, DHCP, etc) @@ -533,8 +556,12 @@ void Network::startEthernet() { return; } - // already up and running, nothing to do - if (ethernet_connect_pending_ || ethernet_connected()) { + // The driver is installed once and then left alone. Re-running the bring-up on a live interface + // tears it down behind our back: pinMode() on the PHY power pin hands the pin from the Ethernet + // bus back to GPIO, which fires the peripheral manager's deinit callback and calls ETH.end(), + // destroying the netif, its event group and the event handlers from the main loop task while the + // arduino_events task may be dispatching a link-up on the other core. + if (ethernet_started_ || ethernet_connect_pending_ || ethernet_connected()) { return; } @@ -577,16 +604,45 @@ void Network::startEthernet() { eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720 : (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110 : ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110) + + // must be in place before ETH.begin() so it beats the esp_netif glue to the START event + strlcpy(eth_hostname, hostname_.c_str(), sizeof(eth_hostname)); + if (!eth_hostname_handler_registered_ && esp_event_handler_register(ETH_EVENT, ETHERNET_EVENT_START, ethApplyHostname, nullptr) == ESP_OK) { + eth_hostname_handler_registered_ = true; + } + if (ETH.begin(type, eth_phy_addr_, 23, 18, eth_power_, (eth_clock_mode_t)eth_clock_mode_)) { - LOG_DEBUG("Ethernet module found - starting"); + // LOG_DEBUG("Ethernet module found - initialising"); + ethernet_started_ = true; ethernet_connect_pending_ = true; - ETH.setHostname(hostname_.c_str()); // Push hostname to the ETH netif immediately after it's created + + // check whether DHCP already announced us under lwIP's default hostname, before we correct it + esp_netif_t * eth_netif = ETH.netif(); + const char * old_hostname = nullptr; + bool stale_dhcp = false; + if (eth_netif != nullptr && !staticIPConfig_) { + esp_netif_dhcp_status_t dhcp_status = ESP_NETIF_DHCP_INIT; + stale_dhcp = esp_netif_get_hostname(eth_netif, &old_hostname) == ESP_OK && old_hostname != nullptr && hostname_ != old_hostname + && esp_netif_dhcpc_get_status(eth_netif, &dhcp_status) == ESP_OK && dhcp_status == ESP_NETIF_DHCP_STARTED; + } + + if (!ETH.setHostname(hostname_.c_str())) { + LOG_WARNING("Failed to set Ethernet hostname to %s", hostname_.c_str()); + } + + // the DHCP server has already been given the wrong name, so re-run the client to re-announce + if (stale_dhcp) { + LOG_DEBUG("Restarting Ethernet DHCP client to announce hostname %s", hostname_.c_str()); + esp_netif_dhcpc_stop(eth_netif); + esp_netif_dhcpc_start(eth_netif); + } + ETH.enableIPv6(true); if (staticIPConfig_) { ETH.config(localIP_, gatewayIP_, subnetMask_, dnsIP1_, dnsIP2_); } } else { - LOG_ERROR("Failed to start Ethernet module"); + LOG_ERROR("Failed to start Ethernet"); } #endif #endif @@ -631,6 +687,11 @@ void Network::findNetworks() { } const NetIface candidate = iface_from_desc(desc); + // AP is disabled in settings — don't treat SoftAP as a usable network + if (candidate == NetIface::AP && ap_provisionMode_ == AP_MODE_NEVER) { + continue; + } + if (iface_priority(candidate) <= iface_priority(best_iface)) { continue; // already have something at least as good } @@ -709,12 +770,17 @@ void Network::findNetworks() { : "AP"); // give up on this interface and try the next one. ETHERNET -> WIFI (or straight to AP if no SSID configured); WIFI -> AP. + // AP_MODE_NEVER: never promote to the AP phase — keep waiting on the current interface. if (connect_retry_ >= MAX_NETWORK_RECONNECTION_ATTEMPTS && phase_ != NetPhase::AP) { if (phase_ == NetPhase::ETHERNET) { if (ssid_.isEmpty()) { if (!ethernet_ever_connected_) { - LOG_WARNING("Ethernet failed to connect after %u attempts, falling back to AP", connect_retry_); - phase_ = NetPhase::AP; + if (ap_provisionMode_ == AP_MODE_NEVER) { + LOG_WARNING("Ethernet failed to connect after %u attempts (AP provision mode is Never)", connect_retry_); + } else { + LOG_WARNING("Ethernet failed to connect after %u attempts, falling back to AP", connect_retry_); + phase_ = NetPhase::AP; + } } } else { LOG_WARNING("Ethernet failed to connect after %u attempts, switching to WiFi", connect_retry_); @@ -723,8 +789,12 @@ void Network::findNetworks() { ethernet_connect_pending_ = false; } else if (phase_ == NetPhase::WIFI) { if (!wifi_ever_connected_) { - LOG_WARNING("WiFi failed to connect after %u attempts, falling back to AP", connect_retry_); - phase_ = NetPhase::AP; + if (ap_provisionMode_ == AP_MODE_NEVER) { + LOG_WARNING("WiFi failed to connect after %u attempts (AP provision mode is Never)", connect_retry_); + } else { + LOG_WARNING("WiFi failed to connect after %u attempts, falling back to AP", connect_retry_); + phase_ = NetPhase::AP; + } } wifi_connect_pending_ = false; } diff --git a/src/core/network.h b/src/core/network.h index d081f0232..13ae4ab41 100644 --- a/src/core/network.h +++ b/src/core/network.h @@ -138,6 +138,11 @@ class Network { void reconnect(); + // ask for a reconnect() to run from the next Network::loop() + void schedule_reconnect() { + reconnect_pending_ = true; + } + // map a netif description string (from esp_netif_get_desc) to a NetIface static NetIface iface_from_desc(const char * desc) { if (!desc) { @@ -202,12 +207,15 @@ class Network { volatile bool wifi_connect_pending_ = false; volatile bool ethernet_connect_pending_ = false; + volatile bool reconnect_pending_ = false; // set by schedule_reconnect(), actioned in loop() NetPhase phase_ = NetPhase::ETHERNET; - bool wifi_events_registered_ = false; // ensure WiFi.onEvent() handlers are registered only once across begin()/reconnect() cycles - bool wifi_ever_connected_ = false; // set true once we've successfully obtained an IP - bool ethernet_ever_connected_ = false; // set true once we've successfully obtained an IP + bool wifi_events_registered_ = false; // ensure WiFi.onEvent() handlers are registered only once across begin()/reconnect() cycles + bool eth_hostname_handler_registered_ = false; // for the ETHERNET_EVENT_START hostname handler + bool ethernet_started_ = false; // ETH.begin() succeeded; the driver runs for the lifetime of the boot + bool wifi_ever_connected_ = false; // set true once we've successfully obtained an IP + bool ethernet_ever_connected_ = false; // set true once we've successfully obtained an IP // Network and AP settings bool enableMDNS_; diff --git a/src/core/shower.cpp b/src/core/shower.cpp index cbbafdf61..9c6552c32 100644 --- a/src/core/shower.cpp +++ b/src/core/shower.cpp @@ -36,7 +36,7 @@ void Shower::start() { Command::add_json( EMSdevice::DeviceType::BOILER, F_(coldshot), - [&](const char * value, const int8_t id, JsonObject output) { + [this](const char * value, const int8_t id, JsonObject output) { LOG_INFO("Forcing coldshot..."); if (shower_state_) { output["message"] = "OK"; diff --git a/src/core/system.cpp b/src/core/system.cpp index 6c98ba8c3..75840689b 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -153,10 +153,10 @@ bool System::command_sendmail(const char * value, const int8_t) { bool success = false; #ifndef EMSESP_STANDALONE - WiFiClient * basic_client = new WiFiClient; - ESP_SSLClient * ssl_client = new ESP_SSLClient; - ReadyClient * r_client = new ReadyClient(*ssl_client); - SMTPClient * smtp = new SMTPClient(*r_client); + auto * basic_client = new WiFiClient; + auto * ssl_client = new ESP_SSLClient; + auto * r_client = new ReadyClient(*ssl_client); + auto * smtp = new SMTPClient(*r_client); ssl_client->setClient(basic_client); ssl_client->setInsecure(); @@ -586,7 +586,7 @@ void System::system_restart(const char * partitionname) { } Serial.flush(); // wait for hardware TX buffer to drain - Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1 + Mqtt::disconnect(); // gracefully disconnect MQTT (flushes the DISCONNECT before reboot, needed for QOS1) EMSuart::stop(); // stop UART so there is no interference #ifndef EMSESP_STANDALONE delay(1000); // wait 1 second @@ -1545,7 +1545,7 @@ bool System::check_upgrade() { EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) { if (apSettings.provisionMode == 0) { apSettings.provisionMode = AP_MODE_DISCONNECTED; // AP_MODE_DISCONNECTED is the new default - LOG_INFO("Upgrade: Setting AP provision mode to auto"); + LOG_INFO("Upgrade: Setting AP provision mode to on disconnect"); return StateUpdateResult::CHANGED; } return StateUpdateResult::UNCHANGED; @@ -3248,7 +3248,7 @@ bool System::uploadFirmwareURL(const char * url) { return false; // error } - LOG_INFO("Firmware uploading (size: %d KB) over %s. Please wait...", firmware_size / 1024, is_https ? "HTTPS" : "HTTP"); + LOG_INFO("Firmware uploading (size: %dKB) over %s. Please wait...", firmware_size / 1024, is_https ? "HTTPS" : "HTTP"); Shell::loop_all(); // flush log buffers so latest messages are shown in console diff --git a/src/emsesp_version.h b/src/emsesp_version.h index 8bedaf023..0173e604e 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.9.0-dev.2" +#define EMSESP_APP_VERSION "3.9.0-dev.3" diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index 16086fa49..40e8e72ac 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -317,7 +317,7 @@ void WebCustomizationService::customization_entities(AsyncWebServerRequest * req // add deleted entities from file read([&](WebCustomization & settings) { - for (EntityCustomization entityCustomization : settings.entityCustomizations) { + for (const auto & entityCustomization : settings.entityCustomizations) { if (entityCustomization.device_id == device_id) { for (const auto & entity_id : entityCustomization.entity_ids) { uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str()); diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index d38f13c1a..da7421ece 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -421,9 +421,9 @@ bool WebStatusService::refresh_versions_cache() { #ifdef EMSESP_STANDALONE return false; #else - // detect scheme from EMSESP_VERSIONS_URL (case-insensitive). One code path for HTTP and HTTPS, + // detect scheme from VERSIONS_URL (case-insensitive). One code path for HTTP and HTTPS, // using ESP_SSLClient as a plain TCP passthrough when SSL is disabled. - String url = EMSESP_VERSIONS_URL; + String url = VERSIONS_URL; String lower = url; lower.toLowerCase(); const bool is_https = lower.startsWith("https://"); diff --git a/src/web/WebStatusService.h b/src/web/WebStatusService.h index e4164f8a3..8f1001170 100644 --- a/src/web/WebStatusService.h +++ b/src/web/WebStatusService.h @@ -4,8 +4,6 @@ #define EMSESP_SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus" #define EMSESP_ACTION_SERVICE_PATH "/rest/action" -#define EMSESP_VERSIONS_URL "http://emsesp.org/versions.json" - #include "../core/firmwareVersion.h" #include "../emsesp_version.h" @@ -72,6 +70,8 @@ class WebStatusService { bool refresh_versions_cache(); // does the actual HTTPS fetch + parse, returns true on success + static constexpr const char * VERSIONS_URL = "http://emsesp.org/versions.json"; + static constexpr uint32_t VERSIONS_REFRESH_INTERVAL_MS = 60UL * 60UL * 1000UL; // 1 hour on success static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after failure static constexpr uint32_t VERSIONS_INITIAL_FETCH_DELAY_MS = 5UL * 1000UL; // 5 s after a link comes up diff --git a/test/test_api/test_api.h b/test/test_api/test_api.h index 838fdd97e..1a73f6219 100644 --- a/test/test_api/test_api.h +++ b/test/test_api/test_api.h @@ -110,17 +110,17 @@ void test_22() { } void test_23() { - auto expected_response = "[{\"system\":{\"version\":\"dev\",\"uptime\":\"000+00:00:00.000\",\"uptimeSec\":0,\"resetReason\":\"Unknown / Unknown\",\"txpause\":false,\"gpios_allowed\":\"0, 2, 5, 18, 23\",\"gpios_in_use\":\"0, 2, 5, 18, 23\",\"gpios_available\":\"\"},\"network\":{\"network\":\"WiFi\",\"hostname\":\"ems-esp\",\"RSSI\":-23,\"TxPowerSetting\":0,\"staticIP\":false,\"lowBandwidth\":false,\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"NTPstatus\":\"disconnected\",\"enabled\":true,\"server\":\"pool.ntp.org\",\"tzLabel\":\"Europe/London\",\"NTPStatus\":\"disconnected\"},\"ap\":{\"provisionMode\":\"always\",\"ssid\":\"ems-esp\"},\"mqtt\":{\"MQTTStatus\":\"disconnected\",\"MQTTPublishes\":0,\"MQTTQueued\":0,\"MQTTPublishFails\":0,\"MQTTReconnects\":0,\"enabled\":true,\"clientID\":\"ems-esp\",\"keepAlive\":60,\"cleanSession\":false,\"entityFormat\":1,\"base\":\"ems-esp\",\"discoveryPrefix\":\"homeassistant\",\"discoveryType\":0,\"nestedFormat\":1,\"haEnabled\":true,\"mqttQos\":0,\"mqttRetain\":false,\"publishTimeHeartbeat\":60,\"publishTimeBoiler\":10,\"publishTimeThermostat\":10,\"publishTimeSolar\":10,\"publishTimeMixer\":10,\"publishTimeWater\":0,\"publishTimeOther\":10,\"publishTimeSensor\":10,\"publishSingle\":false,\"publish2command\":false,\"sendResponse\":false},\"syslog\":{\"enabled\":false},\"modbus\":{\"enabled\":false},\"sensor\":{\"temperatureSensors\":3,\"temperatureSensorReads\":0,\"temperatureSensorFails\":0},\"analog\":{\"enabled\":true,\"analogSensors\":5,\"analogSensorReads\":0,\"analogSensorFails\":0},\"api\":{\"APICalls\":0,\"APIFails\":0},\"bus\":{\"busStatus\":\"connected\",\"busProtocol\":\"Buderus\",\"busTelegramsReceived\":8,\"busReads\":0,\"busWrites\":0,\"busIncompleteTelegrams\":0,\"busReadsFailed\":0,\"busWritesFailed\":0,\"busRxLineQuality\":100,\"busTxLineQuality\":100},\"settings\":{\"boardProfile\":\"S32\",\"locale\":\"en\",\"txMode\":5,\"emsBusID\":73,\"showerTimer\":false,\"showerMinDuration\":180,\"showerAlert\":false,\"hideLed\":false,\"noTokenApi\":false,\"readonlyMode\":false,\"fahrenheit\":false,\"dallasParasite\":false,\"boolFormat\":1,\"boolDashboard\":1,\"enumFormat\":1,\"analogEnabled\":true,\"telnetEnabled\":true,\"maxWebLogBuffer\":25,\"modbusEnabled\":false,\"forceHeatingOff\":false,\"developerMode\":false,\"disableReset\":false},\"devices\":[{\"type\":\"boiler\",\"name\":\"My Custom Boiler\",\"deviceID\":\"0x08\",\"productID\":123,\"brand\":\"\",\"version\":\"01.00\",\"entities\":39,\"handlersReceived\":\"0x18\",\"handlersFetched\":\"0x14 0x33\",\"handlersPending\":\"0xBF 0x10 0x11 0xC2 0xC6 0x15 0x1C 0x19 0x1A 0x35 0x34 0x2A 0xD1 0xE3 0xE4 0xE5 0xE9 0x02E0 0x2E 0x3B\"},{\"type\":\"thermostat\",\"name\":\"FW120\",\"deviceID\":\"0x10\",\"productID\":192,\"brand\":\"\",\"version\":\"01.00\",\"entities\":12,\"handlersReceived\":\"0x016F\",\"handlersFetched\":\"0x0170 0x0171\",\"handlersPending\":\"0xA3 0x06 0xA2 0x12 0x13 0x0172 0x0165 0x0168\"},{\"type\":\"temperaturesensor\",\"name\":\"temperaturesensor\",\"entities\":3},{\"type\":\"analogsensor\",\"name\":\"analogsensor\",\"entities\":5},{\"type\":\"scheduler\",\"name\":\"scheduler\",\"entities\":2},{\"type\":\"commands\",\"name\":\"commands\",\"entities\":3},{\"type\":\"custom\",\"name\":\"custom\",\"entities\":4}]}]"; + auto expected_response = "[{\"system\":{\"version\":\"dev\",\"uptime\":\"000+00:00:00.000\",\"uptimeSec\":0,\"resetReason\":\"Unknown / Unknown\",\"txpause\":false,\"gpios_allowed\":\"0, 2, 5, 18, 23\",\"gpios_in_use\":\"0, 2, 5, 18, 23\",\"gpios_available\":\"\"},\"network\":{\"network\":\"WiFi\",\"hostname\":\"ems-esp\",\"RSSI\":-23,\"TxPowerSetting\":0,\"staticIP\":false,\"lowBandwidth\":false,\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"NTPstatus\":\"disconnected\",\"enabled\":true,\"server\":\"pool.ntp.org\",\"tzLabel\":\"Europe/London\",\"NTPStatus\":\"disconnected\"},\"ap\":{\"provisionMode\":\"disconnected\",\"ssid\":\"ems-esp\"},\"mqtt\":{\"MQTTStatus\":\"disconnected\",\"MQTTPublishes\":0,\"MQTTQueued\":0,\"MQTTPublishFails\":0,\"MQTTReconnects\":0,\"enabled\":true,\"clientID\":\"ems-esp\",\"keepAlive\":60,\"cleanSession\":false,\"entityFormat\":1,\"base\":\"ems-esp\",\"discoveryPrefix\":\"homeassistant\",\"discoveryType\":0,\"nestedFormat\":1,\"haEnabled\":true,\"mqttQos\":0,\"mqttRetain\":false,\"publishTimeHeartbeat\":60,\"publishTimeBoiler\":10,\"publishTimeThermostat\":10,\"publishTimeSolar\":10,\"publishTimeMixer\":10,\"publishTimeWater\":0,\"publishTimeOther\":10,\"publishTimeSensor\":10,\"publishSingle\":false,\"publish2command\":false,\"sendResponse\":false},\"syslog\":{\"enabled\":false},\"modbus\":{\"enabled\":false},\"sensor\":{\"temperatureSensors\":3,\"temperatureSensorReads\":0,\"temperatureSensorFails\":0},\"analog\":{\"enabled\":true,\"analogSensors\":5,\"analogSensorReads\":0,\"analogSensorFails\":0},\"api\":{\"APICalls\":0,\"APIFails\":0},\"bus\":{\"busStatus\":\"connected\",\"busProtocol\":\"Buderus\",\"busTelegramsReceived\":8,\"busReads\":0,\"busWrites\":0,\"busIncompleteTelegrams\":0,\"busReadsFailed\":0,\"busWritesFailed\":0,\"busRxLineQuality\":100,\"busTxLineQuality\":100},\"settings\":{\"boardProfile\":\"S32\",\"locale\":\"en\",\"txMode\":5,\"emsBusID\":73,\"showerTimer\":false,\"showerMinDuration\":180,\"showerAlert\":false,\"hideLed\":false,\"noTokenApi\":false,\"readonlyMode\":false,\"fahrenheit\":false,\"dallasParasite\":false,\"boolFormat\":1,\"boolDashboard\":1,\"enumFormat\":1,\"analogEnabled\":true,\"telnetEnabled\":true,\"maxWebLogBuffer\":25,\"modbusEnabled\":false,\"forceHeatingOff\":false,\"developerMode\":false,\"disableReset\":false},\"devices\":[{\"type\":\"boiler\",\"name\":\"My Custom Boiler\",\"deviceID\":\"0x08\",\"productID\":123,\"brand\":\"\",\"version\":\"01.00\",\"entities\":39,\"handlersReceived\":\"0x18\",\"handlersFetched\":\"0x14 0x33\",\"handlersPending\":\"0xBF 0x10 0x11 0xC2 0xC6 0x15 0x1C 0x19 0x1A 0x35 0x34 0x2A 0xD1 0xE3 0xE4 0xE5 0xE9 0x02E0 0x2E 0x3B\"},{\"type\":\"thermostat\",\"name\":\"FW120\",\"deviceID\":\"0x10\",\"productID\":192,\"brand\":\"\",\"version\":\"01.00\",\"entities\":12,\"handlersReceived\":\"0x016F\",\"handlersFetched\":\"0x0170 0x0171\",\"handlersPending\":\"0xA3 0x06 0xA2 0x12 0x13 0x0172 0x0165 0x0168\"},{\"type\":\"temperaturesensor\",\"name\":\"temperaturesensor\",\"entities\":3},{\"type\":\"analogsensor\",\"name\":\"analogsensor\",\"entities\":5},{\"type\":\"scheduler\",\"name\":\"scheduler\",\"entities\":2},{\"type\":\"commands\",\"name\":\"commands\",\"entities\":3},{\"type\":\"custom\",\"name\":\"custom\",\"entities\":4}]}]"; TEST_ASSERT_EQUAL_STRING(expected_response, call_url("/api/system")); } void test_24() { - auto expected_response = "[{\"system\":{\"version\":\"dev\",\"uptime\":\"000+00:00:00.000\",\"uptimeSec\":0,\"resetReason\":\"Unknown / Unknown\",\"txpause\":false,\"gpios_allowed\":\"0, 2, 5, 18, 23\",\"gpios_in_use\":\"0, 2, 5, 18, 23\",\"gpios_available\":\"\"},\"network\":{\"network\":\"WiFi\",\"hostname\":\"ems-esp\",\"RSSI\":-23,\"TxPowerSetting\":0,\"staticIP\":false,\"lowBandwidth\":false,\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"NTPstatus\":\"disconnected\",\"enabled\":true,\"server\":\"pool.ntp.org\",\"tzLabel\":\"Europe/London\",\"NTPStatus\":\"disconnected\"},\"ap\":{\"provisionMode\":\"always\",\"ssid\":\"ems-esp\"},\"mqtt\":{\"MQTTStatus\":\"disconnected\",\"MQTTPublishes\":0,\"MQTTQueued\":0,\"MQTTPublishFails\":0,\"MQTTReconnects\":0,\"enabled\":true,\"clientID\":\"ems-esp\",\"keepAlive\":60,\"cleanSession\":false,\"entityFormat\":1,\"base\":\"ems-esp\",\"discoveryPrefix\":\"homeassistant\",\"discoveryType\":0,\"nestedFormat\":1,\"haEnabled\":true,\"mqttQos\":0,\"mqttRetain\":false,\"publishTimeHeartbeat\":60,\"publishTimeBoiler\":10,\"publishTimeThermostat\":10,\"publishTimeSolar\":10,\"publishTimeMixer\":10,\"publishTimeWater\":0,\"publishTimeOther\":10,\"publishTimeSensor\":10,\"publishSingle\":false,\"publish2command\":false,\"sendResponse\":false},\"syslog\":{\"enabled\":false},\"modbus\":{\"enabled\":false},\"sensor\":{\"temperatureSensors\":3,\"temperatureSensorReads\":0,\"temperatureSensorFails\":0},\"analog\":{\"enabled\":true,\"analogSensors\":5,\"analogSensorReads\":0,\"analogSensorFails\":0},\"api\":{\"APICalls\":0,\"APIFails\":0},\"bus\":{\"busStatus\":\"connected\",\"busProtocol\":\"Buderus\",\"busTelegramsReceived\":8,\"busReads\":0,\"busWrites\":0,\"busIncompleteTelegrams\":0,\"busReadsFailed\":0,\"busWritesFailed\":0,\"busRxLineQuality\":100,\"busTxLineQuality\":100},\"settings\":{\"boardProfile\":\"S32\",\"locale\":\"en\",\"txMode\":5,\"emsBusID\":73,\"showerTimer\":false,\"showerMinDuration\":180,\"showerAlert\":false,\"hideLed\":false,\"noTokenApi\":false,\"readonlyMode\":false,\"fahrenheit\":false,\"dallasParasite\":false,\"boolFormat\":1,\"boolDashboard\":1,\"enumFormat\":1,\"analogEnabled\":true,\"telnetEnabled\":true,\"maxWebLogBuffer\":25,\"modbusEnabled\":false,\"forceHeatingOff\":false,\"developerMode\":false,\"disableReset\":false},\"devices\":[{\"type\":\"boiler\",\"name\":\"My Custom Boiler\",\"deviceID\":\"0x08\",\"productID\":123,\"brand\":\"\",\"version\":\"01.00\",\"entities\":39,\"handlersReceived\":\"0x18\",\"handlersFetched\":\"0x14 0x33\",\"handlersPending\":\"0xBF 0x10 0x11 0xC2 0xC6 0x15 0x1C 0x19 0x1A 0x35 0x34 0x2A 0xD1 0xE3 0xE4 0xE5 0xE9 0x02E0 0x2E 0x3B\"},{\"type\":\"thermostat\",\"name\":\"FW120\",\"deviceID\":\"0x10\",\"productID\":192,\"brand\":\"\",\"version\":\"01.00\",\"entities\":12,\"handlersReceived\":\"0x016F\",\"handlersFetched\":\"0x0170 0x0171\",\"handlersPending\":\"0xA3 0x06 0xA2 0x12 0x13 0x0172 0x0165 0x0168\"},{\"type\":\"temperaturesensor\",\"name\":\"temperaturesensor\",\"entities\":3},{\"type\":\"analogsensor\",\"name\":\"analogsensor\",\"entities\":5},{\"type\":\"scheduler\",\"name\":\"scheduler\",\"entities\":2},{\"type\":\"commands\",\"name\":\"commands\",\"entities\":3},{\"type\":\"custom\",\"name\":\"custom\",\"entities\":4}]}]"; + auto expected_response = "[{\"system\":{\"version\":\"dev\",\"uptime\":\"000+00:00:00.000\",\"uptimeSec\":0,\"resetReason\":\"Unknown / Unknown\",\"txpause\":false,\"gpios_allowed\":\"0, 2, 5, 18, 23\",\"gpios_in_use\":\"0, 2, 5, 18, 23\",\"gpios_available\":\"\"},\"network\":{\"network\":\"WiFi\",\"hostname\":\"ems-esp\",\"RSSI\":-23,\"TxPowerSetting\":0,\"staticIP\":false,\"lowBandwidth\":false,\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"NTPstatus\":\"disconnected\",\"enabled\":true,\"server\":\"pool.ntp.org\",\"tzLabel\":\"Europe/London\",\"NTPStatus\":\"disconnected\"},\"ap\":{\"provisionMode\":\"disconnected\",\"ssid\":\"ems-esp\"},\"mqtt\":{\"MQTTStatus\":\"disconnected\",\"MQTTPublishes\":0,\"MQTTQueued\":0,\"MQTTPublishFails\":0,\"MQTTReconnects\":0,\"enabled\":true,\"clientID\":\"ems-esp\",\"keepAlive\":60,\"cleanSession\":false,\"entityFormat\":1,\"base\":\"ems-esp\",\"discoveryPrefix\":\"homeassistant\",\"discoveryType\":0,\"nestedFormat\":1,\"haEnabled\":true,\"mqttQos\":0,\"mqttRetain\":false,\"publishTimeHeartbeat\":60,\"publishTimeBoiler\":10,\"publishTimeThermostat\":10,\"publishTimeSolar\":10,\"publishTimeMixer\":10,\"publishTimeWater\":0,\"publishTimeOther\":10,\"publishTimeSensor\":10,\"publishSingle\":false,\"publish2command\":false,\"sendResponse\":false},\"syslog\":{\"enabled\":false},\"modbus\":{\"enabled\":false},\"sensor\":{\"temperatureSensors\":3,\"temperatureSensorReads\":0,\"temperatureSensorFails\":0},\"analog\":{\"enabled\":true,\"analogSensors\":5,\"analogSensorReads\":0,\"analogSensorFails\":0},\"api\":{\"APICalls\":0,\"APIFails\":0},\"bus\":{\"busStatus\":\"connected\",\"busProtocol\":\"Buderus\",\"busTelegramsReceived\":8,\"busReads\":0,\"busWrites\":0,\"busIncompleteTelegrams\":0,\"busReadsFailed\":0,\"busWritesFailed\":0,\"busRxLineQuality\":100,\"busTxLineQuality\":100},\"settings\":{\"boardProfile\":\"S32\",\"locale\":\"en\",\"txMode\":5,\"emsBusID\":73,\"showerTimer\":false,\"showerMinDuration\":180,\"showerAlert\":false,\"hideLed\":false,\"noTokenApi\":false,\"readonlyMode\":false,\"fahrenheit\":false,\"dallasParasite\":false,\"boolFormat\":1,\"boolDashboard\":1,\"enumFormat\":1,\"analogEnabled\":true,\"telnetEnabled\":true,\"maxWebLogBuffer\":25,\"modbusEnabled\":false,\"forceHeatingOff\":false,\"developerMode\":false,\"disableReset\":false},\"devices\":[{\"type\":\"boiler\",\"name\":\"My Custom Boiler\",\"deviceID\":\"0x08\",\"productID\":123,\"brand\":\"\",\"version\":\"01.00\",\"entities\":39,\"handlersReceived\":\"0x18\",\"handlersFetched\":\"0x14 0x33\",\"handlersPending\":\"0xBF 0x10 0x11 0xC2 0xC6 0x15 0x1C 0x19 0x1A 0x35 0x34 0x2A 0xD1 0xE3 0xE4 0xE5 0xE9 0x02E0 0x2E 0x3B\"},{\"type\":\"thermostat\",\"name\":\"FW120\",\"deviceID\":\"0x10\",\"productID\":192,\"brand\":\"\",\"version\":\"01.00\",\"entities\":12,\"handlersReceived\":\"0x016F\",\"handlersFetched\":\"0x0170 0x0171\",\"handlersPending\":\"0xA3 0x06 0xA2 0x12 0x13 0x0172 0x0165 0x0168\"},{\"type\":\"temperaturesensor\",\"name\":\"temperaturesensor\",\"entities\":3},{\"type\":\"analogsensor\",\"name\":\"analogsensor\",\"entities\":5},{\"type\":\"scheduler\",\"name\":\"scheduler\",\"entities\":2},{\"type\":\"commands\",\"name\":\"commands\",\"entities\":3},{\"type\":\"custom\",\"name\":\"custom\",\"entities\":4}]}]"; TEST_ASSERT_EQUAL_STRING(expected_response, call_url("/api/system/info")); } void test_25() { - auto expected_response = "[{\"api_data\":\"# HELP emsesp_system_uptimesec uptimeSec\\n# TYPE emsesp_system_uptimesec gauge\\nemsesp_system_uptimesec 0\\n# HELP emsesp_system_txpause txpause\\n# TYPE emsesp_system_txpause gauge\\nemsesp_system_txpause 0\\n# HELP emsesp_system_info info\\n# TYPE emsesp_system_info gauge\\nemsesp_system_info{version=\\\"dev\\\", resetreason=\\\"Unknown / Unknown\\\", gpios_allowed=\\\"0, 2, 5, 18, 23\\\", gpios_in_use=\\\"0, 2, 5, 18, 23\\\"} 1\\n# HELP emsesp_network_rssi RSSI\\n# TYPE emsesp_network_rssi gauge\\nemsesp_network_rssi -23\\n# HELP emsesp_network_txpowersetting TxPowerSetting\\n# TYPE emsesp_network_txpowersetting gauge\\nemsesp_network_txpowersetting 0\\n# HELP emsesp_network_staticip staticIP\\n# TYPE emsesp_network_staticip gauge\\nemsesp_network_staticip 0\\n# HELP emsesp_network_lowbandwidth lowBandwidth\\n# TYPE emsesp_network_lowbandwidth gauge\\nemsesp_network_lowbandwidth 0\\n# HELP emsesp_network_disablesleep disableSleep\\n# TYPE emsesp_network_disablesleep gauge\\nemsesp_network_disablesleep 1\\n# HELP emsesp_network_enablemdns enableMDNS\\n# TYPE emsesp_network_enablemdns gauge\\nemsesp_network_enablemdns 1\\n# HELP emsesp_network_enablecors enableCORS\\n# TYPE emsesp_network_enablecors gauge\\nemsesp_network_enablecors 0\\n# HELP emsesp_network_info info\\n# TYPE emsesp_network_info gauge\\nemsesp_network_info{network=\\\"WiFi\\\", hostname=\\\"ems-esp\\\"} 1\\n# HELP emsesp_ntp_enabled enabled\\n# TYPE emsesp_ntp_enabled gauge\\nemsesp_ntp_enabled 1\\n# HELP emsesp_ntp_info info\\n# TYPE emsesp_ntp_info gauge\\nemsesp_ntp_info{ntpstatus=\\\"disconnected\\\", server=\\\"pool.ntp.org\\\", tzlabel=\\\"Europe/London\\\"} 1\\n# HELP emsesp_ap_info info\\n# TYPE emsesp_ap_info gauge\\nemsesp_ap_info{provisionmode=\\\"always\\\", ssid=\\\"ems-esp\\\"} 1\\n# HELP emsesp_mqtt_mqttpublishes MQTTPublishes\\n# TYPE emsesp_mqtt_mqttpublishes gauge\\nemsesp_mqtt_mqttpublishes 0\\n# HELP emsesp_mqtt_mqttqueued MQTTQueued\\n# TYPE emsesp_mqtt_mqttqueued gauge\\nemsesp_mqtt_mqttqueued 0\\n# HELP emsesp_mqtt_mqttpublishfails MQTTPublishFails\\n# TYPE emsesp_mqtt_mqttpublishfails gauge\\nemsesp_mqtt_mqttpublishfails 0\\n# HELP emsesp_mqtt_mqttreconnects MQTTReconnects\\n# TYPE emsesp_mqtt_mqttreconnects gauge\\nemsesp_mqtt_mqttreconnects 0\\n# HELP emsesp_mqtt_enabled enabled\\n# TYPE emsesp_mqtt_enabled gauge\\nemsesp_mqtt_enabled 1\\n# HELP emsesp_mqtt_keepalive keepAlive\\n# TYPE emsesp_mqtt_keepalive gauge\\nemsesp_mqtt_keepalive 60\\n# HELP emsesp_mqtt_cleansession cleanSession\\n# TYPE emsesp_mqtt_cleansession gauge\\nemsesp_mqtt_cleansession 0\\n# HELP emsesp_mqtt_entityformat entityFormat\\n# TYPE emsesp_mqtt_entityformat gauge\\nemsesp_mqtt_entityformat 1\\n# HELP emsesp_mqtt_discoverytype discoveryType\\n# TYPE emsesp_mqtt_discoverytype gauge\\nemsesp_mqtt_discoverytype 0\\n# HELP emsesp_mqtt_nestedformat nestedFormat\\n# TYPE emsesp_mqtt_nestedformat gauge\\nemsesp_mqtt_nestedformat 1\\n# HELP emsesp_mqtt_haenabled haEnabled\\n# TYPE emsesp_mqtt_haenabled gauge\\nemsesp_mqtt_haenabled 1\\n# HELP emsesp_mqtt_mqttqos mqttQos\\n# TYPE emsesp_mqtt_mqttqos gauge\\nemsesp_mqtt_mqttqos 0\\n# HELP emsesp_mqtt_mqttretain mqttRetain\\n# TYPE emsesp_mqtt_mqttretain gauge\\nemsesp_mqtt_mqttretain 0\\n# HELP emsesp_mqtt_publishtimeheartbeat publishTimeHeartbeat\\n# TYPE emsesp_mqtt_publishtimeheartbeat gauge\\nemsesp_mqtt_publishtimeheartbeat 60\\n# HELP emsesp_mqtt_publishtimeboiler publishTimeBoiler\\n# TYPE emsesp_mqtt_publishtimeboiler gauge\\nemsesp_mqtt_publishtimeboiler 10\\n# HELP emsesp_mqtt_publishtimethermostat publishTimeThermostat\\n# TYPE emsesp_mqtt_publishtimethermostat gauge\\nemsesp_mqtt_publishtimethermostat 10\\n# HELP emsesp_mqtt_publishtimesolar publishTimeSolar\\n# TYPE emsesp_mqtt_publishtimesolar gauge\\nemsesp_mqtt_publishtimesolar 10\\n# HELP emsesp_mqtt_publishtimemixer publishTimeMixer\\n# TYPE emsesp_mqtt_publishtimemixer gauge\\nemsesp_mqtt_publishtimemixer 10\\n# HELP emsesp_mqtt_publishtimewater publishTimeWater\\n# TYPE emsesp_mqtt_publishtimewater gauge\\nemsesp_mqtt_publishtimewater 0\\n# HELP emsesp_mqtt_publishtimeother publishTimeOther\\n# TYPE emsesp_mqtt_publishtimeother gauge\\nemsesp_mqtt_publishtimeother 10\\n# HELP emsesp_mqtt_publishtimesensor publishTimeSensor\\n# TYPE emsesp_mqtt_publishtimesensor gauge\\nemsesp_mqtt_publishtimesensor 10\\n# HELP emsesp_mqtt_publishsingle publishSingle\\n# TYPE emsesp_mqtt_publishsingle gauge\\nemsesp_mqtt_publishsingle 0\\n# HELP emsesp_mqtt_publish2command publish2command\\n# TYPE emsesp_mqtt_publish2command gauge\\nemsesp_mqtt_publish2command 0\\n# HELP emsesp_mqtt_sendresponse sendResponse\\n# TYPE emsesp_mqtt_sendresponse gauge\\nemsesp_mqtt_sendresponse 0\\n# HELP emsesp_mqtt_info info\\n# TYPE emsesp_mqtt_info gauge\\nemsesp_mqtt_info{mqttstatus=\\\"disconnected\\\", clientid=\\\"ems-esp\\\", base=\\\"ems-esp\\\", discoveryprefix=\\\"homeassistant\\\"} 1\\n# HELP emsesp_syslog_enabled enabled\\n# TYPE emsesp_syslog_enabled gauge\\nemsesp_syslog_enabled 0\\n# HELP emsesp_modbus_enabled enabled\\n# TYPE emsesp_modbus_enabled gauge\\nemsesp_modbus_enabled 0\\n# HELP emsesp_sensor_temperaturesensors temperatureSensors\\n# TYPE emsesp_sensor_temperaturesensors gauge\\nemsesp_sensor_temperaturesensors 3\\n# HELP emsesp_sensor_temperaturesensorreads temperatureSensorReads\\n# TYPE emsesp_sensor_temperaturesensorreads gauge\\nemsesp_sensor_temperaturesensorreads 0\\n# HELP emsesp_sensor_temperaturesensorfails temperatureSensorFails\\n# TYPE emsesp_sensor_temperaturesensorfails gauge\\nemsesp_sensor_temperaturesensorfails 0\\n# HELP emsesp_analog_enabled enabled\\n# TYPE emsesp_analog_enabled gauge\\nemsesp_analog_enabled 1\\n# HELP emsesp_analog_analogsensors analogSensors\\n# TYPE emsesp_analog_analogsensors gauge\\nemsesp_analog_analogsensors 5\\n# HELP emsesp_analog_analogsensorreads analogSensorReads\\n# TYPE emsesp_analog_analogsensorreads gauge\\nemsesp_analog_analogsensorreads 0\\n# HELP emsesp_analog_analogsensorfails analogSensorFails\\n# TYPE emsesp_analog_analogsensorfails gauge\\nemsesp_analog_analogsensorfails 0\\n# HELP emsesp_api_apicalls APICalls\\n# TYPE emsesp_api_apicalls gauge\\nemsesp_api_apicalls 0\\n# HELP emsesp_api_apifails APIFails\\n# TYPE emsesp_api_apifails gauge\\nemsesp_api_apifails 0\\n# HELP emsesp_bus_bustelegramsreceived busTelegramsReceived\\n# TYPE emsesp_bus_bustelegramsreceived gauge\\nemsesp_bus_bustelegramsreceived 8\\n# HELP emsesp_bus_busreads busReads\\n# TYPE emsesp_bus_busreads gauge\\nemsesp_bus_busreads 0\\n# HELP emsesp_bus_buswrites busWrites\\n# TYPE emsesp_bus_buswrites gauge\\nemsesp_bus_buswrites 0\\n# HELP emsesp_bus_busincompletetelegrams busIncompleteTelegrams\\n# TYPE emsesp_bus_busincompletetelegrams gauge\\nemsesp_bus_busincompletetelegrams 0\\n# HELP emsesp_bus_busreadsfailed busReadsFailed\\n# TYPE emsesp_bus_busreadsfailed gauge\\nemsesp_bus_busreadsfailed 0\\n# HELP emsesp_bus_buswritesfailed busWritesFailed\\n# TYPE emsesp_bus_buswritesfailed gauge\\nemsesp_bus_buswritesfailed 0\\n# HELP emsesp_bus_busrxlinequality busRxLineQuality\\n# TYPE emsesp_bus_busrxlinequality gauge\\nemsesp_bus_busrxlinequality 100\\n# HELP emsesp_bus_bustxlinequality busTxLineQuality\\n# TYPE emsesp_bus_bustxlinequality gauge\\nemsesp_bus_bustxlinequality 100\\n# HELP emsesp_bus_info info\\n# TYPE emsesp_bus_info gauge\\nemsesp_bus_info{busstatus=\\\"connected\\\", busprotocol=\\\"Buderus\\\"} 1\\n# HELP emsesp_settings_txmode txMode\\n# TYPE emsesp_settings_txmode gauge\\nemsesp_settings_txmode 5\\n# HELP emsesp_settings_emsbusid emsBusID\\n# TYPE emsesp_settings_emsbusid gauge\\nemsesp_settings_emsbusid 73\\n# HELP emsesp_settings_showertimer showerTimer\\n# TYPE emsesp_settings_showertimer gauge\\nemsesp_settings_showertimer 0\\n# HELP emsesp_settings_showerminduration showerMinDuration\\n# TYPE emsesp_settings_showerminduration gauge\\nemsesp_settings_showerminduration 180\\n# HELP emsesp_settings_showeralert showerAlert\\n# TYPE emsesp_settings_showeralert gauge\\nemsesp_settings_showeralert 0\\n# HELP emsesp_settings_hideled hideLed\\n# TYPE emsesp_settings_hideled gauge\\nemsesp_settings_hideled 0\\n# HELP emsesp_settings_notokenapi noTokenApi\\n# TYPE emsesp_settings_notokenapi gauge\\nemsesp_settings_notokenapi 0\\n# HELP emsesp_settings_readonlymode readonlyMode\\n# TYPE emsesp_settings_readonlymode gauge\\nemsesp_settings_readonlymode 0\\n# HELP emsesp_settings_fahrenheit fahrenheit\\n# TYPE emsesp_settings_fahrenheit gauge\\nemsesp_settings_fahrenheit 0\\n# HELP emsesp_settings_dallasparasite dallasParasite\\n# TYPE emsesp_settings_dallasparasite gauge\\nemsesp_settings_dallasparasite 0\\n# HELP emsesp_settings_boolformat boolFormat\\n# TYPE emsesp_settings_boolformat gauge\\nemsesp_settings_boolformat 1\\n# HELP emsesp_settings_booldashboard boolDashboard\\n# TYPE emsesp_settings_booldashboard gauge\\nemsesp_settings_booldashboard 1\\n# HELP emsesp_settings_enumformat enumFormat\\n# TYPE emsesp_settings_enumformat gauge\\nemsesp_settings_enumformat 1\\n# HELP emsesp_settings_analogenabled analogEnabled\\n# TYPE emsesp_settings_analogenabled gauge\\nemsesp_settings_analogenabled 1\\n# HELP emsesp_settings_telnetenabled telnetEnabled\\n# TYPE emsesp_settings_telnetenabled gauge\\nemsesp_settings_telnetenabled 1\\n# HELP emsesp_settings_maxweblogbuffer maxWebLogBuffer\\n# TYPE emsesp_settings_maxweblogbuffer gauge\\nemsesp_settings_maxweblogbuffer 25\\n# HELP emsesp_settings_modbusenabled modbusEnabled\\n# TYPE emsesp_settings_modbusenabled gauge\\nemsesp_settings_modbusenabled 0\\n# HELP emsesp_settings_forceheatingoff forceHeatingOff\\n# TYPE emsesp_settings_forceheatingoff gauge\\nemsesp_settings_forceheatingoff 0\\n# HELP emsesp_settings_developermode developerMode\\n# TYPE emsesp_settings_developermode gauge\\nemsesp_settings_developermode 0\\n# HELP emsesp_settings_disablereset disableReset\\n# TYPE emsesp_settings_disablereset gauge\\nemsesp_settings_disablereset 0\\n# HELP emsesp_settings_info info\\n# TYPE emsesp_settings_info gauge\\nemsesp_settings_info{boardprofile=\\\"S32\\\", locale=\\\"en\\\"} 1\\n# HELP emsesp_device_productid productID\\n# TYPE emsesp_device_productid gauge\\nemsesp_device_productid{type=\\\"boiler\\\", name=\\\"My Custom Boiler\\\", deviceid=\\\"0x08\\\", version=\\\"01.00\\\"} 123\\n# HELP emsesp_device_entities entities\\n# TYPE emsesp_device_entities gauge\\nemsesp_device_entities{type=\\\"boiler\\\", name=\\\"My Custom Boiler\\\", deviceid=\\\"0x08\\\", version=\\\"01.00\\\"} 39\\nemsesp_device_productid{type=\\\"thermostat\\\", name=\\\"FW120\\\", deviceid=\\\"0x10\\\", version=\\\"01.00\\\"} 192\\nemsesp_device_entities{type=\\\"thermostat\\\", name=\\\"FW120\\\", deviceid=\\\"0x10\\\", version=\\\"01.00\\\"} 12\\nemsesp_device_entities{type=\\\"temperaturesensor\\\", name=\\\"temperaturesensor\\\"} 3\\nemsesp_device_entities{type=\\\"analogsensor\\\", name=\\\"analogsensor\\\"} 5\\nemsesp_device_entities{type=\\\"scheduler\\\", name=\\\"scheduler\\\"} 2\\nemsesp_device_entities{type=\\\"commands\\\", name=\\\"commands\\\"} 3\\nemsesp_device_entities{type=\\\"custom\\\", name=\\\"custom\\\"} 4\\n\"}]"; + auto expected_response = "[{\"api_data\":\"# HELP emsesp_system_uptimesec uptimeSec\\n# TYPE emsesp_system_uptimesec gauge\\nemsesp_system_uptimesec 0\\n# HELP emsesp_system_txpause txpause\\n# TYPE emsesp_system_txpause gauge\\nemsesp_system_txpause 0\\n# HELP emsesp_system_info info\\n# TYPE emsesp_system_info gauge\\nemsesp_system_info{version=\\\"dev\\\", resetreason=\\\"Unknown / Unknown\\\", gpios_allowed=\\\"0, 2, 5, 18, 23\\\", gpios_in_use=\\\"0, 2, 5, 18, 23\\\"} 1\\n# HELP emsesp_network_rssi RSSI\\n# TYPE emsesp_network_rssi gauge\\nemsesp_network_rssi -23\\n# HELP emsesp_network_txpowersetting TxPowerSetting\\n# TYPE emsesp_network_txpowersetting gauge\\nemsesp_network_txpowersetting 0\\n# HELP emsesp_network_staticip staticIP\\n# TYPE emsesp_network_staticip gauge\\nemsesp_network_staticip 0\\n# HELP emsesp_network_lowbandwidth lowBandwidth\\n# TYPE emsesp_network_lowbandwidth gauge\\nemsesp_network_lowbandwidth 0\\n# HELP emsesp_network_disablesleep disableSleep\\n# TYPE emsesp_network_disablesleep gauge\\nemsesp_network_disablesleep 1\\n# HELP emsesp_network_enablemdns enableMDNS\\n# TYPE emsesp_network_enablemdns gauge\\nemsesp_network_enablemdns 1\\n# HELP emsesp_network_enablecors enableCORS\\n# TYPE emsesp_network_enablecors gauge\\nemsesp_network_enablecors 0\\n# HELP emsesp_network_info info\\n# TYPE emsesp_network_info gauge\\nemsesp_network_info{network=\\\"WiFi\\\", hostname=\\\"ems-esp\\\"} 1\\n# HELP emsesp_ntp_enabled enabled\\n# TYPE emsesp_ntp_enabled gauge\\nemsesp_ntp_enabled 1\\n# HELP emsesp_ntp_info info\\n# TYPE emsesp_ntp_info gauge\\nemsesp_ntp_info{ntpstatus=\\\"disconnected\\\", server=\\\"pool.ntp.org\\\", tzlabel=\\\"Europe/London\\\"} 1\\n# HELP emsesp_ap_info info\\n# TYPE emsesp_ap_info gauge\\nemsesp_ap_info{provisionmode=\\\"disconnected\\\", ssid=\\\"ems-esp\\\"} 1\\n# HELP emsesp_mqtt_mqttpublishes MQTTPublishes\\n# TYPE emsesp_mqtt_mqttpublishes gauge\\nemsesp_mqtt_mqttpublishes 0\\n# HELP emsesp_mqtt_mqttqueued MQTTQueued\\n# TYPE emsesp_mqtt_mqttqueued gauge\\nemsesp_mqtt_mqttqueued 0\\n# HELP emsesp_mqtt_mqttpublishfails MQTTPublishFails\\n# TYPE emsesp_mqtt_mqttpublishfails gauge\\nemsesp_mqtt_mqttpublishfails 0\\n# HELP emsesp_mqtt_mqttreconnects MQTTReconnects\\n# TYPE emsesp_mqtt_mqttreconnects gauge\\nemsesp_mqtt_mqttreconnects 0\\n# HELP emsesp_mqtt_enabled enabled\\n# TYPE emsesp_mqtt_enabled gauge\\nemsesp_mqtt_enabled 1\\n# HELP emsesp_mqtt_keepalive keepAlive\\n# TYPE emsesp_mqtt_keepalive gauge\\nemsesp_mqtt_keepalive 60\\n# HELP emsesp_mqtt_cleansession cleanSession\\n# TYPE emsesp_mqtt_cleansession gauge\\nemsesp_mqtt_cleansession 0\\n# HELP emsesp_mqtt_entityformat entityFormat\\n# TYPE emsesp_mqtt_entityformat gauge\\nemsesp_mqtt_entityformat 1\\n# HELP emsesp_mqtt_discoverytype discoveryType\\n# TYPE emsesp_mqtt_discoverytype gauge\\nemsesp_mqtt_discoverytype 0\\n# HELP emsesp_mqtt_nestedformat nestedFormat\\n# TYPE emsesp_mqtt_nestedformat gauge\\nemsesp_mqtt_nestedformat 1\\n# HELP emsesp_mqtt_haenabled haEnabled\\n# TYPE emsesp_mqtt_haenabled gauge\\nemsesp_mqtt_haenabled 1\\n# HELP emsesp_mqtt_mqttqos mqttQos\\n# TYPE emsesp_mqtt_mqttqos gauge\\nemsesp_mqtt_mqttqos 0\\n# HELP emsesp_mqtt_mqttretain mqttRetain\\n# TYPE emsesp_mqtt_mqttretain gauge\\nemsesp_mqtt_mqttretain 0\\n# HELP emsesp_mqtt_publishtimeheartbeat publishTimeHeartbeat\\n# TYPE emsesp_mqtt_publishtimeheartbeat gauge\\nemsesp_mqtt_publishtimeheartbeat 60\\n# HELP emsesp_mqtt_publishtimeboiler publishTimeBoiler\\n# TYPE emsesp_mqtt_publishtimeboiler gauge\\nemsesp_mqtt_publishtimeboiler 10\\n# HELP emsesp_mqtt_publishtimethermostat publishTimeThermostat\\n# TYPE emsesp_mqtt_publishtimethermostat gauge\\nemsesp_mqtt_publishtimethermostat 10\\n# HELP emsesp_mqtt_publishtimesolar publishTimeSolar\\n# TYPE emsesp_mqtt_publishtimesolar gauge\\nemsesp_mqtt_publishtimesolar 10\\n# HELP emsesp_mqtt_publishtimemixer publishTimeMixer\\n# TYPE emsesp_mqtt_publishtimemixer gauge\\nemsesp_mqtt_publishtimemixer 10\\n# HELP emsesp_mqtt_publishtimewater publishTimeWater\\n# TYPE emsesp_mqtt_publishtimewater gauge\\nemsesp_mqtt_publishtimewater 0\\n# HELP emsesp_mqtt_publishtimeother publishTimeOther\\n# TYPE emsesp_mqtt_publishtimeother gauge\\nemsesp_mqtt_publishtimeother 10\\n# HELP emsesp_mqtt_publishtimesensor publishTimeSensor\\n# TYPE emsesp_mqtt_publishtimesensor gauge\\nemsesp_mqtt_publishtimesensor 10\\n# HELP emsesp_mqtt_publishsingle publishSingle\\n# TYPE emsesp_mqtt_publishsingle gauge\\nemsesp_mqtt_publishsingle 0\\n# HELP emsesp_mqtt_publish2command publish2command\\n# TYPE emsesp_mqtt_publish2command gauge\\nemsesp_mqtt_publish2command 0\\n# HELP emsesp_mqtt_sendresponse sendResponse\\n# TYPE emsesp_mqtt_sendresponse gauge\\nemsesp_mqtt_sendresponse 0\\n# HELP emsesp_mqtt_info info\\n# TYPE emsesp_mqtt_info gauge\\nemsesp_mqtt_info{mqttstatus=\\\"disconnected\\\", clientid=\\\"ems-esp\\\", base=\\\"ems-esp\\\", discoveryprefix=\\\"homeassistant\\\"} 1\\n# HELP emsesp_syslog_enabled enabled\\n# TYPE emsesp_syslog_enabled gauge\\nemsesp_syslog_enabled 0\\n# HELP emsesp_modbus_enabled enabled\\n# TYPE emsesp_modbus_enabled gauge\\nemsesp_modbus_enabled 0\\n# HELP emsesp_sensor_temperaturesensors temperatureSensors\\n# TYPE emsesp_sensor_temperaturesensors gauge\\nemsesp_sensor_temperaturesensors 3\\n# HELP emsesp_sensor_temperaturesensorreads temperatureSensorReads\\n# TYPE emsesp_sensor_temperaturesensorreads gauge\\nemsesp_sensor_temperaturesensorreads 0\\n# HELP emsesp_sensor_temperaturesensorfails temperatureSensorFails\\n# TYPE emsesp_sensor_temperaturesensorfails gauge\\nemsesp_sensor_temperaturesensorfails 0\\n# HELP emsesp_analog_enabled enabled\\n# TYPE emsesp_analog_enabled gauge\\nemsesp_analog_enabled 1\\n# HELP emsesp_analog_analogsensors analogSensors\\n# TYPE emsesp_analog_analogsensors gauge\\nemsesp_analog_analogsensors 5\\n# HELP emsesp_analog_analogsensorreads analogSensorReads\\n# TYPE emsesp_analog_analogsensorreads gauge\\nemsesp_analog_analogsensorreads 0\\n# HELP emsesp_analog_analogsensorfails analogSensorFails\\n# TYPE emsesp_analog_analogsensorfails gauge\\nemsesp_analog_analogsensorfails 0\\n# HELP emsesp_api_apicalls APICalls\\n# TYPE emsesp_api_apicalls gauge\\nemsesp_api_apicalls 0\\n# HELP emsesp_api_apifails APIFails\\n# TYPE emsesp_api_apifails gauge\\nemsesp_api_apifails 0\\n# HELP emsesp_bus_bustelegramsreceived busTelegramsReceived\\n# TYPE emsesp_bus_bustelegramsreceived gauge\\nemsesp_bus_bustelegramsreceived 8\\n# HELP emsesp_bus_busreads busReads\\n# TYPE emsesp_bus_busreads gauge\\nemsesp_bus_busreads 0\\n# HELP emsesp_bus_buswrites busWrites\\n# TYPE emsesp_bus_buswrites gauge\\nemsesp_bus_buswrites 0\\n# HELP emsesp_bus_busincompletetelegrams busIncompleteTelegrams\\n# TYPE emsesp_bus_busincompletetelegrams gauge\\nemsesp_bus_busincompletetelegrams 0\\n# HELP emsesp_bus_busreadsfailed busReadsFailed\\n# TYPE emsesp_bus_busreadsfailed gauge\\nemsesp_bus_busreadsfailed 0\\n# HELP emsesp_bus_buswritesfailed busWritesFailed\\n# TYPE emsesp_bus_buswritesfailed gauge\\nemsesp_bus_buswritesfailed 0\\n# HELP emsesp_bus_busrxlinequality busRxLineQuality\\n# TYPE emsesp_bus_busrxlinequality gauge\\nemsesp_bus_busrxlinequality 100\\n# HELP emsesp_bus_bustxlinequality busTxLineQuality\\n# TYPE emsesp_bus_bustxlinequality gauge\\nemsesp_bus_bustxlinequality 100\\n# HELP emsesp_bus_info info\\n# TYPE emsesp_bus_info gauge\\nemsesp_bus_info{busstatus=\\\"connected\\\", busprotocol=\\\"Buderus\\\"} 1\\n# HELP emsesp_settings_txmode txMode\\n# TYPE emsesp_settings_txmode gauge\\nemsesp_settings_txmode 5\\n# HELP emsesp_settings_emsbusid emsBusID\\n# TYPE emsesp_settings_emsbusid gauge\\nemsesp_settings_emsbusid 73\\n# HELP emsesp_settings_showertimer showerTimer\\n# TYPE emsesp_settings_showertimer gauge\\nemsesp_settings_showertimer 0\\n# HELP emsesp_settings_showerminduration showerMinDuration\\n# TYPE emsesp_settings_showerminduration gauge\\nemsesp_settings_showerminduration 180\\n# HELP emsesp_settings_showeralert showerAlert\\n# TYPE emsesp_settings_showeralert gauge\\nemsesp_settings_showeralert 0\\n# HELP emsesp_settings_hideled hideLed\\n# TYPE emsesp_settings_hideled gauge\\nemsesp_settings_hideled 0\\n# HELP emsesp_settings_notokenapi noTokenApi\\n# TYPE emsesp_settings_notokenapi gauge\\nemsesp_settings_notokenapi 0\\n# HELP emsesp_settings_readonlymode readonlyMode\\n# TYPE emsesp_settings_readonlymode gauge\\nemsesp_settings_readonlymode 0\\n# HELP emsesp_settings_fahrenheit fahrenheit\\n# TYPE emsesp_settings_fahrenheit gauge\\nemsesp_settings_fahrenheit 0\\n# HELP emsesp_settings_dallasparasite dallasParasite\\n# TYPE emsesp_settings_dallasparasite gauge\\nemsesp_settings_dallasparasite 0\\n# HELP emsesp_settings_boolformat boolFormat\\n# TYPE emsesp_settings_boolformat gauge\\nemsesp_settings_boolformat 1\\n# HELP emsesp_settings_booldashboard boolDashboard\\n# TYPE emsesp_settings_booldashboard gauge\\nemsesp_settings_booldashboard 1\\n# HELP emsesp_settings_enumformat enumFormat\\n# TYPE emsesp_settings_enumformat gauge\\nemsesp_settings_enumformat 1\\n# HELP emsesp_settings_analogenabled analogEnabled\\n# TYPE emsesp_settings_analogenabled gauge\\nemsesp_settings_analogenabled 1\\n# HELP emsesp_settings_telnetenabled telnetEnabled\\n# TYPE emsesp_settings_telnetenabled gauge\\nemsesp_settings_telnetenabled 1\\n# HELP emsesp_settings_maxweblogbuffer maxWebLogBuffer\\n# TYPE emsesp_settings_maxweblogbuffer gauge\\nemsesp_settings_maxweblogbuffer 25\\n# HELP emsesp_settings_modbusenabled modbusEnabled\\n# TYPE emsesp_settings_modbusenabled gauge\\nemsesp_settings_modbusenabled 0\\n# HELP emsesp_settings_forceheatingoff forceHeatingOff\\n# TYPE emsesp_settings_forceheatingoff gauge\\nemsesp_settings_forceheatingoff 0\\n# HELP emsesp_settings_developermode developerMode\\n# TYPE emsesp_settings_developermode gauge\\nemsesp_settings_developermode 0\\n# HELP emsesp_settings_disablereset disableReset\\n# TYPE emsesp_settings_disablereset gauge\\nemsesp_settings_disablereset 0\\n# HELP emsesp_settings_info info\\n# TYPE emsesp_settings_info gauge\\nemsesp_settings_info{boardprofile=\\\"S32\\\", locale=\\\"en\\\"} 1\\n# HELP emsesp_device_productid productID\\n# TYPE emsesp_device_productid gauge\\nemsesp_device_productid{type=\\\"boiler\\\", name=\\\"My Custom Boiler\\\", deviceid=\\\"0x08\\\", version=\\\"01.00\\\"} 123\\n# HELP emsesp_device_entities entities\\n# TYPE emsesp_device_entities gauge\\nemsesp_device_entities{type=\\\"boiler\\\", name=\\\"My Custom Boiler\\\", deviceid=\\\"0x08\\\", version=\\\"01.00\\\"} 39\\nemsesp_device_productid{type=\\\"thermostat\\\", name=\\\"FW120\\\", deviceid=\\\"0x10\\\", version=\\\"01.00\\\"} 192\\nemsesp_device_entities{type=\\\"thermostat\\\", name=\\\"FW120\\\", deviceid=\\\"0x10\\\", version=\\\"01.00\\\"} 12\\nemsesp_device_entities{type=\\\"temperaturesensor\\\", name=\\\"temperaturesensor\\\"} 3\\nemsesp_device_entities{type=\\\"analogsensor\\\", name=\\\"analogsensor\\\"} 5\\nemsesp_device_entities{type=\\\"scheduler\\\", name=\\\"scheduler\\\"} 2\\nemsesp_device_entities{type=\\\"commands\\\", name=\\\"commands\\\"} 3\\nemsesp_device_entities{type=\\\"custom\\\", name=\\\"custom\\\"} 4\\n\"}]"; TEST_ASSERT_EQUAL_STRING(expected_response, call_url("/api/system/metrics")); }