diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index d0b5685c1..73b2939e3 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -4,7 +4,7 @@ ## Added -- Translations in Web UI and all device entity names (DE, NL, SE, PL, NO, ...) [#22](https://github.com/emsesp/EMS-ESP32/issues/22) +- Translations in Web UI and all device entity names (DE, NL, SE, PL, NO) [#22](https://github.com/emsesp/EMS-ESP32/issues/22) - Add support for Lolin C3 mini [#620](https://github.com/emsesp/EMS-ESP32/pull/620) - Add support for ESP32-S2 [#667](https://github.com/emsesp/EMS-ESP32/pull/667) - Add devices: Greenstar 30Ri boiler, Junkers FW500 thermostat, Buderus BC30 controller @@ -17,8 +17,9 @@ - Add min/max to customization table [#686](https://github.com/emsesp/EMS-ESP32/issues/686) - Add MD5 check [#637](https://github.com/emsesp/EMS-ESP32/issues/637) - Add more bus-ids [#673](https://github.com/emsesp/EMS-ESP32/issues/673) -- Use HA connectivity device class for Status [#751](https://github.com/emsesp/EMS-ESP32/issues/751) +- Use HA connectivity device class for Status, added boot time [#751](https://github.com/emsesp/EMS-ESP32/issues/751) - Add commands for analog sensors outputs +- Support for multiple EMS-ESPs with MQTT and HA [[#759](https://github.com/emsesp/EMS-ESP32/issues/759)] ## Fixed @@ -29,11 +30,13 @@ - Discovery in HomeAssistant don't work with custom base topic. [#596](https://github.com/emsesp/EMS-ESP32/issues/596) Base topic containing `/` are changed to `_` - RF room temperature sensor are shown as thermostat - render mqtt float json values with trailing zero -- removed flash strings +- removed flash strings, to increase available heap memory - reload page after restart button is pressed - analog/dallas values command as list like ems-devices - analog/dallas HA-entities based on id +- MQTT Base is a mandatory field. Removed MQTT topic length from settings. ## **BREAKING CHANGES:** -- When upgrading from 3.4.x you may need to erase the flash on the ESP32 before uploading the firmware. Make sure you make a backup of the settings and customizations via the WebUI (System->Upload/Download) +- When upgrading for the first time from 3.4.x on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases). Make sure you make a backup of the settings and customizations via the WebUI (System->Upload/Download). +- Because of #759 the Home Assistant entity IDs have been renamed diff --git a/interface/package-lock.json b/interface/package-lock.json index 9bd895607..a29315980 100644 --- a/interface/package-lock.json +++ b/interface/package-lock.json @@ -14,9 +14,9 @@ "@mui/icons-material": "^5.10.16", "@mui/material": "^5.10.16", "@table-library/react-table-library": "4.0.23", - "@types/lodash": "^4.14.190", - "@types/node": "^18.11.9", - "@types/react": "^18.0.25", + "@types/lodash": "^4.14.191", + "@types/node": "^18.11.10", + "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/react-router-dom": "^5.3.3", "async-validator": "^4.2.5", @@ -30,8 +30,8 @@ "react-app-rewired": "^2.2.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", - "react-icons": "^4.6.0", - "react-router-dom": "^6.4.3", + "react-icons": "^4.7.1", + "react-router-dom": "^6.4.4", "react-scripts": "5.0.1", "sockette": "^2.0.6", "typesafe-i18n": "^5.17.1", @@ -3458,9 +3458,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.3.tgz", - "integrity": "sha512-ceuyTSs7PZ/tQqi19YZNBc5X7kj1f8p+4DIyrcIYFY9h+hd1OKm4RqtiWldR9eGEvIiJfsqwM4BsuCtRIuEw6Q==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.4.tgz", + "integrity": "sha512-gTL8H5USTAKOyVA4xczzDJnC3HMssdFa3tRlwBicXynx9XfiXwneHnYQogwSKpdCkjXISrEKSTtX62rLpNEVQg==", "engines": { "node": ">=14" } @@ -4007,9 +4007,9 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "node_modules/@types/lodash": { - "version": "4.14.190", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.190.tgz", - "integrity": "sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw==" + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" }, "node_modules/@types/mime": { "version": "3.0.1", @@ -4017,9 +4017,9 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.10.tgz", + "integrity": "sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4052,9 +4052,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.0.25", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", - "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", + "version": "18.0.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", + "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -14540,9 +14540,9 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, "node_modules/react-icons": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz", - "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz", + "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==", "peerDependencies": { "react": "*" } @@ -14561,11 +14561,11 @@ } }, "node_modules/react-router": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.3.tgz", - "integrity": "sha512-BT6DoGn6aV1FVP5yfODMOiieakp3z46P1Fk0RNzJMACzE7C339sFuHebfvWtnB4pzBvXXkHP2vscJzWRuUjTtA==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.4.tgz", + "integrity": "sha512-SA6tSrUCRfuLWeYsTJDuriRqfFIsrSvuH7SqAJHegx9ZgxadE119rU8oOX/rG5FYEthpdEaEljdjDlnBxvfr+Q==", "dependencies": { - "@remix-run/router": "1.0.3" + "@remix-run/router": "1.0.4" }, "engines": { "node": ">=14" @@ -14575,12 +14575,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.3.tgz", - "integrity": "sha512-MiaYQU8CwVCaOfJdYvt84KQNjT78VF0TJrA17SIQgNHRvLnXDJO6qsFqq8F/zzB1BWZjCFIrQpu4QxcshitziQ==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.4.tgz", + "integrity": "sha512-0Axverhw5d+4SBhLqLpzPhNkmv7gahUwlUVIOrRLGJ4/uwt30JVajVJXqv2Qr/LCwyvHhQc7YyK1Do8a9Jj7qA==", "dependencies": { - "@remix-run/router": "1.0.3", - "react-router": "6.4.3" + "@remix-run/router": "1.0.4", + "react-router": "6.4.4" }, "engines": { "node": ">=14" @@ -19816,9 +19816,9 @@ "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" }, "@remix-run/router": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.3.tgz", - "integrity": "sha512-ceuyTSs7PZ/tQqi19YZNBc5X7kj1f8p+4DIyrcIYFY9h+hd1OKm4RqtiWldR9eGEvIiJfsqwM4BsuCtRIuEw6Q==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.4.tgz", + "integrity": "sha512-gTL8H5USTAKOyVA4xczzDJnC3HMssdFa3tRlwBicXynx9XfiXwneHnYQogwSKpdCkjXISrEKSTtX62rLpNEVQg==" }, "@rollup/plugin-babel": { "version": "5.3.1", @@ -20220,9 +20220,9 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "@types/lodash": { - "version": "4.14.190", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.190.tgz", - "integrity": "sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw==" + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" }, "@types/mime": { "version": "3.0.1", @@ -20230,9 +20230,9 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.10.tgz", + "integrity": "sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==" }, "@types/parse-json": { "version": "4.0.0", @@ -20265,9 +20265,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/react": { - "version": "18.0.25", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", - "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", + "version": "18.0.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", + "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -27705,9 +27705,9 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, "react-icons": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz", - "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz", + "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==", "requires": {} }, "react-is": { @@ -27721,20 +27721,20 @@ "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, "react-router": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.3.tgz", - "integrity": "sha512-BT6DoGn6aV1FVP5yfODMOiieakp3z46P1Fk0RNzJMACzE7C339sFuHebfvWtnB4pzBvXXkHP2vscJzWRuUjTtA==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.4.tgz", + "integrity": "sha512-SA6tSrUCRfuLWeYsTJDuriRqfFIsrSvuH7SqAJHegx9ZgxadE119rU8oOX/rG5FYEthpdEaEljdjDlnBxvfr+Q==", "requires": { - "@remix-run/router": "1.0.3" + "@remix-run/router": "1.0.4" } }, "react-router-dom": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.3.tgz", - "integrity": "sha512-MiaYQU8CwVCaOfJdYvt84KQNjT78VF0TJrA17SIQgNHRvLnXDJO6qsFqq8F/zzB1BWZjCFIrQpu4QxcshitziQ==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.4.tgz", + "integrity": "sha512-0Axverhw5d+4SBhLqLpzPhNkmv7gahUwlUVIOrRLGJ4/uwt30JVajVJXqv2Qr/LCwyvHhQc7YyK1Do8a9Jj7qA==", "requires": { - "@remix-run/router": "1.0.3", - "react-router": "6.4.3" + "@remix-run/router": "1.0.4", + "react-router": "6.4.4" } }, "react-scripts": { diff --git a/interface/package.json b/interface/package.json index 42e2ad379..c727c1dc4 100644 --- a/interface/package.json +++ b/interface/package.json @@ -10,9 +10,9 @@ "@mui/icons-material": "^5.10.16", "@mui/material": "^5.10.16", "@table-library/react-table-library": "4.0.23", - "@types/lodash": "^4.14.190", - "@types/node": "^18.11.9", - "@types/react": "^18.0.25", + "@types/lodash": "^4.14.191", + "@types/node": "^18.11.10", + "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/react-router-dom": "^5.3.3", "async-validator": "^4.2.5", @@ -26,8 +26,8 @@ "react-app-rewired": "^2.2.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", - "react-icons": "^4.6.0", - "react-router-dom": "^6.4.3", + "react-icons": "^4.7.1", + "react-router-dom": "^6.4.4", "react-scripts": "5.0.1", "sockette": "^2.0.6", "typesafe-i18n": "^5.17.1", diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index 09d3e3f7c..5837fac56 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -37,7 +37,7 @@ const de: Translation = { PRODUCT: 'Produkt', VERSION: 'Version', ENTITY_NAME: 'Entitätsname', - VALUE: 'Wert', + VALUE: '{{Wert|wert}}', SHOW_FAV: 'nur Favoriten anzeigen', DEVICE_SENSOR_DATA: 'Geräte- und Sensordaten', DEVICES_SENSORS: 'Geräte & Sensoren', diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 2e949f2e8..4cd5e69ec 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -37,7 +37,7 @@ const en: Translation = { VERSION: 'Version', BRAND: 'Brand', ENTITY_NAME: 'Entity Name', - VALUE: 'Value', + VALUE: '{{Value|value}}', SHOW_FAV: 'only show favorites', DEVICE_SENSOR_DATA: 'Device and Sensor Data', DEVICES_SENSORS: 'Devices & Sensors', diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 496ea4a35..75897d27d 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -100,8 +100,8 @@ const nl: Translation = { NUM_ANALOG_SENSORS: '{num} Analoge sensor{{en}}', NUM_DAYS: '{num} dag{{en}}', NUM_SECONDS: '{num} second{{en}}', - NUM_HOURS: '{num} uur{{en}}', - NUM_MINUTES: '{num} minuut{{en}}', + NUM_HOURS: '{num} {{uur|uren}}', + NUM_MINUTES: '{num} minute{{n}}', APPLICATION_SETTINGS: 'Applicatieinstellingen', CUSTOMIZATION: 'Custom aanpassingen', APPLICATION_RESTARTING: 'EMS-ESP herstarten', diff --git a/interface/src/project/validators.ts b/interface/src/project/validators.ts index 6000e0769..73991f5ac 100644 --- a/interface/src/project/validators.ts +++ b/interface/src/project/validators.ts @@ -72,11 +72,11 @@ export const createSettingsValidator = (settings: Settings) => syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR], syslog_port: [ { required: true, message: 'Port is required' }, - { type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' } + { type: 'number', min: 0, max: 65535, message: 'Invalid Port' } ], syslog_mark_interval: [ { required: true, message: 'Mark interval is required' }, - { type: 'number', min: 0, max: 10, message: 'Port must be between 0 and 10' } + { type: 'number', min: 0, max: 10, message: ' must be between 0 and 10' } ] }), ...(settings.shower_alert && { diff --git a/interface/src/types/mqtt.ts b/interface/src/types/mqtt.ts index 0e8a10eec..7ecf694ca 100644 --- a/interface/src/types/mqtt.ts +++ b/interface/src/types/mqtt.ts @@ -29,7 +29,6 @@ export interface MqttSettings { client_id: string; keep_alive: number; clean_session: boolean; - max_topic_length: number; publish_time_boiler: number; publish_time_thermostat: number; publish_time_solar: number; diff --git a/interface/src/validators/mqtt.ts b/interface/src/validators/mqtt.ts index b26619e82..362c66ca9 100644 --- a/interface/src/validators/mqtt.ts +++ b/interface/src/validators/mqtt.ts @@ -3,16 +3,13 @@ import { IP_OR_HOSTNAME_VALIDATOR } from './shared'; export const MQTT_SETTINGS_VALIDATOR = new Schema({ host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR], + base: { required: true, message: 'Base is required' }, port: [ { required: true, message: 'Port is required' }, - { type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' } + { type: 'number', min: 0, max: 65535, message: 'Invalid Port' } ], keep_alive: [ { required: true, message: 'Keep alive is required' }, { type: 'number', min: 1, max: 86400, message: 'Keep alive must be between 1 and 86400' } - ], - max_topic_length: [ - { required: true, message: 'Max topic length is required' }, - { type: 'number', min: 16, max: 1024, message: 'Max topic length must be between 16 and 1024' } ] }); diff --git a/lib/framework/MqttSettingsService.cpp b/lib/framework/MqttSettingsService.cpp index 6a2c13413..be23b107d 100644 --- a/lib/framework/MqttSettingsService.cpp +++ b/lib/framework/MqttSettingsService.cpp @@ -145,16 +145,15 @@ void MqttSettingsService::configureMqtt() { } void MqttSettings::read(MqttSettings & settings, JsonObject & root) { - root["enabled"] = settings.enabled; - root["host"] = settings.host; - root["port"] = settings.port; - root["base"] = settings.base; - root["username"] = settings.username; - root["password"] = settings.password; - root["client_id"] = settings.clientId; - root["keep_alive"] = settings.keepAlive; - root["clean_session"] = settings.cleanSession; - root["max_topic_length"] = settings.maxTopicLength; + root["enabled"] = settings.enabled; + root["host"] = settings.host; + root["port"] = settings.port; + root["base"] = settings.base; + root["username"] = settings.username; + root["password"] = settings.password; + root["client_id"] = settings.clientId; + root["keep_alive"] = settings.keepAlive; + root["clean_session"] = settings.cleanSession; // added by proddy for EMS-ESP root["publish_time_boiler"] = settings.publish_time_boiler; @@ -177,18 +176,19 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting MqttSettings newSettings = {}; bool changed = false; - newSettings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED; - newSettings.host = root["host"] | FACTORY_MQTT_HOST; - newSettings.port = root["port"] | FACTORY_MQTT_PORT; - newSettings.base = root["base"] | FACTORY_MQTT_BASE; - newSettings.username = root["username"] | FACTORY_MQTT_USERNAME; - newSettings.password = root["password"] | FACTORY_MQTT_PASSWORD; - newSettings.clientId = root["client_id"] | FACTORY_MQTT_CLIENT_ID; - newSettings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE; - newSettings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION; - newSettings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH; - newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS; - newSettings.mqtt_retain = root["mqtt_retain"] | EMSESP_DEFAULT_MQTT_RETAIN; + newSettings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED; + newSettings.host = root["host"] | FACTORY_MQTT_HOST; + newSettings.port = root["port"] | FACTORY_MQTT_PORT; + newSettings.base = root["base"] | FACTORY_MQTT_BASE; + newSettings.username = root["username"] | FACTORY_MQTT_USERNAME; + newSettings.password = root["password"] | FACTORY_MQTT_PASSWORD; + newSettings.clientId = root["client_id"] | FACTORY_MQTT_CLIENT_ID; + newSettings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE; + newSettings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION; + newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS; + newSettings.mqtt_retain = root["mqtt_retain"] | EMSESP_DEFAULT_MQTT_RETAIN; + + newSettings.maxTopicLength = FACTORY_MQTT_MAX_TOPIC_LENGTH; // hardcode. We don't take this from the settings anymore. newSettings.publish_time_boiler = root["publish_time_boiler"] | EMSESP_DEFAULT_PUBLISH_TIME; newSettings.publish_time_thermostat = root["publish_time_thermostat"] | EMSESP_DEFAULT_PUBLISH_TIME; diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index 3df0d3118..1697ad3d1 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -83,5 +83,5 @@ void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVari void NTPSettingsService::ntp_received(struct timeval * tv) { // emsesp::EMSESP::logger().info("NTP sync to %d sec", tv->tv_sec); emsesp::EMSESP::system_.ntp_connected(true); - emsesp::EMSESP::system_.send_info_mqtt("connected"); + emsesp::EMSESP::system_.send_info_mqtt("connected", true); // send info topic with NTP time } diff --git a/lib/framework/UploadFileService.cpp b/lib/framework/UploadFileService.cpp index e2d07d83b..b876a195b 100644 --- a/lib/framework/UploadFileService.cpp +++ b/lib/framework/UploadFileService.cpp @@ -29,9 +29,12 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri std::string extension = fname.substr(position + 1); size_t fsize = request->contentLength(); +#ifdef EMSESP_DEBUG #if defined(EMSESP_USE_SERIAL) + Serial.println(); Serial.printf("Received filename: %s, len: %d, index: %d, ext: %s, fsize: %d", filename.c_str(), len, index, extension.c_str(), fsize); Serial.println(); +#endif #endif is_firmware = false; diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index 4c17cc4aa..96590bef2 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -74,7 +74,7 @@ class DummySettings { String base = "ems-esp"; bool publish_single = false; bool publish_single2cmd = false; - bool send_response = true; + bool send_response = false; // don't send response String host = "192.168.1.4"; uint16_t port = 1883; String clientId = "ems-esp"; diff --git a/mock-api/server.js b/mock-api/server.js index 248257958..f37697551 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -219,7 +219,6 @@ mqtt_settings = { client_id: 'ems-esp', keep_alive: 60, clean_session: true, - max_topic_length: 128, publish_time_boiler: 10, publish_time_thermostat: 10, publish_time_solar: 10, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..2442bf511 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "EMS-ESP32", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/analogsensor.cpp b/src/analogsensor.cpp index cd4b09d7c..69cd7e8a6 100644 --- a/src/analogsensor.cpp +++ b/src/analogsensor.cpp @@ -451,16 +451,13 @@ void AnalogSensor::publish_values(const bool force) { } config["val_tpl"] = str; - // snprintf(str, sizeof(str), "%s_analog_sensor_%s", Mqtt::basename().c_str(), sensor.name().c_str()); - snprintf(str, sizeof(str), "analog_sensor_%d", sensor.gpio()); + snprintf(str, sizeof(str), "%s_analog_sensor_%s", Mqtt::basename().c_str(), sensor.name().c_str()); config["object_id"] = str; + config["uniq_id"] = str; // same as object_id snprintf(str, sizeof(str), "%s", sensor.name().c_str()); config["name"] = str; - snprintf(str, sizeof(str), "analogsensor_%d", sensor.gpio()); - config["uniq_id"] = str; - if (sensor.uom() != DeviceValueUOM::NONE) { config["unit_of_meas"] = EMSdevice::uom_to_string(sensor.uom()); } diff --git a/src/command.cpp b/src/command.cpp index 8c6cd7019..a8fea507a 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -52,8 +52,10 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec } } +#ifdef EMSESP_DEBUG #if defined(EMSESP_USE_SERIAL) // Serial.println(p.path().c_str()); // dump paths, for debugging +#endif #endif // re-calculate new path @@ -179,6 +181,8 @@ std::string Command::return_code_string(const uint8_t return_code) { return "Not Authorized"; case CommandRet::FAIL: return "Failed"; + case CommandRet::INVALID: + return "Invalid"; default: break; } @@ -270,18 +274,22 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * } auto description = Helpers::translated_word(cf->description_); + char info_s[100]; + if (strlen(description)) { + snprintf(info_s, sizeof(info_s), "'%s/%s' (%s)", dname, cmd, description); + } else { + snprintf(info_s, sizeof(info_s), "'%s/%s'", dname, cmd); + } + + std::string ro = EMSESP::system_.readonly_mode() ? "[readonly] " : ""; if ((value == nullptr) || (strlen(value) == 0)) { - if (EMSESP::system_.readonly_mode()) { - LOG_INFO(("[readonly] Calling command '%s/%s' (%s)"), dname, cmd, description); - } else { - LOG_DEBUG(("Calling command '%s/%s' (%s)"), dname, cmd, description); - } + LOG_INFO(("%sCalling command %s"), ro.c_str(), info_s); } else { - if (EMSESP::system_.readonly_mode()) { - LOG_INFO(("[readonly] Calling command '%s/%s' (%s) with value %s"), dname, cmd, description, value); + if (id > 0) { + LOG_INFO(("%sCalling command %s with value %s and id %d"), ro.c_str(), info_s, value, id); } else { - LOG_DEBUG(("Calling command '%s/%s' (%s) with value %s"), dname, cmd, description, value); + LOG_INFO(("%sCalling command %s with value %s"), ro.c_str(), info_s, value); } } @@ -290,8 +298,13 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * return_code = ((cf->cmdfunction_json_)(value, id, output)) ? CommandRet::OK : CommandRet::ERROR; } - if (cf->cmdfunction_ && !EMSESP::cmd_is_readonly(device_type, cmd, id)) { - return_code = ((cf->cmdfunction_)(value, id)) ? CommandRet::OK : CommandRet::ERROR; + // check if read-only. This also checks for valid tags (e.g. heating circuits) + if (cf->cmdfunction_) { + if (EMSESP::cmd_is_readonly(device_type, cmd, id)) { + return_code = CommandRet::INVALID; // readonly or invalid hc + } else { + return_code = ((cf->cmdfunction_)(value, id)) ? CommandRet::OK : CommandRet::ERROR; + } } // report back diff --git a/src/command.h b/src/command.h index d131f0574..187a213d7 100644 --- a/src/command.h +++ b/src/command.h @@ -40,12 +40,12 @@ enum CommandFlag : uint8_t { // return status after calling a Command enum CommandRet : uint8_t { - FAIL = 0, // 0 or FALSE - OK, // 1 or TRUE - NOT_FOUND, // 2 - ERROR, // 3 - NOT_ALLOWED // needs authentication - + FAIL = 0, // 0 or FALSE + OK, // 1 or TRUE + NOT_FOUND, // 2 + ERROR, // 3 + NOT_ALLOWED, // 4 - needs authentication + INVALID // 5 - invalid (tag) }; using cmd_function_p = std::function; diff --git a/src/dallassensor.cpp b/src/dallassensor.cpp index 116a43090..ccfc7438c 100644 --- a/src/dallassensor.cpp +++ b/src/dallassensor.cpp @@ -518,18 +518,15 @@ void DallasSensor::publish_values(const bool force) { } config["val_tpl"] = str; - // snprintf(str, sizeof(str), "%s_temperature_sensor_%s", Mqtt::basename().c_str(), sensor.id().c_str()); - snprintf(str, sizeof(str), "temperature_sensor_%s", sensor.id().c_str()); + snprintf(str, sizeof(str), "%s_temperature_sensor_%s", Mqtt::basename().c_str(), sensor.id().c_str()); config["object_id"] = str; + config["uniq_id"] = str; // same as object_id snprintf(str, sizeof(str), "%s", sensor.name().c_str()); config["name"] = str; - snprintf(str, sizeof(str), "dallasensor_%s", sensor.id().c_str()); - config["uniq_id"] = str; - - JsonObject dev = config.createNestedObject("dev"); - JsonArray ids = dev.createNestedArray("ids"); + JsonObject dev = config.createNestedObject("dev"); + JsonArray ids = dev.createNestedArray("ids"); ids.add("ems-esp"); char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 90dac5a49..17c99a0ca 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -623,6 +623,7 @@ bool EMSdevice::is_readable(const void * value_p) const { } // check if value/command is readonly +// matches valid tags too bool EMSdevice::is_readonly(const std::string & cmd, const int8_t id) const { uint8_t tag = id > 0 ? DeviceValueTAG::TAG_HC1 + id - 1 : DeviceValueTAG::TAG_NONE; for (const auto & dv : devicevalues_) { diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 5690fc264..16cc4e0e9 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -589,7 +589,7 @@ void EMSESP::publish_response(std::shared_ptr telegram) { doc["value"] = value; } - Mqtt::publish(F_(response), doc.as()); + Mqtt::publish("response", doc.as()); } // builds json with the detail of each value, for a specific EMS device type or the dallas sensor diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 2e8b4de4a..43f0c8d3c 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -280,6 +280,7 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) cons } return; } + // for misconfigured mqtt servers and publish2command ignore echos if (publish_single_ && publish_single2cmd_ && lasttopic_ == topic && lastpayload_ == message) { LOG_DEBUG("Received echo message %s: %s", topic, message); @@ -291,11 +292,12 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) cons // add the base back char full_topic[MQTT_TOPIC_MAX_SIZE]; snprintf(full_topic, sizeof(full_topic), "%s/%s", mqtt_base_.c_str(), mf.topic_.c_str()); + if ((!strcmp(topic, full_topic)) && (mf.mqtt_subfunction_)) { if (!(mf.mqtt_subfunction_)(message)) { LOG_ERROR("error: invalid payload %s for this topic %s", message, topic); if (send_response_) { - Mqtt::publish(F_(response), "error: invalid data"); + Mqtt::publish("response", "error: invalid data"); } } return; @@ -330,12 +332,12 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) cons } LOG_ERROR(error); if (send_response_) { - Mqtt::publish(F_(response), error); + Mqtt::publish("response", error); } } else { // all good, send back json output from call if (send_response_) { - Mqtt::publish(F_(response), output); + Mqtt::publish("response", output); } } } @@ -591,12 +593,12 @@ void Mqtt::on_connect() { void Mqtt::ha_status() { StaticJsonDocument doc; - doc["uniq_id"] = "ems-esp-system"; - doc["~"] = mqtt_base_; // default ems-esp + doc["uniq_id"] = "ems-esp-system"; + doc["object_id"] = "ems_esp_status"; + doc["~"] = mqtt_base_; // default ems-esp // doc["avty_t"] = "~/status"; // commented out, as it causes errors in HA sometimes // doc["json_attr_t"] = "~/heartbeat"; // store also as HA attributes doc["stat_t"] = "~/status"; - doc["object_id"] = "ems_esp_status"; doc["name"] = "EMS-ESP status"; doc["payload_on"] = "online"; doc["payload_off"] = "offline"; @@ -904,7 +906,6 @@ void Mqtt::publish_ha_sensor_config(DeviceValue & dv, const std::string & model, publish_ha_sensor_config(dv.type, dv.tag, dv.get_fullname().c_str(), - (dv.fullname ? dv.fullname[0] : nullptr), // EN name dv.device_type, dv.short_name, dv.uom, @@ -925,7 +926,7 @@ void Mqtt::publish_system_ha_sensor_config(uint8_t type, const char * name, cons JsonArray ids = dev_json.createNestedArray("ids"); ids.add("ems-esp"); - publish_ha_sensor_config(type, DeviceValueTAG::TAG_HEARTBEAT, name, name, EMSdevice::DeviceType::SYSTEM, entity, uom, false, false, nullptr, 0, 0, 0, dev_json); + publish_ha_sensor_config(type, DeviceValueTAG::TAG_HEARTBEAT, name, EMSdevice::DeviceType::SYSTEM, entity, uom, false, false, nullptr, 0, 0, 0, dev_json); } // MQTT discovery configs @@ -934,7 +935,6 @@ void Mqtt::publish_system_ha_sensor_config(uint8_t type, const char * name, cons void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdevice::DeviceValueType uint8_t tag, // EMSdevice::DeviceValueTAG const char * const fullname, // fullname, already translated - const char * const en_name, // original name const uint8_t device_type, // EMSdevice::DeviceType const char * const entity, // same as shortname const uint8_t uom, // EMSdevice::DeviceValueUOM (0=NONE) @@ -946,25 +946,29 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev const int16_t dv_set_max, const JsonObject & dev_json) { // ignore if name (fullname) is empty - if (fullname == nullptr || en_name == nullptr) { + if (!fullname) { return; } // create the device name auto device_name = EMSdevice::device_type_2_device_name(device_type); - // create entity by add the hc/wwc tag if present, separating with a . - char new_entity[50]; + // create entity by add the hc/wwc tag if present, separating with a _ + char entity_with_tag[50]; if (tag >= DeviceValueTAG::TAG_HC1) { - snprintf(new_entity, sizeof(new_entity), "%s.%s", EMSdevice::tag_to_mqtt(tag).c_str(), entity); + snprintf(entity_with_tag, sizeof(entity_with_tag), "%s_%s", EMSdevice::tag_to_mqtt(tag).c_str(), entity); } else { - snprintf(new_entity, sizeof(new_entity), "%s", entity); + snprintf(entity_with_tag, sizeof(entity_with_tag), "%s", entity); } - // build unique identifier which will be used in the topic, replacing all . with _ as not to break HA - char uniq[101]; - snprintf(uniq, sizeof(uniq), "%s_%s", device_name, new_entity); - Helpers::replace_char(uniq, '.', '_'); + // build unique identifier which will be used in the topic, also used as object_id + char uniq_id[70]; + snprintf(uniq_id, sizeof(uniq_id), "%s_%s_%s", mqtt_basename_.c_str(), device_name, entity_with_tag); + + // build a config topic that will be prefix onto a HA type (e.g. number, switch) + // e.g. homeassistant/number/ems-esp/thermostat_hc1_manualtemp + char config_topic[70]; + snprintf(config_topic, sizeof(config_topic), "%s/%s_%s/config", mqtt_basename_.c_str(), device_name, entity_with_tag); bool set_ha_classes = false; // set to true if we want to set the state class and device class @@ -981,46 +985,43 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev case DeviceValueType::ULONG: // number - https://www.home-assistant.io/integrations/number.mqtt // https://developers.home-assistant.io/docs/core/entity/number - snprintf(topic, sizeof(topic), "number/%s/%s/config", mqtt_basename_.c_str(), uniq); + snprintf(topic, sizeof(topic), "number/%s", config_topic); break; case DeviceValueType::BOOL: // switch - https://www.home-assistant.io/integrations/switch.mqtt - snprintf(topic, sizeof(topic), "switch/%s/%s/config", mqtt_basename_.c_str(), uniq); + snprintf(topic, sizeof(topic), "switch/%s", config_topic); break; case DeviceValueType::ENUM: // select - https://www.home-assistant.io/integrations/select.mqtt - snprintf(topic, sizeof(topic), "select/%s/%s/config", mqtt_basename_.c_str(), uniq); + snprintf(topic, sizeof(topic), "select/%s", config_topic); break; default: // plain old sensor - snprintf(topic, sizeof(topic), "sensor/%s/%s/config", mqtt_basename_.c_str(), uniq); + snprintf(topic, sizeof(topic), "sensor/%s", config_topic); break; } } else { - // these are Read only sensors. We can set the device class and state class - set_ha_classes = true; + set_ha_classes = true; // these are Read only sensors. We can set the device class and state class // plain old read only device entity if (type == DeviceValueType::BOOL) { - snprintf(topic, sizeof(topic), "binary_sensor/%s/%s/config", mqtt_basename_.c_str(), uniq); // binary sensor (for booleans) + snprintf(topic, sizeof(topic), "binary_sensor/%s", config_topic); // binary sensor (for booleans) } else { - snprintf(topic, sizeof(topic), "sensor/%s/%s/config", mqtt_basename_.c_str(), uniq); // normal HA sensor + snprintf(topic, sizeof(topic), "sensor/%s", config_topic); // normal HA sensor } } // if we're asking to remove this topic, send an empty payload and exit // https://github.com/emsesp/EMS-ESP32/issues/196 if (remove) { - LOG_DEBUG("Removing HA config for %s", uniq); + LOG_DEBUG("Removing HA config for %s", uniq_id); publish_ha(topic); return; } - bool have_tag = !EMSdevice::tag_to_string(tag).empty(); - // build the payload DynamicJsonDocument doc(EMSESP_JSON_SIZE_HA_CONFIG); - doc["~"] = mqtt_base_; - doc["uniq_id"] = uniq; + doc["uniq_id"] = uniq_id; + doc["object_id"] = uniq_id; // same as unique_id const char * ic_ha = "ic"; // icon - only set this if there is no device class const char * sc_ha = "state_class"; // state class @@ -1031,9 +1032,12 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // note: there is no way to handle strings in HA so datetimes (e.g. set_datetime, set_holiday, set_wwswitchtime etc) are excluded if (has_cmd) { // command topic back to EMS-ESP - char command_topic[105]; - snprintf(command_topic, sizeof(command_topic), "~/%s", uniq); - Helpers::replace_char(command_topic, '_', '/'); + char command_topic[MQTT_TOPIC_MAX_SIZE]; + if (tag >= DeviceValueTAG::TAG_HC1) { + snprintf(command_topic, sizeof(command_topic), "%s/%s/%s/%s", mqtt_basename_.c_str(), device_name, EMSdevice::tag_to_mqtt(tag).c_str(), entity); + } else { + snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", mqtt_basename_.c_str(), device_name, entity); + } doc["command_topic"] = command_topic; // for enums, add options @@ -1075,38 +1079,30 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // state topic char stat_t[MQTT_TOPIC_MAX_SIZE]; - snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str()); + snprintf(stat_t, sizeof(stat_t), "%s/%s", mqtt_basename_.c_str(), tag_to_topic(device_type, tag).c_str()); doc["stat_t"] = stat_t; // friendly name = char ha_name[70]; char * F_name = strdup(fullname); F_name[0] = toupper(F_name[0]); // capitalize first letter - if (have_tag) { - snprintf(ha_name, sizeof(ha_name), "%s %s", EMSdevice::tag_to_string(tag).c_str(), F_name); + if (EMSdevice::tag_to_string(tag).empty()) { + snprintf(ha_name, sizeof(ha_name), "%s", F_name); // no tag } else { - snprintf(ha_name, sizeof(ha_name), "%s", F_name); + snprintf(ha_name, sizeof(ha_name), "%s %s", EMSdevice::tag_to_string(tag).c_str(), F_name); } - free(F_name); + free(F_name); // very important! doc["name"] = ha_name; - // entity id is generated from the name, see https://www.home-assistant.io/docs/mqtt/discovery/#use-object_id-to-influence-the-entity-id - // so we override it to make it unique using entity_id - // See https://github.com/emsesp/EMS-ESP32/issues/596 - // keep it compatible to v3.4, use english fullname, no prefix (basename prefix commented out) - char object_id[130]; - if (have_tag) { - snprintf(object_id, sizeof(object_id), "%s_%s_%s", device_name, EMSdevice::tag_to_string(tag, false).c_str(), en_name); - } else { - snprintf(object_id, sizeof(object_id), "%s_%s", device_name, en_name); - } - doc["object_id"] = object_id; - // value template - // if its nested mqtt format then use the appended entity name, otherwise take the original + // if its nested mqtt format then use the appended entity name, otherwise take the original name char val_tpl[75]; if (is_nested()) { - snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", new_entity); + if (tag >= DeviceValueTAG::TAG_HC1) { + snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s.%s}}", EMSdevice::tag_to_mqtt(tag).c_str(), entity); + } else { + snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", entity); + } } else { snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", entity); } @@ -1230,8 +1226,7 @@ void Mqtt::publish_ha_climate_config(const uint8_t tag, const bool has_roomtemp, char currtemp_s[30]; char mode_str_tpl[400]; char name_s[10]; - char id_s[20]; - char uniq_id_s[30]; + char uniq_id_s[60]; char temp_cmd_s[30]; char mode_cmd_s[30]; char min_s[10]; @@ -1269,18 +1264,17 @@ void Mqtt::publish_ha_climate_config(const uint8_t tag, const bool has_roomtemp, hc_mode_s, hc_mode_s); - snprintf(id_s, sizeof(id_s), "thermostat_hc%d", hc_num); snprintf(name_s, sizeof(name_s), "Hc%d", hc_num); - snprintf(uniq_id_s, sizeof(uniq_id_s), "thermostat_hc%d", hc_num); + snprintf(uniq_id_s, sizeof(uniq_id_s), "%s_thermostat_hc%d", mqtt_basename_.c_str(), hc_num); // add basename snprintf(temp_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/seltemp", hc_num); snprintf(mode_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/mode", hc_num); StaticJsonDocument doc; doc["~"] = mqtt_base_; - doc["object_id"] = id_s; - doc["name"] = name_s; doc["uniq_id"] = uniq_id_s; + doc["object_id"] = uniq_id_s; // same as uniq_id + doc["name"] = name_s; doc["mode_stat_t"] = topic_t; doc["mode_stat_tpl"] = mode_str_tpl; doc["temp_cmd_t"] = temp_cmd_s; diff --git a/src/mqtt.h b/src/mqtt.h index 406752fa8..65e4fcad7 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -94,7 +94,6 @@ class Mqtt { static void publish_ha_sensor_config(uint8_t type, uint8_t tag, const char * const fullname, - const char * const en_name, const uint8_t device_type, const char * const entity, const uint8_t uom, diff --git a/src/shower.cpp b/src/shower.cpp index d2cb1c678..01fa187cb 100644 --- a/src/shower.cpp +++ b/src/shower.cpp @@ -151,10 +151,13 @@ void Shower::set_shower_state(bool state, bool force) { ha_configdone_ = true; StaticJsonDocument doc; - doc["name"] = "Shower Active"; - doc["uniq_id"] = "shower_active"; - doc["~"] = Mqtt::base(); - doc["stat_t"] = "~/shower_active"; + doc["name"] = "Shower Active"; + char str[70]; + snprintf(str, sizeof(str), "%s_shower_active", Mqtt::basename().c_str()); + doc["uniq_id"] = str; + doc["object_id"] = str; + doc["~"] = Mqtt::base(); + doc["stat_t"] = "~/shower_active"; // always render boolean as strings for HA char result[12]; diff --git a/src/system.cpp b/src/system.cpp index cdeba2322..1fbfe9aa9 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -536,23 +536,23 @@ void System::loop() { } // send MQTT info topic appended with the version information as JSON, as a retained flag -void System::send_info_mqtt(const char * event_str) { +void System::send_info_mqtt(const char * event_str, bool send_ntp) { StaticJsonDocument doc; doc["event"] = event_str; doc["version"] = EMSESP_APP_VERSION; // if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38) // https://github.com/emsesp/EMS-ESP32/issues/751 - if (ntp_connected()) { + if (send_ntp) { char time_string[25]; - time_t now = time(nullptr); // grab the current instant in unix seconds - strftime(time_string, 25, "%F %T", localtime(&now)); - doc["boot_time"] = time_string; + time_t now = time(nullptr) - uuid::get_uptime_sec(); + strftime(time_string, 25, "%FT%T%z", localtime(&now)); + doc["boot time"] = time_string; } #ifndef EMSESP_STANDALONE if (EMSESP::system_.ethernet_connected()) { - doc["connection"] = "ethernet"; + doc["network"] = "ethernet"; doc["hostname"] = ETH.getHostname(); doc["MAC"] = ETH.macAddress(); doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask()); @@ -562,7 +562,7 @@ void System::send_info_mqtt(const char * event_str) { doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6()); } } else if (WiFi.status() == WL_CONNECTED) { - doc["connection"] = "wifi"; + doc["network"] = "wifi"; doc["hostname"] = WiFi.getHostname(); doc["SSID"] = WiFi.SSID(); doc["BSSID"] = WiFi.BSSIDstr(); @@ -896,19 +896,19 @@ void System::show_system(uuid::console::Shell & shell) { shell.println("Network:"); switch (WiFi.status()) { case WL_IDLE_STATUS: - shell.printfln(" Network: Idle"); + shell.printfln(" Status: Idle"); break; case WL_NO_SSID_AVAIL: - shell.printfln(" Network: Network not found"); + shell.printfln(" Status: Network not found"); break; case WL_SCAN_COMPLETED: - shell.printfln(" Network: Network scan complete"); + shell.printfln(" Status: Network scan complete"); break; case WL_CONNECTED: - shell.printfln(" Network: connected"); + shell.printfln(" Status: connected"); shell.printfln(" SSID: %s", WiFi.SSID().c_str()); shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str()); shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI())); @@ -943,7 +943,7 @@ void System::show_system(uuid::console::Shell & shell) { // show Ethernet if connected if (ethernet_connected_) { shell.println(); - shell.printfln(" Ethernet Network: connected"); + shell.printfln(" Status: Ethernet connected"); shell.printfln(" MAC address: %s", ETH.macAddress().c_str()); shell.printfln(" Hostname: %s", ETH.getHostname()); shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str()); @@ -1071,9 +1071,9 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); // node["uptime (seconds)"] = uuid::get_uptime_sec(); #ifndef EMSESP_STANDALONE - node["freemem"] = ESP.getFreeHeap() / 1024; // kilobytes - node["maxalloc"] = ESP.getMaxAllocHeap() / 1024; // kilobytes - node["free_app"] = EMSESP::system_.appFree(); // kilobytes + node["free mem"] = ESP.getFreeHeap() / 1024; // kilobytes + node["max alloc"] = ESP.getMaxAllocHeap() / 1024; // kilobytes + node["free app"] = EMSESP::system_.appFree(); // kilobytes #endif node["reset reason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1); @@ -1081,7 +1081,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp // Network Status node = output.createNestedObject("Network Info"); if (EMSESP::system_.ethernet_connected()) { - node["connection"] = "Ethernet"; + node["network"] = "Ethernet"; node["hostname"] = ETH.getHostname(); node["MAC"] = ETH.macAddress(); node["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask()); @@ -1091,8 +1091,8 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp node["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6()); } } else if (WiFi.status() == WL_CONNECTED) { - node["connection"] = "WiFi"; - node["hostname"] = WiFi.getHostname(); + node["network"] = "WiFi"; + node["hostname"] = WiFi.getHostname(); // node["SSID"] = WiFi.SSID(); // node["BSSID"] = WiFi.BSSIDstr(); node["RSSI"] = WiFi.RSSI(); @@ -1150,7 +1150,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp } EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { node["enabled"] = settings.enabled; - node["client_id"] = settings.clientId; + node["client id"] = settings.clientId; node["keep alive"] = settings.keepAlive; node["clean session"] = settings.cleanSession; node["base"] = settings.base; @@ -1245,7 +1245,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp node["phy type"] = settings.phy_type; if (settings.phy_type != PHY_type::PHY_TYPE_NONE) { node["eth power"] = settings.eth_power; - node["eth phy_addr"] = settings.eth_phy_addr; + node["eth phy addr"] = settings.eth_phy_addr; node["eth clock_mode"] = settings.eth_clock_mode; } node["rx gpio"] = settings.rx_gpio; @@ -1264,6 +1264,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp node["enum format"] = settings.enum_format; node["analog enabled"] = settings.analog_enabled; node["telnet enabled"] = settings.telnet_enabled; + node["web log buffer"] = settings.weblog_buffer; }); // Devices - show EMS devices if we have any @@ -1416,6 +1417,7 @@ void System::ntp_connected(bool b) { if (b != ntp_connected_) { LOG_INFO(b ? "NTP connected" : "NTP disconnected"); // if changed report it } + ntp_connected_ = b; ntp_last_check_ = b ? uuid::get_uptime_sec() : 0; } @@ -1426,6 +1428,7 @@ bool System::ntp_connected() { if ((uuid::get_uptime_sec() - ntp_last_check_ > 7201) && ntp_connected_) { ntp_connected(false); } + return ntp_connected_; } diff --git a/src/system.h b/src/system.h index e13efd3a8..f8ee51dc2 100644 --- a/src/system.h +++ b/src/system.h @@ -77,7 +77,7 @@ class System { bool check_upgrade(); bool heartbeat_json(JsonObject & output); void send_heartbeat(); - void send_info_mqtt(const char * event_str); + void send_info_mqtt(const char * event_str, bool send_ntp = false); bool syslog_enabled() { return syslog_enabled_; diff --git a/src/test/test.cpp b/src/test/test.cpp index 3135f6ba4..44fce64f1 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -1397,9 +1397,21 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const // add a boiler add_device(0x08, 123); // Nefit Trendline - // add a thermostat - add_device(0x18, 157); // Bosch CR100 - https://github.com/emsesp/EMS-ESP/issues/355 + // add some boiler data + // Boiler -> Me, UBAMonitorFast(0x18), telegram: 08 00 18 00 00 02 5A 73 3D 0A 10 65 40 02 1A 80 00 01 E1 01 76 0E 3D 48 00 C9 44 02 00 (#data=25) + uart_telegram({0x08, 0x00, 0x18, 0x00, 0x00, 0x02, 0x5A, 0x73, 0x3D, 0x0A, 0x10, 0x65, 0x40, 0x02, 0x1A, + 0x80, 0x00, 0x01, 0xE1, 0x01, 0x76, 0x0E, 0x3D, 0x48, 0x00, 0xC9, 0x44, 0x02, 0x00}); + // Boiler -> Thermostat, UBAParameterWW(0x33), telegram: 08 97 33 00 23 24 (#data=2) + uart_telegram({0x08, 0x98, 0x33, 0x00, 0x23, 0x24}); + + // Boiler -> Me, UBAParameterWW(0x33), telegram: 08 0B 33 00 08 FF 34 FB 00 28 00 00 46 00 FF FF 00 (#data=13) + uart_telegram({0x08, 0x0B, 0x33, 0x00, 0x08, 0xFF, 0x34, 0xFB, 0x00, 0x28, 0x00, 0x00, 0x46, 0x00, 0xFF, 0xFF, 0x00}); + + // add a thermostat + add_device(0x18, 157); // Bosch CR100 + + // add some thermostat data // RCPLUSStatusMessage_HC1(0x01A5) - HC1 uart_telegram({0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24, 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); @@ -1412,11 +1424,13 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const char boiler_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; char thermostat_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; + char thermostat_topic_hc1[Mqtt::MQTT_TOPIC_MAX_SIZE]; char system_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; Mqtt::show_mqtt(shell); // show queue strlcpy(boiler_topic, "ems-esp/boiler", sizeof(boiler_topic)); strlcpy(thermostat_topic, "ems-esp/thermostat", sizeof(thermostat_topic)); + strlcpy(thermostat_topic_hc1, "ems-esp/thermostat/hc1", sizeof(thermostat_topic)); strlcpy(system_topic, "ems-esp/system", sizeof(system_topic)); // test publishing @@ -1425,11 +1439,13 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const // test receiving EMSESP::mqtt_.incoming(boiler_topic, ""); // test if ignore empty payloads, should return values - EMSESP::mqtt_.incoming(boiler_topic, "12345"); // error: invalid format - EMSESP::mqtt_.incoming("bad_topic", "123456"); // error: no matching topic - EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"garbage\",\"data\":22.52}"); // error: should report error + // these all should fail + EMSESP::mqtt_.incoming(boiler_topic, "12345"); // error: invalid format + EMSESP::mqtt_.incoming("bad_topic", "123456"); // error: no matching topic + EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"garbage\",\"data\":22.52}"); // error: should report error + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":\"1\"}"); // RC35 only, should error - EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"comfort\",\"data\":\"eco\"}"); + // these all should pass EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":\"1\"}"); // with quotes EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":1}"); // without quotes EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"selflowtemp\",\"data\":55}"); @@ -1437,20 +1453,23 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const // test direct commands EMSESP::mqtt_.incoming("ems-esp/boiler/selflowtemp", "56"); - EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"send\",\"data\":\"01 02 03 04 05\"}"); - EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"pin\",\"id\":12,\"data\":\"1\"}"); + EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"send\",\"data\":\"01 02 03 04 05\"}"); // TODO check if works + // EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"pin\",\"id\":12,\"data\":\"1\"}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"wwmode\",\"data\":\"auto\"}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":\"1\"}"); // RC35 only, should error EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"typo\",\"id\":2}"); // invalid mode EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"id\":2}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":2}"); // hc as number - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":19.5,\"hc\":1}"); // data as number - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":\"2\"}"); // hc as string. should error as no hc2 - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22.56}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":\"22.56\"}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"id\":2,\"data\":22}"); + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":2}"); // hc as number + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":19.5,\"hc\":1}"); // data as number + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":\"auto\",\"hc\":\"2\"}"); // hc as string. should error as no hc2 + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":22.56}"); + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":22}"); + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":\"22.56\"}"); + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"id\":2,\"data\":22}"); + + // test with hc + EMSESP::mqtt_.incoming("ems-esp/thermostat/hc1/seltemp", "30"); + EMSESP::mqtt_.incoming("ems-esp/thermostat/hc2/seltemp", "32"); // test single commands EMSESP::mqtt_.incoming(thermostat_topic, "auto"); diff --git a/src/test/test.h b/src/test/test.h index af16002f3..e8d7a287e 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -30,8 +30,8 @@ namespace emsesp { // #define EMSESP_DEBUG_DEFAULT "solar" // #define EMSESP_DEBUG_DEFAULT "mixer" // #define EMSESP_DEBUG_DEFAULT "web" -// #define EMSESP_DEBUG_DEFAULT "mqtt" -#define EMSESP_DEBUG_DEFAULT "general" +#define EMSESP_DEBUG_DEFAULT "mqtt" +// #define EMSESP_DEBUG_DEFAULT "general" // #define EMSESP_DEBUG_DEFAULT "boiler" // #define EMSESP_DEBUG_DEFAULT "mqtt2" // #define EMSESP_DEBUG_DEFAULT "mqtt_nested" diff --git a/src/version.h b/src/version.h index 76199a5b4..1a4fba9ca 100644 --- a/src/version.h +++ b/src/version.h @@ -1,4 +1,4 @@ -#define EMSESP_APP_VERSION "3.5.0b10" +#define EMSESP_APP_VERSION "3.5.0b11" #if CONFIG_IDF_TARGET_ESP32C3 #define EMSESP_PLATFORM "ESP32-C3"; diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 085aec367..0b4de5c36 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -136,7 +136,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject & input) { // send the json that came back from the command call // FAIL, OK, NOT_FOUND, ERROR, NOT_ALLOWED = 400 (bad request), 200 (OK), 400 (not found), 400 (bad request), 401 (unauthorized) - int ret_codes[5] = {400, 200, 400, 400, 401}; + int ret_codes[6] = {400, 200, 400, 400, 401, 400}; response->setCode(ret_codes[return_code]); response->setLength(); response->setContentType("application/json; charset=utf-8");