diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index b38aa99e3..142464e6e 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -4,37 +4,38 @@ ## Added -- Add system commands for syslog level and watch [#98](https://github.com/emsesp/EMS-ESP32/issues/98) -- Added pool data to telegrams 0x494 & 0x495 #102 [#102](https://github.com/emsesp/EMS-ESP32/issues/102) (@Sunbuzz) -- Add RC300 second summermode telegram [#108](https://github.com/emsesp/EMS-ESP32/issues/108) -- Add support for the RC25 thermostat [#106](https://github.com/emsesp/EMS-ESP32/issues/106) -- Add new command 'entities' for a device, e.g. http://ems-esp/api/boiler/entities to show the shortname, description and HA Entity name (if HA enabled) [#116](https://github.com/emsesp/EMS-ESP32/issues/116) +- Add system commands for syslog level and watch [#98] +- Added pool data to telegrams 0x494 & 0x495 [#102] +- Add RC300 second summermode telegram [#108] +- Add support for the RC25 thermostat [#106] +- Add new command 'entities' for a device, e.g. http://ems-esp/api/boiler/entities to show the shortname, description and HA Entity name (if HA enabled) [#116] - Support for Junkers program and remote (fb10/fb110) temperature -- Home Assistant `state_class` attribute for Wh, kWh, W and KW [#129](https://github.com/emsesp/EMS-ESP32/issues/129) -- Add current room influence for RC300[#136] +- Home Assistant `state_class` attribute for Wh, kWh, W and KW [#129] +- Add current room influence for RC300 [#136] - Added Home Assistant device_class to sensor entities -- Added another Buderus RC10 thermostat with Product ID 65 [#160](https://github.com/emsesp/EMS-ESP32/issues/160) -- Added support for mDNS [#161](https://github.com/emsesp/EMS-ESP32/issues/161) +- Added another Buderus RC10 thermostat with Product ID 65 [#160] +- Added support for mDNS [#161] - Added last system ESP32 reset code to log (and `system info` output) -- Firmware Checker in WebUI [#168](https://github.com/emsesp/EMS-ESP32/issues/168) +- Firmware Checker in WebUI [#168] - Added new MQTT setting for enabling 'response' topic -- Support for non-standard Thermostats like Tado [#174](https://github.com/emsesp/EMS-ESP32/issues/174) +- Support for non-standard Thermostats like Tado [#174] - Include MQTT connection status in 'api/system/info' ## Fixed -- MQTT reconnecting after WiFi reconnect [#99](https://github.com/emsesp/EMS-ESP32/issues/99) -- Manually Controlling Solar Circuit [#107](https://github.com/emsesp/EMS-ESP32/issues/107) -- Fix thermostat commands not defaulting to the master thermostat [#110](https://github.com/emsesp/EMS-ESP32/issues/110) +- MQTT reconnecting after WiFi reconnect [#99] +- Manually Controlling Solar Circuit [#107] +- Fix thermostat commands not defaulting to the master thermostat [#110] - Enlarge parse-buffer for long names like `cylinderpumpmodulation` -- MQTT not subscribing to all device entities [#166](https://github.com/emsesp/EMS-ESP32/issues/166) -- Help fix issues with WebUI unable to fully load UI over Ethernet [#177](https://github.com/emsesp/EMS-ESP32/issues/177) -- Shower alert never reset after limit reached when enabled [(PR #185)](https://github.com/emsesp/EMS-ESP32/pull/185) +- MQTT not subscribing to all device entities [#166] +- Help fix issues with WebUI unable to fully load UI over Ethernet [#177] +- Shower alert never reset after limit reached when enabled [(PR #185)] +- Remove HA entity entries when a device value goes dormant [#196] ## Changed -- Syslog BOM only for utf-8 messages [#91](https://github.com/emsesp/EMS-ESP32/issues/91) -- Check for KM200 by device-id 0x48, remove tx-delay [#90](https://github.com/emsesp/EMS-ESP32/issues/90) +- Syslog BOM only for utf-8 messages [#91] +- Check for KM200 by device-id 0x48, remove tx-delay [#90] - rename `fastheatupfactor` to `fastheatup` and add percent [#122] - "unit" renamed to "uom" in API call to recall a Device Value - initial backend React changes to replace the class components (HOCs) with React Hooks @@ -42,8 +43,8 @@ - Boiler's maintenancemessage always published in MQTT (to prevent HA missing entity) - Unit of Measure 'times' added to MQTT Fails, Rx fails, Rx received, Tx fails, Tx reads & Tx writes - Improved API. Restful HTTP API works in the same way as MQTT calls -- Removed settings for MQTT subscribe format [#173](https://github.com/emsesp/EMS-ESP32/issues/173) -- Improve moduline 200 functionality [#183](https://github.com/emsesp/EMS-ESP32/issues/183) +- Removed settings for MQTT subscribe format [#173] +- Improve moduline 200 functionality [#183] ## **BREAKING CHANGES** diff --git a/interface/src/project/EMSESPDataForm.tsx b/interface/src/project/EMSESPDataForm.tsx index 9c3b40d25..7f9f0d5c4 100644 --- a/interface/src/project/EMSESPDataForm.tsx +++ b/interface/src/project/EMSESPDataForm.tsx @@ -219,7 +219,9 @@ class EMSESPDataForm extends Component< }; sendCommand = (dv: DeviceValue) => { - this.setState({ edit_devicevalue: dv }); + if (dv.c && this.props.authenticatedContext.me.admin) { + this.setState({ edit_devicevalue: dv }); + } }; handleSensorChange = (name: keyof Sensor) => ( @@ -285,7 +287,9 @@ class EMSESPDataForm extends Component< }; sendSensor = (sn: Sensor) => { - this.setState({ edit_Sensor: sn }); + if (this.props.authenticatedContext.me.admin) { + this.setState({ edit_Sensor: sn }); + } }; noDevices = () => { @@ -391,7 +395,11 @@ class EMSESPDataForm extends Component< {data.sensors.map((sensorData) => ( - + this.sendSensor(sensorData)} + > {me.admin && ( @@ -553,7 +561,6 @@ class EMSESPDataForm extends Component< renderDeviceData() { const { deviceData } = this.state; const { width } = this.props; - const me = this.props.authenticatedContext.me; if (this.noDevices()) { return; @@ -580,9 +587,13 @@ class EMSESPDataForm extends Component< {deviceData.data.map((item, i) => ( - + this.sendCommand(item)} + > - {item.c && me.admin && ( + {item.c && this.props.authenticatedContext.me.admin && ( (event: React.ChangeEvent) => void; } +function formatValue(value: any, uom: number) { + if (uom === DeviceValueUOM.DEGREES) { + return new Intl.NumberFormat(undefined, { + minimumFractionDigits: 1 + }).format(value); + } + + return value; +} + class ValueForm extends React.Component { formRef: RefObject = React.createRef(); @@ -69,7 +79,7 @@ class ValueForm extends React.Component { {!devicevalue.l && ( sorted_cmds; for (const auto & cf : cmdfunctions_) { if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) { - sorted_cmds.push_back(uuid::read_flash_string(cf.cmd_)); + sorted_cmds.push_back(read_flash_string(cf.cmd_)); } } sorted_cmds.sort(); for (auto & cl : sorted_cmds) { for (const auto & cf : cmdfunctions_) { - if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) { + if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == read_flash_string(cf.cmd_))) { output[cl] = cf.description_; } } @@ -380,7 +380,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo std::list sorted_cmds; for (const auto & cf : cmdfunctions_) { if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) { - sorted_cmds.push_back(uuid::read_flash_string(cf.cmd_)); + sorted_cmds.push_back(read_flash_string(cf.cmd_)); } } sorted_cmds.sort(); @@ -400,7 +400,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo for (auto & cl : sorted_cmds) { // find and print the description for (const auto & cf : cmdfunctions_) { - if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) { + if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == read_flash_string(cf.cmd_))) { uint8_t i = cl.length(); shell.print(" "); if (cf.has_flags(MQTT_SUB_FLAG_HC)) { @@ -420,7 +420,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo shell.print(EMSdevice::tag_to_string(TAG_DEVICE_DATA_WW)); shell.print(' '); } - shell.print(uuid::read_flash_string(cf.description_)); + shell.print(read_flash_string(cf.description_)); if (!cf.has_flags(CommandFlag::ADMIN_ONLY)) { shell.print(' '); shell.print(COLOR_BRIGHT_RED); diff --git a/src/console.cpp b/src/console.cpp index f3f1c2d88..ce71a9217 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -235,7 +235,7 @@ void EMSESPShell::add_console_commands() { shell.printfln(F_(bus_id_fmt), settings.ems_bus_id); char buffer[4]; shell.printfln(F_(master_thermostat_fmt), - settings.master_thermostat == 0 ? uuid::read_flash_string(F_(auto)).c_str() + settings.master_thermostat == 0 ? read_flash_string(F_(auto)).c_str() : Helpers::hextoa(buffer, settings.master_thermostat)); shell.printfln(F_(board_profile_fmt), settings.board_profile.c_str()); }); @@ -278,8 +278,7 @@ void EMSESPShell::add_console_commands() { settings.master_thermostat = value; EMSESP::actual_master_thermostat(value); // set the internal value too char buffer[5]; - shell.printfln(F_(master_thermostat_fmt), - !value ? uuid::read_flash_string(F_(auto)).c_str() : Helpers::hextoa(buffer, value)); + shell.printfln(F_(master_thermostat_fmt), !value ? read_flash_string(F_(auto)).c_str() : Helpers::hextoa(buffer, value)); return StateUpdateResult::CHANGED; }, "local"); @@ -446,7 +445,7 @@ void EMSESPShell::add_console_commands() { if (Command::device_has_commands(device_type)) { for (const auto & cf : Command::commands()) { if (cf.device_type_ == device_type) { - command_list.emplace_back(uuid::read_flash_string(cf.cmd_)); + command_list.emplace_back(read_flash_string(cf.cmd_)); } } return command_list; @@ -823,14 +822,14 @@ std::string EMSESPShell::prompt_suffix() { } void EMSESPShell::end_of_transmission() { - invoke_command(uuid::read_flash_string(F_(exit))); + invoke_command(read_flash_string(F_(exit))); } EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, bool local) : uuid::console::Shell(commands, ShellContext::MAIN, local ? (CommandFlags::USER | CommandFlags::LOCAL) : CommandFlags::USER) , uuid::console::StreamConsole(stream) , EMSESPShell() - , name_(uuid::read_flash_string(F("Serial"))) + , name_(read_flash_string(F("Serial"))) , pty_(SIZE_MAX) , addr_() , port_(0) { diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 11e1b0c8d..5b35d0911 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -267,7 +267,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const } // publish HA config -bool Boiler::publish_ha_config() { +bool Boiler::publish_ha_device_config() { StaticJsonDocument doc; doc["uniq_id"] = F_(boiler); doc["ic"] = F_(icondevice); @@ -1407,7 +1407,7 @@ bool Boiler::set_reset(const char * value, const int8_t id) { bool Boiler::set_maintenance(const char * value, const int8_t id) { std::string s(12, '\0'); if (Helpers::value2string(value, s)) { - if (s == Helpers::toLower(uuid::read_flash_string(F_(reset)))) { + if (s == Helpers::toLower(read_flash_string(F_(reset)))) { LOG_INFO(F("Reset boiler maintenance message")); write_command(0x05, 0x08, 0xFF, 0x1C); return true; diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 3fa6e494b..7f5332c62 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -27,7 +27,7 @@ class Boiler : public EMSdevice { public: Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/connect.cpp b/src/devices/connect.cpp index 2a16039d0..9ee937b90 100644 --- a/src/devices/connect.cpp +++ b/src/devices/connect.cpp @@ -29,7 +29,7 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con } // publish HA config -bool Connect::publish_ha_config() { +bool Connect::publish_ha_device_config() { return true; } diff --git a/src/devices/connect.h b/src/devices/connect.h index 960ad61ef..87b9a39d9 100644 --- a/src/devices/connect.h +++ b/src/devices/connect.h @@ -27,7 +27,7 @@ class Connect : public EMSdevice { public: Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/controller.cpp b/src/devices/controller.cpp index 6a50c138a..6367bb173 100644 --- a/src/devices/controller.cpp +++ b/src/devices/controller.cpp @@ -29,7 +29,7 @@ Controller::Controller(uint8_t device_type, uint8_t device_id, uint8_t product_i } // publish HA config -bool Controller::publish_ha_config() { +bool Controller::publish_ha_device_config() { return true; } diff --git a/src/devices/controller.h b/src/devices/controller.h index 67496b64b..fbd9978b3 100644 --- a/src/devices/controller.h +++ b/src/devices/controller.h @@ -27,7 +27,7 @@ class Controller : public EMSdevice { public: Controller(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/gateway.cpp b/src/devices/gateway.cpp index b162d9037..11d9169d9 100644 --- a/src/devices/gateway.cpp +++ b/src/devices/gateway.cpp @@ -29,7 +29,7 @@ Gateway::Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, con } // publish HA config -bool Gateway::publish_ha_config() { +bool Gateway::publish_ha_device_config() { return true; } diff --git a/src/devices/gateway.h b/src/devices/gateway.h index df47ddeb0..97295c1c9 100644 --- a/src/devices/gateway.h +++ b/src/devices/gateway.h @@ -27,7 +27,7 @@ class Gateway : public EMSdevice { public: Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/generic.cpp b/src/devices/generic.cpp index 516e92fa4..9616151dd 100644 --- a/src/devices/generic.cpp +++ b/src/devices/generic.cpp @@ -34,7 +34,7 @@ Generic::Generic(uint8_t device_type, uint8_t device_id, uint8_t product_id, con } // publish HA config -bool Generic::publish_ha_config() { +bool Generic::publish_ha_device_config() { return true; } diff --git a/src/devices/generic.h b/src/devices/generic.h index 7170bedbf..19e8c0c01 100644 --- a/src/devices/generic.h +++ b/src/devices/generic.h @@ -27,7 +27,7 @@ class Generic : public EMSdevice { public: Generic(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/heatpump.cpp b/src/devices/heatpump.cpp index d4c064c29..de10bc9e7 100644 --- a/src/devices/heatpump.cpp +++ b/src/devices/heatpump.cpp @@ -39,7 +39,7 @@ Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, c } // publish HA config -bool Heatpump::publish_ha_config() { +bool Heatpump::publish_ha_device_config() { StaticJsonDocument doc; doc["uniq_id"] = F_(heatpump); doc["ic"] = F_(icondevice); diff --git a/src/devices/heatpump.h b/src/devices/heatpump.h index 6dc6e8237..ef69a6358 100644 --- a/src/devices/heatpump.h +++ b/src/devices/heatpump.h @@ -27,7 +27,7 @@ class Heatpump : public EMSdevice { public: Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/mixer.cpp b/src/devices/mixer.cpp index 460738c72..53fbb097b 100644 --- a/src/devices/mixer.cpp +++ b/src/devices/mixer.cpp @@ -100,7 +100,7 @@ Mixer::Mixer(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s } // publish HA config -bool Mixer::publish_ha_config() { +bool Mixer::publish_ha_device_config() { // if we don't have valid values for this HC don't add it ever again if (!Helpers::hasValue(pumpStatus_)) { return false; diff --git a/src/devices/mixer.h b/src/devices/mixer.h index 2583e05f7..7bb4abd18 100644 --- a/src/devices/mixer.h +++ b/src/devices/mixer.h @@ -27,7 +27,7 @@ class Mixer : public EMSdevice { public: Mixer(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index a6921848f..667b5ee2f 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -180,7 +180,7 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s } // publish HA config -bool Solar::publish_ha_config() { +bool Solar::publish_ha_device_config() { StaticJsonDocument doc; doc["uniq_id"] = F_(solar); doc["ic"] = F_(icondevice); diff --git a/src/devices/solar.h b/src/devices/solar.h index b0a68d0ea..e0a44c7de 100644 --- a/src/devices/solar.h +++ b/src/devices/solar.h @@ -27,7 +27,7 @@ class Solar : public EMSdevice { public: Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/switch.cpp b/src/devices/switch.cpp index 0514f0a26..f424d7654 100644 --- a/src/devices/switch.cpp +++ b/src/devices/switch.cpp @@ -40,7 +40,7 @@ Switch::Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const } // publish HA config -bool Switch::publish_ha_config() { +bool Switch::publish_ha_device_config() { // if we don't have valid values don't add it ever again if (!Helpers::hasValue(flowTempHc_)) { return false; diff --git a/src/devices/switch.h b/src/devices/switch.h index d821f1df5..3b1c9e560 100644 --- a/src/devices/switch.h +++ b/src/devices/switch.h @@ -27,7 +27,7 @@ class Switch : public EMSdevice { public: Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index b0d139b19..5bfcfa2a0 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -192,7 +192,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i } // publish HA config -bool Thermostat::publish_ha_config() { +bool Thermostat::publish_ha_device_config() { StaticJsonDocument doc; doc["uniq_id"] = F_(thermostat); doc["ic"] = F_(icondevice); @@ -384,7 +384,7 @@ std::shared_ptr Thermostat::heating_circuit(std::sha // publish config topic for HA MQTT Discovery for each of the heating circuit // e.g. homeassistant/climate/ems-esp/thermostat_hc1/config void Thermostat::publish_ha_config_hc(std::shared_ptr hc) { - uint8_t hc_num = hc->hc_num(); + uint8_t hc_num = hc->hc_num(); StaticJsonDocument doc; char str1[20]; @@ -437,7 +437,7 @@ void Thermostat::publish_ha_config_hc(std::shared_ptrget_model() != EMSdevice::EMS_DEVICE_FLAG_RC10){ + if (hc->get_model() != EMSdevice::EMS_DEVICE_FLAG_RC10) { modes.add("auto"); } modes.add("heat"); @@ -1798,26 +1798,26 @@ bool Thermostat::set_mode(const char * value, const int8_t id) { uint8_t num = value[0] - '0'; switch (model()) { case EMSdevice::EMS_DEVICE_FLAG_RC10: - mode = uuid::read_flash_string(FL_(enum_mode6)[num]); + mode = read_flash_string(FL_(enum_mode6)[num]); break; case EMSdevice::EMS_DEVICE_FLAG_RC20: case EMSdevice::EMS_DEVICE_FLAG_RC20_N: - mode = uuid::read_flash_string(FL_(enum_mode2)[num]); + mode = read_flash_string(FL_(enum_mode2)[num]); break; case EMSdevice::EMS_DEVICE_FLAG_RC30: case EMSdevice::EMS_DEVICE_FLAG_RC35: case EMSdevice::EMS_DEVICE_FLAG_RC30_N: - mode = uuid::read_flash_string(FL_(enum_mode3)[num]); + mode = read_flash_string(FL_(enum_mode3)[num]); break; case EMSdevice::EMS_DEVICE_FLAG_RC300: case EMSdevice::EMS_DEVICE_FLAG_RC100: - mode = uuid::read_flash_string(FL_(enum_mode)[num]); + mode = read_flash_string(FL_(enum_mode)[num]); break; case EMSdevice::EMS_DEVICE_FLAG_JUNKERS: - mode = uuid::read_flash_string(FL_(enum_mode4)[num]); + mode = read_flash_string(FL_(enum_mode4)[num]); break; case EMSdevice::EMS_DEVICE_FLAG_CRF: - mode = uuid::read_flash_string(FL_(enum_mode5)[num]); + mode = read_flash_string(FL_(enum_mode5)[num]); break; default: LOG_WARNING(F("Set mode: Invalid mode")); @@ -2673,13 +2673,8 @@ void Thermostat::register_device_values() { FL_(ibaCalIntTemperature), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_calinttemp)); - register_device_value(TAG_THERMOSTAT_DATA, - &heatingpid_, - DeviceValueType::ENUM, - FL_(enum_PID), - FL_(heatingPID), - DeviceValueUOM::NONE, - MAKE_CF_CB(set_heatingpid)); + register_device_value( + TAG_THERMOSTAT_DATA, &heatingpid_, DeviceValueType::ENUM, FL_(enum_PID), FL_(heatingPID), DeviceValueUOM::NONE, MAKE_CF_CB(set_heatingpid)); register_device_value(TAG_THERMOSTAT_DATA, &backlight_, DeviceValueType::BOOL, nullptr, FL_(backlight), DeviceValueUOM::NONE, MAKE_CF_CB(set_backlight)); register_device_value(TAG_DEVICE_DATA_WW, &wwMode_, DeviceValueType::ENUM, FL_(enum_wwMode3), FL_(wwMode), DeviceValueUOM::NONE, MAKE_CF_CB(set_wwmode)); break; diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index cfad3e888..9cba96ec4 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -127,7 +127,7 @@ class Thermostat : public EMSdevice { static std::string mode_tostring(uint8_t mode); - virtual bool publish_ha_config(); + virtual bool publish_ha_device_config(); private: static uuid::log::Logger logger_; diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 7255d422c..f3ca772b5 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -117,18 +117,18 @@ static const __FlashStringHelper * const DeviceValueTAG_mqtt[] PROGMEM = { }; const std::string EMSdevice::tag_to_string(uint8_t tag) { - return uuid::read_flash_string(DeviceValueTAG_s[tag]); + return read_flash_string(DeviceValueTAG_s[tag]); } const std::string EMSdevice::tag_to_mqtt(uint8_t tag) { - return uuid::read_flash_string(DeviceValueTAG_mqtt[tag]); + return read_flash_string(DeviceValueTAG_mqtt[tag]); } const std::string EMSdevice::uom_to_string(uint8_t uom) { if (uom == DeviceValueUOM::NONE) { return std::string{}; } - return uuid::read_flash_string(DeviceValueUOM_s[uom - 1]); // offset by 1 to account for NONE + return read_flash_string(DeviceValueUOM_s[uom - 1]); // offset by 1 to account for NONE } const std::vector EMSdevice::devicevalues() const { @@ -374,13 +374,13 @@ bool EMSdevice::is_fetch(uint16_t telegram_id) { // list of registered device entries, adding the HA entity if it exists void EMSdevice::list_device_entries(JsonObject & output) { for (const auto & dv : devicevalues_) { - if (dv_is_visible(dv) && dv.type != DeviceValueType::CMD) { + if (dv.has_state(DeviceValueState::DV_VISIBLE) && dv.type != DeviceValueType::CMD) { // if we have a tag prefix it char key[50]; if (!EMSdevice::tag_to_string(dv.tag).empty()) { - snprintf(key, 50, "%s.%s", EMSdevice::tag_to_string(dv.tag).c_str(), uuid::read_flash_string(dv.short_name).c_str()); + snprintf(key, 50, "%s.%s", EMSdevice::tag_to_string(dv.tag).c_str(), read_flash_string(dv.short_name).c_str()); } else { - snprintf(key, 50, "%s", uuid::read_flash_string(dv.short_name).c_str()); + snprintf(key, 50, "%s", read_flash_string(dv.short_name).c_str()); } JsonArray details = output.createNestedArray(key); @@ -405,14 +405,14 @@ void EMSdevice::list_device_entries(JsonObject & output) { const std::string EMSdevice::device_entity_ha(DeviceValue const & dv) { std::string entity_name(50, '\0'); if (EMSdevice::tag_to_string(dv.tag).empty()) { - snprintf(&entity_name[0], entity_name.capacity() + 1, "sensor.%s %s", this->device_type_name().c_str(), uuid::read_flash_string(dv.full_name).c_str()); + snprintf(&entity_name[0], entity_name.capacity() + 1, "sensor.%s %s", this->device_type_name().c_str(), read_flash_string(dv.full_name).c_str()); } else { snprintf(&entity_name[0], entity_name.capacity() + 1, "sensor.%s %s %s", this->device_type_name().c_str(), EMSdevice::tag_to_string(dv.tag).c_str(), - uuid::read_flash_string(dv.full_name).c_str()); + read_flash_string(dv.full_name).c_str()); } std::replace(entity_name.begin(), entity_name.end(), ' ', '_'); return Helpers::toLower(entity_name); @@ -512,7 +512,11 @@ void EMSdevice::register_device_value(uint8_t tag, }; } - devicevalues_.emplace_back(device_type_, tag, value_p, type, options, options_size, short_name, full_name, uom, 0, has_cmd, min, max); + // set state + // if fullname is empty don't set the flag to visible (used for hamode and hatemp) + uint8_t state = (full_name) ? DeviceValueState::DV_VISIBLE : DeviceValueState::DV_DEFAULT; + + devicevalues_.emplace_back(device_type_, tag, value_p, type, options, options_size, short_name, full_name, uom, 0, has_cmd, min, max, state); } // function with min and max values @@ -581,7 +585,7 @@ const std::string EMSdevice::get_value_uom(const char * key) { size_t sz = sizeof(DeviceValueTAG_s) / sizeof(__FlashStringHelper *); for (uint8_t i = 0; i < sz; i++) { - auto tag = uuid::read_flash_string(DeviceValueTAG_s[i]); + auto tag = read_flash_string(DeviceValueTAG_s[i]); if (!tag.empty()) { std::string key2 = key; // copy char to a std::string if ((key2.find(tag) != std::string::npos) && (key[tag.length()] == ' ')) { @@ -593,8 +597,8 @@ const std::string EMSdevice::get_value_uom(const char * key) { // look up key in our device value list for (const auto & dv : devicevalues_) { - if (dv_is_visible(dv)) { - if (uuid::read_flash_string(dv.full_name) == key_p) { + if (dv.has_state(DeviceValueState::DV_VISIBLE)) { + if (read_flash_string(dv.full_name) == key_p) { // ignore TIME since "minutes" is already added to the string value if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) { break; @@ -608,51 +612,50 @@ const std::string EMSdevice::get_value_uom(const char * key) { } // prepare array of device values used for the WebUI +// this is loosely based of the function generate_values_json used for the MQTT and Console +// except additional data is stored in the JSON document needed for the Web UI like the UOM and command // v = value, u=uom, n=name, c=cmd void EMSdevice::generate_values_json_web(JsonObject & output) { output["name"] = to_string_short(); JsonArray data = output.createNestedArray("data"); for (const auto & dv : devicevalues_) { + // check conditions: + // 1. full_name cannot be empty + // 2. it can't be a command (like publish) + // 3. it must have a valid value + // ignore if full_name empty and also commands - if (dv_is_visible(dv) && dv.type != DeviceValueType::CMD) { + if (dv.has_state(DeviceValueState::DV_VISIBLE) && (dv.type != DeviceValueType::CMD) && check_dv_hasvalue(dv)) { JsonObject obj; // create the object, if needed // handle Booleans (true, false) - if ((dv.type == DeviceValueType::BOOL) && Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) { + if (dv.type == DeviceValueType::BOOL) { obj = data.createNestedObject(); obj["v"] = *(bool *)(dv.value_p) ? "on" : "off"; } // handle TEXT strings - else if ((dv.type == DeviceValueType::STRING) && (Helpers::hasValue((char *)(dv.value_p)))) { + else if (dv.type == DeviceValueType::STRING) { obj = data.createNestedObject(); obj["v"] = (char *)(dv.value_p); } // handle ENUMs - else if ((dv.type == DeviceValueType::ENUM) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) { - if (*(uint8_t *)(dv.value_p) < dv.options_size) { - obj = data.createNestedObject(); - obj["v"] = dv.options[*(uint8_t *)(dv.value_p)]; - } + else if ((dv.type == DeviceValueType::ENUM) && (*(uint8_t *)(dv.value_p) < dv.options_size)) { + obj = data.createNestedObject(); + obj["v"] = dv.options[*(uint8_t *)(dv.value_p)]; } - // // handle commands without value - // else if (dv.type == DeviceValueType::CMD) { - // obj = data.createNestedObject(); - // obj["v"] = ""; - // } - + // handle Integers and Floats else { - // handle Integers and Floats // If a divider is specified, do the division to 2 decimals places and send back as double/float // otherwise force as an integer whole // the nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler uint8_t divider = 0; uint8_t factor = 1; if (dv.options_size == 1) { - const char * s = uuid::read_flash_string(dv.options[0]).c_str(); + const char * s = read_flash_string(dv.options[0]).c_str(); if (s[0] == '*') { factor = Helpers::atoint(&s[1]); } else { @@ -660,22 +663,22 @@ void EMSdevice::generate_values_json_web(JsonObject & output) { } } - if ((dv.type == DeviceValueType::INT) && Helpers::hasValue(*(int8_t *)(dv.value_p))) { + if (dv.type == DeviceValueType::INT) { obj = data.createNestedObject(); obj["v"] = (divider) ? Helpers::round2(*(int8_t *)(dv.value_p), divider) : *(int8_t *)(dv.value_p) * factor; - } else if ((dv.type == DeviceValueType::UINT) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::UINT) { obj = data.createNestedObject(); obj["v"] = (divider) ? Helpers::round2(*(uint8_t *)(dv.value_p), divider) : *(uint8_t *)(dv.value_p) * factor; - } else if ((dv.type == DeviceValueType::SHORT) && Helpers::hasValue(*(int16_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::SHORT) { obj = data.createNestedObject(); obj["v"] = (divider) ? Helpers::round2(*(int16_t *)(dv.value_p), divider) : *(int16_t *)(dv.value_p) * factor; - } else if ((dv.type == DeviceValueType::USHORT) && Helpers::hasValue(*(uint16_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::USHORT) { obj = data.createNestedObject(); obj["v"] = (divider) ? Helpers::round2(*(uint16_t *)(dv.value_p), divider) : *(uint16_t *)(dv.value_p) * factor; - } else if ((dv.type == DeviceValueType::ULONG) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::ULONG) { obj = data.createNestedObject(); obj["v"] = divider ? Helpers::round2(*(uint32_t *)(dv.value_p), divider) : *(uint32_t *)(dv.value_p) * factor; - } else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::TIME) { uint32_t time_value = *(uint32_t *)(dv.value_p); obj = data.createNestedObject(); obj["v"] = (divider > 0) ? time_value / divider : time_value * factor; // sometimes we need to divide by 60 @@ -684,15 +687,14 @@ void EMSdevice::generate_values_json_web(JsonObject & output) { // check if we've added a data element then add the remaining elements if (obj.containsKey("v")) { - // add the unit of measure (uom) - obj["u"] = dv.uom; + obj["u"] = dv.uom; // add the unit of measure (uom) // add name, prefixing the tag if it exists if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) { obj["n"] = dv.full_name; } else { char name[50]; - snprintf(name, sizeof(name), "%s %s", tag_to_string(dv.tag).c_str(), uuid::read_flash_string(dv.full_name).c_str()); + snprintf(name, sizeof(name), "%s %s", tag_to_string(dv.tag).c_str(), read_flash_string(dv.full_name).c_str()); obj["n"] = name; } @@ -700,7 +702,7 @@ void EMSdevice::generate_values_json_web(JsonObject & output) { if (dv.has_cmd) { // add the name of the Command function if (dv.tag >= DeviceValueTAG::TAG_HC1) { - obj["c"] = tag_to_string(dv.tag) + "/" + uuid::read_flash_string(dv.short_name); + obj["c"] = tag_to_string(dv.tag) + "/" + read_flash_string(dv.short_name); } else { obj["c"] = dv.short_name; } @@ -708,8 +710,8 @@ void EMSdevice::generate_values_json_web(JsonObject & output) { if (dv.type == DeviceValueType::ENUM) { JsonArray l = obj.createNestedArray("l"); for (uint8_t i = 0; i < dv.options_size; i++) { - if (!uuid::read_flash_string(dv.options[i]).empty()) { - l.add(uuid::read_flash_string(dv.options[i])); + if (!read_flash_string(dv.options[i]).empty()) { + l.add(read_flash_string(dv.options[i])); } } } @@ -724,7 +726,7 @@ void EMSdevice::generate_values_json_web(JsonObject & output) { } #if defined(EMSESP_DEBUG) - // serializeJson(data, Serial); // debug only +// serializeJson(data, Serial); // debug only #endif } @@ -746,11 +748,12 @@ bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8 // search device value with this tag for (auto & dv : devicevalues_) { - if (dv_is_visible(dv) && (strcmp(cmd, Helpers::toLower(uuid::read_flash_string(dv.short_name)).c_str()) == 0 && (tag <= 0 || tag == dv.tag))) { + if (dv.has_state(DeviceValueState::DV_VISIBLE) + && (strcmp(cmd, Helpers::toLower(read_flash_string(dv.short_name)).c_str()) == 0 && (tag <= 0 || tag == dv.tag))) { uint8_t divider = 0; uint8_t factor = 1; if (dv.options_size == 1) { - const char * s = uuid::read_flash_string(dv.options[0]).c_str(); + const char * s = read_flash_string(dv.options[0]).c_str(); if (s[0] == '*') { factor = Helpers::atoint(&s[1]); } else { @@ -767,7 +770,7 @@ bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8 if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) { json["fullname"] = dv.full_name; } else { - json["fullname"] = tag_to_string(dv.tag) + " " + uuid::read_flash_string(dv.full_name); + json["fullname"] = tag_to_string(dv.tag) + " " + read_flash_string(dv.full_name); } if (!tag_to_mqtt(dv.tag).empty()) { @@ -784,7 +787,7 @@ bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8 } } json[type] = F_(enum); - // uint8_t min_ = (uuid::read_flash_string(dv.options[0]) == "") ? 1 : 0; + // uint8_t min_ = (read_flash_string(dv.options[0]) == "") ? 1 : 0; // json[min] = min_; // json[max] = dv.options_size - 1; JsonArray enum_ = json.createNestedArray(F_(enum)); @@ -800,7 +803,7 @@ bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8 } json[type] = F_(number); json[min] = 0; - json[max] = divider ? EMS_VALUE_USHORT_NOTSET / divider : EMS_VALUE_USHORT_NOTSET; + json[max] = divider ? EMS_VALUE_USHORT_NOTSET / divider : EMS_VALUE_USHORT_NOTSET - 1; break; case DeviceValueType::UINT: @@ -812,7 +815,7 @@ bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8 if (dv.uom == DeviceValueUOM::PERCENT) { json[max] = 100; } else { - json[max] = divider ? EMS_VALUE_UINT_NOTSET / divider : EMS_VALUE_UINT_NOTSET; + json[max] = divider ? EMS_VALUE_UINT_NOTSET / divider : EMS_VALUE_UINT_NOTSET - 1; } break; @@ -822,7 +825,7 @@ bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8 } json[type] = F_(number); json[min] = divider ? -EMS_VALUE_SHORT_NOTSET / divider : -EMS_VALUE_SHORT_NOTSET; - json[max] = divider ? EMS_VALUE_SHORT_NOTSET / divider : EMS_VALUE_SHORT_NOTSET; + json[max] = divider ? EMS_VALUE_SHORT_NOTSET / divider : EMS_VALUE_SHORT_NOTSET - 1; break; case DeviceValueType::INT: @@ -835,7 +838,7 @@ bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8 json[max] = 100; } else { json[min] = divider ? -EMS_VALUE_INT_NOTSET / divider : -EMS_VALUE_INT_NOTSET; - json[max] = divider ? EMS_VALUE_INT_NOTSET / divider : EMS_VALUE_INT_NOTSET; + json[max] = divider ? EMS_VALUE_INT_NOTSET / divider : EMS_VALUE_INT_NOTSET - 1; } break; @@ -932,42 +935,53 @@ bool EMSdevice::generate_values_json(JsonObject & output, const uint8_t tag_filt JsonObject json = output; for (auto & dv : devicevalues_) { - // conditions - bool condition; - condition = (tag_filter == DeviceValueTAG::TAG_NONE) || (tag_filter == dv.tag); // tag must be either empty or match a tag passed to this function + // check conditions: + // 1. it must have a valid value + // 2. it must be visible, unless our output destination is MQTT + // 3. it must match the given tag filter or have an empty tag - if (output_target != OUTPUT_TARGET::MQTT) { - condition &= dv_is_visible(dv); // value must be visible if outputting to API (web or console). This is for ID, hamode, hatemp etc + // check if it exists. We set the value activated once here + bool has_value = check_dv_hasvalue(dv); + if (has_value) { + dv.add_state(DeviceValueState::DV_ACTIVE); + } else { + dv.remove_state(DeviceValueState::DV_ACTIVE); } - bool has_value = false; + bool conditions = ((tag_filter == DeviceValueTAG::TAG_NONE) || (tag_filter == dv.tag)) && has_value; + if (output_target != OUTPUT_TARGET::MQTT) { + conditions &= + dv.has_state(DeviceValueState::DV_VISIBLE); // value must be visible if outputting to API (web or console). This is for ID, hamode, hatemp etc + } + + if (conditions) { + has_values = true; // we actually have data - if (condition) { // we have a tag if it matches the filter given, and that the tag name is not empty/"" bool have_tag = ((dv.tag != tag_filter) && !tag_to_string(dv.tag).empty()); + // create the name for the JSON key char name[80]; if (output_target == OUTPUT_TARGET::API_VERBOSE) { - // prefix the tag in brackets, unless it's Boiler because we're naughty and use tag for the MQTT topic if (have_tag) { - snprintf(name, 80, "%s %s", tag_to_string(dv.tag).c_str(), uuid::read_flash_string(dv.full_name).c_str()); + snprintf(name, 80, "%s %s", tag_to_string(dv.tag).c_str(), read_flash_string(dv.full_name).c_str()); // prefix the tag } else { - strcpy(name, uuid::read_flash_string(dv.full_name).c_str()); // use full name + strcpy(name, read_flash_string(dv.full_name).c_str()); // use full name } } else { - strcpy(name, uuid::read_flash_string(dv.short_name).c_str()); // use short name + strcpy(name, read_flash_string(dv.short_name).c_str()); // use short name - // if we have a tag, and its different to the last one create a nested object + // if we have a tag, and its different to the last one create a nested object. only for hc, wwc and hs if (dv.tag != old_tag) { old_tag = dv.tag; - if (nested && have_tag && dv.tag >= DeviceValueTAG::TAG_HC1) { // no nests for boiler tags + if (nested && have_tag && dv.tag >= DeviceValueTAG::TAG_HC1) { json = output.createNestedObject(tag_to_string(dv.tag)); } } } // handle Booleans (true, false) - if ((dv.type == DeviceValueType::BOOL) && Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) { + if (dv.type == DeviceValueType::BOOL) { // see how to render the value depending on the setting uint8_t bool_format = EMSESP::bool_format(); if (bool_format == BOOL_FORMAT_ONOFF) { @@ -979,17 +993,15 @@ bool EMSdevice::generate_values_json(JsonObject & output, const uint8_t tag_filt } else { json[name] = (uint8_t)(*(uint8_t *)(dv.value_p)) ? 1 : 0; } - has_value = true; } // handle TEXT strings - else if ((dv.type == DeviceValueType::STRING) && (Helpers::hasValue((char *)(dv.value_p)))) { + else if (dv.type == DeviceValueType::STRING) { json[name] = (char *)(dv.value_p); - has_value = true; } // handle ENUMs - else if ((dv.type == DeviceValueType::ENUM) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) { + else if (dv.type == DeviceValueType::ENUM) { if (*(uint8_t *)(dv.value_p) < dv.options_size) { // check for numeric enum-format, but "hamode" always as text if ((EMSESP::enum_format() == ENUM_FORMAT_NUMBER) && (dv.short_name != FL_(hamode)[0])) { @@ -997,19 +1009,18 @@ bool EMSdevice::generate_values_json(JsonObject & output, const uint8_t tag_filt } else { json[name] = dv.options[*(uint8_t *)(dv.value_p)]; } - has_value = true; } } // handle Integers and Floats + // If a divider is specified, do the division to 2 decimals places and send back as double/float + // otherwise force as a whole integer + // note: the strange nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler else { - // If a divider is specified, do the division to 2 decimals places and send back as double/float - // otherwise force as an integer whole - // the nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler uint8_t divider = 0; uint8_t factor = 1; if (dv.options_size == 1) { - const char * s = uuid::read_flash_string(dv.options[0]).c_str(); + const char * s = read_flash_string(dv.options[0]).c_str(); if (s[0] == '*') { factor = Helpers::atoint(&s[1]); } else { @@ -1017,85 +1028,109 @@ bool EMSdevice::generate_values_json(JsonObject & output, const uint8_t tag_filt } } - // always convert temperatures to floats + // always convert temperatures to floats with 1 decimal place bool make_float = (divider || (dv.uom == DeviceValueUOM::DEGREES)); - // INT - if ((dv.type == DeviceValueType::INT) && Helpers::hasValue(*(int8_t *)(dv.value_p))) { + if (dv.type == DeviceValueType::INT) { if (make_float) { json[name] = Helpers::round2(*(int8_t *)(dv.value_p), divider); } else { json[name] = *(int8_t *)(dv.value_p) * factor; } - has_value = true; - } else if ((dv.type == DeviceValueType::UINT) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::UINT) { if (make_float) { json[name] = Helpers::round2(*(uint8_t *)(dv.value_p), divider); } else { json[name] = *(uint8_t *)(dv.value_p) * factor; } - has_value = true; - } else if ((dv.type == DeviceValueType::SHORT) && Helpers::hasValue(*(int16_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::SHORT) { if (make_float) { json[name] = Helpers::round2(*(int16_t *)(dv.value_p), divider); } else { json[name] = *(int16_t *)(dv.value_p) * factor; } - has_value = true; - } else if ((dv.type == DeviceValueType::USHORT) && Helpers::hasValue(*(uint16_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::USHORT) { if (make_float) { json[name] = Helpers::round2(*(uint16_t *)(dv.value_p), divider); } else { json[name] = *(uint16_t *)(dv.value_p) * factor; } - has_value = true; - } else if ((dv.type == DeviceValueType::ULONG) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::ULONG) { if (make_float) { json[name] = Helpers::round2(*(uint32_t *)(dv.value_p), divider); } else { json[name] = *(uint32_t *)(dv.value_p) * factor; } - has_value = true; - } else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) { + } else if (dv.type == DeviceValueType::TIME) { uint32_t time_value = *(uint32_t *)(dv.value_p); time_value = (divider) ? time_value / divider : time_value * factor; // sometimes we need to divide by 60 if (output_target == EMSdevice::OUTPUT_TARGET::API_VERBOSE) { char time_s[40]; - snprintf(time_s, sizeof(time_s), "%d days %d hours %d minutes", (time_value / 1440), ((time_value % 1440) / 60), (time_value % 60)); + snprintf(time_s, + sizeof(time_s), + "%d %s %d %s %d %s", + (time_value / 1440), + read_flash_string(F_(days)).c_str(), + ((time_value % 1440) / 60), + read_flash_string(F_(hours)).c_str(), + (time_value % 60), + read_flash_string(F_(minutes)).c_str()); json[name] = time_s; } else { json[name] = time_value; } - has_value = true; } } } - dv.ha |= has_value ? DeviceValueHA::HA_VALUE : DeviceValueHA::HA_NONE; - has_values |= has_value; } return has_values; } // create the Home Assistant configs for each value -// this is called when an MQTT publish is done via an EMS Device -void EMSdevice::publish_mqtt_ha_sensor() { - for (auto & dv : devicevalues_) { - if (dv.ha == DeviceValueHA::HA_VALUE) { - Mqtt::publish_ha_sensor(dv.type, dv.tag, dv.full_name, dv.device_type, dv.short_name, dv.uom, dv.has_cmd); - dv.ha |= DeviceValueHA::HA_DONE; - } +// this is called when an MQTT publish is done via an EMS Device in emsesp.cpp +// if the main Device Entity config for the device hasn't been setup its also done here +void EMSdevice::publish_mqtt_ha_entity_config() { + // create the main device config if not doing already + if (!ha_config_done()) { + bool ok = publish_ha_device_config(); + ha_config_done(ok); // see if it worked } - if (!ha_config_done()) { - bool ok = publish_ha_config(); - ha_config_done(ok); // see if it worked + for (auto & dv : devicevalues_) { +#if defined(EMSESP_STANDALONE) + // debug messages to go with the test called 'dv' + if (strcmp(read_flash_string(dv.short_name).c_str(), "wwseltemp") == 0) { + EMSESP::logger().warning(F("! init: wwseltemp state=%d, active=%d config_created=%d"), + dv.get_state(), + dv.has_state(DV_ACTIVE), + dv.has_state(DV_HA_CONFIG_CREATED)); + } +#endif + + // if the HA config has already been created and now the value has gone dormant, delete the config + // https://github.com/emsesp/EMS-ESP32/issues/196 + if (dv.has_state(DV_ACTIVE)) { + if (!dv.has_state(DV_HA_CONFIG_CREATED)) { + // add it + Mqtt::publish_ha_sensor_config(dv.type, dv.tag, dv.full_name, dv.device_type, dv.short_name, dv.uom, false, dv.has_cmd); + dv.add_state(DV_HA_CONFIG_CREATED); + } + } else { + if (dv.has_state(DV_HA_CONFIG_CREATED)) { + // remove it + Mqtt::publish_ha_sensor_config(dv.type, dv.tag, dv.full_name, dv.device_type, dv.short_name, dv.uom, true, dv.has_cmd); + dv.remove_state(DV_HA_CONFIG_CREATED); + } + } } } +// remove all config topics in HA void EMSdevice::ha_config_clear() { for (auto & dv : devicevalues_) { - dv.ha = DeviceValueHA::HA_NONE; // also wait for new value + Mqtt::publish_ha_sensor_config(dv.type, dv.tag, dv.full_name, dv.device_type, dv.short_name, dv.uom, true, dv.has_cmd); // delete topic + dv.remove_state(DV_HA_CONFIG_CREATED); } ha_config_done(false); } @@ -1111,7 +1146,7 @@ const std::string EMSdevice::telegram_type_name(std::shared_ptr for (const auto & tf : telegram_functions_) { if ((tf.telegram_type_id_ == telegram->type_id) && (telegram->type_id != 0xFF)) { - return uuid::read_flash_string(tf.telegram_type_name_); + return read_flash_string(tf.telegram_type_name_); } } @@ -1126,7 +1161,7 @@ bool EMSdevice::handle_telegram(std::shared_ptr telegram) { // if the data block is empty, assume that this telegram is not recognized by the bus master // so remove it from the automatic fetch list if (telegram->message_length == 0 && telegram->offset == 0) { - EMSESP::logger().debug(F("This telegram (%s) is not recognized by the EMS bus"), uuid::read_flash_string(tf.telegram_type_name_).c_str()); + EMSESP::logger().debug(F("This telegram (%s) is not recognized by the EMS bus"), read_flash_string(tf.telegram_type_name_).c_str()); toggle_fetch(tf.telegram_type_id_, false); return false; } @@ -1161,4 +1196,51 @@ void EMSdevice::read_command(const uint16_t type_id, const uint8_t offset, const EMSESP::send_read_request(type_id, device_id(), offset, length); } +// checks whether the device value has an actual value +// returns true if its valid +// state is stored in the dv object +bool EMSdevice::check_dv_hasvalue(const DeviceValue & dv) { + bool has_value = false; + switch (dv.type) { + case DeviceValueType::BOOL: + has_value = Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL); + break; + case DeviceValueType::STRING: + has_value = Helpers::hasValue((char *)(dv.value_p)); + break; + case DeviceValueType::ENUM: + has_value = Helpers::hasValue(*(uint8_t *)(dv.value_p)); + break; + case DeviceValueType::INT: + has_value = Helpers::hasValue(*(int8_t *)(dv.value_p)); + break; + case DeviceValueType::UINT: + has_value = Helpers::hasValue(*(uint8_t *)(dv.value_p)); + break; + case DeviceValueType::SHORT: + has_value = Helpers::hasValue(*(int16_t *)(dv.value_p)); + break; + case DeviceValueType::USHORT: + has_value = Helpers::hasValue(*(uint16_t *)(dv.value_p)); + break; + case DeviceValueType::ULONG: + has_value = Helpers::hasValue(*(uint32_t *)(dv.value_p)); + break; + case DeviceValueType::TIME: + has_value = Helpers::hasValue(*(uint32_t *)(dv.value_p)); + break; + default: + break; + } + +#if defined(EMSESP_DEBUG) + // https://github.com/emsesp/EMS-ESP32/issues/196 + if (dv.has_state(DeviceValueState::DV_ACTIVE) && !has_value) { + EMSESP::logger().warning(F("[DEBUG] Lost device value %s"), dv.short_name); + } +#endif + + return has_value; +} + } // namespace emsesp diff --git a/src/emsdevice.h b/src/emsdevice.h index ce31cf306..cea919fb3 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -119,12 +119,14 @@ enum DeviceValueTAG : uint8_t { }; -// MQTT HA flags -enum DeviceValueHA : uint8_t { +// states of a device value +enum DeviceValueState : uint8_t { + + DV_DEFAULT = 0, // 0 - does not yet have a value + DV_ACTIVE = (1 << 0), // 1 - has a value + DV_VISIBLE = (1 << 1), // 2 - shown on web and console + DV_HA_CONFIG_CREATED = (1 << 2) // 4 - set if the HA config has been created - HA_NONE = 0, - HA_VALUE, - HA_DONE }; class EMSdevice { @@ -134,7 +136,7 @@ class EMSdevice { static constexpr uint8_t EMS_DEVICES_MAX_TELEGRAMS = 20; // virtual functions overrules by derived classes - virtual bool publish_ha_config() = 0; + virtual bool publish_ha_device_config() = 0; // device_type defines which derived class to use, e.g. BOILER, THERMOSTAT etc.. EMSdevice(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) @@ -291,7 +293,7 @@ class EMSdevice { void read_command(const uint16_t type_id, uint8_t offset = 0, uint8_t length = 0); - void publish_mqtt_ha_sensor(); + void publish_mqtt_ha_entity_config(); const std::string telegram_type_name(std::shared_ptr telegram); @@ -415,7 +417,6 @@ class EMSdevice { } }; - // DeviceValue holds all the attributes for a device value (also a device parameter) struct DeviceValue { uint8_t device_type; // EMSdevice::DeviceType @@ -431,6 +432,7 @@ class EMSdevice { bool has_cmd; // true if there is a Console/MQTT command which matches the short_name int32_t min; uint32_t max; + uint8_t state; // DeviceValueState::* DeviceValue(uint8_t device_type, uint8_t tag, @@ -444,7 +446,8 @@ class EMSdevice { uint8_t ha, bool has_cmd, int32_t min, - uint32_t max) + uint32_t max, + uint8_t state) : device_type(device_type) , tag(tag) , value_p(value_p) @@ -457,7 +460,22 @@ class EMSdevice { , ha(ha) , has_cmd(has_cmd) , min(min) - , max(max) { + , max(max) + , state(state) { + } + + // state flags + inline void add_state(uint8_t s) { + state |= s; + } + inline bool has_state(uint8_t s) const { + return (state & s) == s; + } + inline void remove_state(uint8_t s) { + state &= ~s; + } + inline uint8_t get_state() const { + return state; } }; const std::vector devicevalues() const; @@ -467,13 +485,11 @@ class EMSdevice { const std::string device_entity_ha(DeviceValue const & dv); + bool check_dv_hasvalue(const DeviceValue & dv); + void init_devicevalues(uint8_t size) { devicevalues_.reserve(size); } - - inline bool dv_is_visible(DeviceValue dv) { - return (dv.full_name); - } }; } // namespace emsesp diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 527e45b1e..a15e76fe1 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -458,7 +458,7 @@ void EMSESP::reset_mqtt_ha() { } // create json doc for the devices values and add to MQTT publish queue -// generate_values_json is called without verbose mode (defaults to false) +// generate_values_json is called to build the device value (dv) object array void EMSESP::publish_device_values(uint8_t device_type) { DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); // use max size JsonObject json = doc.to(); @@ -469,11 +469,6 @@ void EMSESP::publish_device_values(uint8_t device_type) { // group by device type for (const auto & emsdevice : emsdevices) { if (emsdevice && (emsdevice->device_type() == device_type)) { - // if we're using HA, done is checked for each sensor in devices - if (Mqtt::ha_enabled()) { - emsdevice->publish_mqtt_ha_sensor(); // create the configs for each value as a sensor - } - // if its a boiler, generate json for each group and publish it directly. not nested if (device_type == DeviceType::BOILER) { if (emsdevice->generate_values_json(json, DeviceValueTAG::TAG_BOILER_DATA, false, EMSdevice::OUTPUT_TARGET::MQTT)) { @@ -528,6 +523,11 @@ void EMSESP::publish_device_values(uint8_t device_type) { need_publish |= emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true, EMSdevice::OUTPUT_TARGET::MQTT); // nested } } + + // if we're using HA, done is checked for each sensor in devices + if (Mqtt::ha_enabled()) { + emsdevice->publish_mqtt_ha_entity_config(); // create the configs for each value as a sensor + } } // publish it under a single topic, only if we have data to publish @@ -949,7 +949,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std:: // find the name and flags in our database for (const auto & device : device_library_) { if (device.product_id == product_id && device.device_type == emsdevice->device_type()) { - emsdevice->name(std::move(uuid::read_flash_string(device.name))); + emsdevice->name(std::move(read_flash_string(device.name))); emsdevice->add_flags(device.flags); } } @@ -988,7 +988,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std:: return false; // not found } - auto name = uuid::read_flash_string(device_p->name); + auto name = read_flash_string(device_p->name); auto device_type = device_p->device_type; auto flags = device_p->flags; diff --git a/src/helpers.cpp b/src/helpers.cpp index 55a209810..d1edb9303 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -126,9 +126,9 @@ char * Helpers::smallitoa(char * result, const uint16_t value) { char * Helpers::render_boolean(char * result, bool value) { uint8_t bool_format_ = EMSESP::bool_format(); if (bool_format_ == BOOL_FORMAT_ONOFF) { - strlcpy(result, value ? uuid::read_flash_string(F_(on)).c_str() : uuid::read_flash_string(F_(off)).c_str(), 5); + strlcpy(result, value ? read_flash_string(F_(on)).c_str() : read_flash_string(F_(off)).c_str(), 5); } else if (bool_format_ == BOOL_FORMAT_ONOFF_CAP) { - strlcpy(result, value ? uuid::read_flash_string(F_(ON)).c_str() : uuid::read_flash_string(F_(OFF)).c_str(), 5); + strlcpy(result, value ? read_flash_string(F_(ON)).c_str() : read_flash_string(F_(OFF)).c_str(), 5); } else if (bool_format_ == BOOL_FORMAT_TRUEFALSE) { strlcpy(result, value ? "true" : "false", 7); } else { @@ -306,7 +306,7 @@ char * Helpers::render_value(char * result, const uint32_t value, const uint8_t // creates string of hex values from an arrray of bytes std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) { if (length == 0) { - return uuid::read_flash_string(F("")); + return read_flash_string(F("")); } std::string str(160, '\0'); @@ -464,12 +464,12 @@ bool Helpers::value2bool(const char * v, bool & value) { std::string bool_str = toLower(v); // convert to lower case - if ((bool_str == uuid::read_flash_string(F_(on))) || (bool_str == "1") or (bool_str == "true")) { + if ((bool_str == read_flash_string(F_(on))) || (bool_str == "1") or (bool_str == "true")) { value = true; return true; // is a bool } - if ((bool_str == uuid::read_flash_string(F_(off))) || (bool_str == "0") or (bool_str == "false")) { + if ((bool_str == read_flash_string(F_(off))) || (bool_str == "0") or (bool_str == "false")) { value = false; return true; // is a bool } @@ -484,8 +484,8 @@ bool Helpers::value2enum(const char * v, uint8_t & value, const __FlashStringHel } std::string str = toLower(v); for (value = 0; strs[value]; value++) { - std::string str1 = toLower(uuid::read_flash_string(strs[value])); - if ((str1 == uuid::read_flash_string(F_(off)) && str == "false") || (str1 == uuid::read_flash_string(F_(on)) && str == "true") || (str == str1) + std::string str1 = toLower(read_flash_string(strs[value])); + if ((str1 == read_flash_string(F_(off)) && str == "false") || (str1 == read_flash_string(F_(on)) && str == "true") || (str == str1) || (v[0] == ('0' + value) && v[1] == '\0')) { return true; } diff --git a/src/locale_EN.h b/src/locale_EN.h index b392a8416..f36878d5a 100644 --- a/src/locale_EN.h +++ b/src/locale_EN.h @@ -187,6 +187,8 @@ MAKE_PSTR(mv, "mV") MAKE_PSTR(times, "times") MAKE_PSTR(oclock, "o'clock") +MAKE_PSTR(days, "days") + // TAG mapping - maps to DeviceValueTAG_s in emsdevice.cpp // use empty string if want to suppress showing tags // tags must not have spaces diff --git a/src/mqtt.cpp b/src/mqtt.cpp index bdf461cf7..4bb54fc54 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -158,7 +158,7 @@ void Mqtt::loop() { // print MQTT log and other stuff to console void Mqtt::show_mqtt(uuid::console::Shell & shell) { - shell.printfln(F("MQTT is %s"), connected() ? uuid::read_flash_string(F_(connected)).c_str() : uuid::read_flash_string(F_(disconnected)).c_str()); + shell.printfln(F("MQTT is %s"), connected() ? read_flash_string(F_(connected)).c_str() : read_flash_string(F_(disconnected)).c_str()); shell.printfln(F("MQTT publish errors: %lu"), mqtt_publish_fails_); shell.println(); @@ -583,24 +583,39 @@ void Mqtt::ha_status() { // create the sensors - must match the MQTT payload keys if (!EMSESP::system_.ethernet_connected()) { - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("WiFi RSSI"), EMSdevice::DeviceType::SYSTEM, F("rssi"), DeviceValueUOM::DBM); - publish_ha_sensor(DeviceValueType::INT, - DeviceValueTAG::TAG_HEARTBEAT, - F("WiFi strength"), - EMSdevice::DeviceType::SYSTEM, - F("wifistrength"), - DeviceValueUOM::PERCENT); + publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("WiFi RSSI"), EMSdevice::DeviceType::SYSTEM, F("rssi"), DeviceValueUOM::DBM); + publish_ha_sensor_config(DeviceValueType::INT, + DeviceValueTAG::TAG_HEARTBEAT, + F("WiFi strength"), + EMSdevice::DeviceType::SYSTEM, + F("wifistrength"), + DeviceValueUOM::PERCENT); } - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Uptime"), EMSdevice::DeviceType::SYSTEM, F("uptime"), DeviceValueUOM::NONE); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Uptime (sec)"), EMSdevice::DeviceType::SYSTEM, F("uptime_sec"), DeviceValueUOM::SECONDS); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Free memory"), EMSdevice::DeviceType::SYSTEM, F("freemem"), DeviceValueUOM::KB); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("MQTT fails"), EMSdevice::DeviceType::SYSTEM, F("mqttfails"), DeviceValueUOM::TIMES); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Rx received"), EMSdevice::DeviceType::SYSTEM, F("rxreceived"), DeviceValueUOM::TIMES); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Rx fails"), EMSdevice::DeviceType::SYSTEM, F("rxfails"), DeviceValueUOM::TIMES); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx reads"), EMSdevice::DeviceType::SYSTEM, F("txreads"), DeviceValueUOM::TIMES); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx writes"), EMSdevice::DeviceType::SYSTEM, F("txwrites"), DeviceValueUOM::TIMES); - publish_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx fails"), EMSdevice::DeviceType::SYSTEM, F("txfails"), DeviceValueUOM::TIMES); + publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Uptime"), EMSdevice::DeviceType::SYSTEM, F("uptime"), DeviceValueUOM::NONE); + publish_ha_sensor_config(DeviceValueType::INT, + DeviceValueTAG::TAG_HEARTBEAT, + F("Uptime (sec)"), + EMSdevice::DeviceType::SYSTEM, + F("uptime_sec"), + DeviceValueUOM::SECONDS); + publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Free memory"), EMSdevice::DeviceType::SYSTEM, F("freemem"), DeviceValueUOM::KB); + publish_ha_sensor_config(DeviceValueType::INT, + DeviceValueTAG::TAG_HEARTBEAT, + F("MQTT fails"), + EMSdevice::DeviceType::SYSTEM, + F("mqttfails"), + DeviceValueUOM::TIMES); + publish_ha_sensor_config(DeviceValueType::INT, + DeviceValueTAG::TAG_HEARTBEAT, + F("Rx received"), + EMSdevice::DeviceType::SYSTEM, + F("rxreceived"), + DeviceValueUOM::TIMES); + publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Rx fails"), EMSdevice::DeviceType::SYSTEM, F("rxfails"), DeviceValueUOM::TIMES); + publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx reads"), EMSdevice::DeviceType::SYSTEM, F("txreads"), DeviceValueUOM::TIMES); + publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx writes"), EMSdevice::DeviceType::SYSTEM, F("txwrites"), DeviceValueUOM::TIMES); + publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx fails"), EMSdevice::DeviceType::SYSTEM, F("txfails"), DeviceValueUOM::TIMES); } // add sub or pub task to the queue. @@ -612,9 +627,14 @@ std::shared_ptr Mqtt::queue_message(const uint8_t operation, } // if it's a publish and the payload is empty, stop + /* if ((operation == Operation::PUBLISH) && (payload.empty())) { +#ifdef EMSESP_DEBUG + LOG_WARNING("[DEBUG] Publish empty payload - quitting"); +#endif return nullptr; } + */ // take the topic and prefix the base, unless its for HA std::shared_ptr message; @@ -622,7 +642,11 @@ std::shared_ptr Mqtt::queue_message(const uint8_t operation, #ifdef EMSESP_DEBUG if (operation == Operation::PUBLISH) { - LOG_INFO("[DEBUG] Adding to queue: (Publish) topic='%s' payload=%s", message->topic.c_str(), message->payload.c_str()); + if (message->payload.empty()) { + LOG_INFO("[DEBUG] Adding to queue: (Publish) topic='%s' empty payload", message->topic.c_str()); + } else { + LOG_INFO("[DEBUG] Adding to queue: (Publish) topic='%s' payload=%s", message->topic.c_str(), message->payload.c_str()); + } } else { LOG_INFO("[DEBUG] Adding to queue: (Subscribe) topic='%s'", message->topic.c_str()); } @@ -657,16 +681,16 @@ void Mqtt::publish(const std::string & topic, const std::string & payload) { // MQTT Publish, using a user's retain flag - except for char * strings void Mqtt::publish(const __FlashStringHelper * topic, const char * payload) { - queue_publish_message(uuid::read_flash_string(topic), payload, mqtt_retain_); + queue_publish_message(read_flash_string(topic), payload, mqtt_retain_); } // MQTT Publish, using a specific retain flag, topic is a flash string void Mqtt::publish(const __FlashStringHelper * topic, const std::string & payload) { - queue_publish_message(uuid::read_flash_string(topic), payload, mqtt_retain_); + queue_publish_message(read_flash_string(topic), payload, mqtt_retain_); } void Mqtt::publish(const __FlashStringHelper * topic, const JsonObject & payload) { - publish(uuid::read_flash_string(topic), payload); + publish(read_flash_string(topic), payload); } // publish json doc, only if its not empty @@ -681,7 +705,7 @@ void Mqtt::publish(const std::string & topic) { // MQTT Publish, using a specific retain flag, topic is a flash string, forcing retain flag void Mqtt::publish_retain(const __FlashStringHelper * topic, const std::string & payload, bool retain) { - queue_publish_message(uuid::read_flash_string(topic), payload, retain); + queue_publish_message(read_flash_string(topic), payload, retain); } // publish json doc, only if its not empty, using the retain flag @@ -694,11 +718,25 @@ void Mqtt::publish_retain(const std::string & topic, const JsonObject & payload, } void Mqtt::publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain) { - publish_retain(uuid::read_flash_string(topic), payload, retain); + publish_retain(read_flash_string(topic), payload, retain); } void Mqtt::publish_ha(const __FlashStringHelper * topic, const JsonObject & payload) { - publish_ha(uuid::read_flash_string(topic), payload); + publish_ha(read_flash_string(topic), payload); +} + +// publish empty payload to remove the topic +void Mqtt::publish_ha(const std::string & topic) { + if (!enabled()) { + return; + } + + std::string fulltopic = read_flash_string(F_(homeassistant)) + topic; +#if defined(EMSESP_DEBUG) + LOG_DEBUG(F("[DEBUG] Publishing empty HA topic=%s"), fulltopic.c_str()); +#endif + + publish(topic); // call it immediately, don't queue it } // publish a Home Assistant config topic and payload, with retain flag off. @@ -714,7 +752,7 @@ void Mqtt::publish_ha(const std::string & topic, const JsonObject & payload) { payload_text.reserve(measureJson(payload) + 1); serializeJson(payload, payload_text); // convert json to string - std::string fulltopic = uuid::read_flash_string(F_(homeassistant)) + topic; + std::string fulltopic = read_flash_string(F_(homeassistant)) + topic; #if defined(EMSESP_STANDALONE) LOG_DEBUG(F("Publishing HA topic=%s, payload=%s"), fulltopic.c_str(), payload_text.c_str()); #elif defined(EMSESP_DEBUG) @@ -737,7 +775,7 @@ void Mqtt::process_queue() { auto message = mqtt_message.content_; char topic[MQTT_TOPIC_MAX_SIZE]; - if (message->topic.find(uuid::read_flash_string(F_(homeassistant))) == 0) { + if (message->topic.find(read_flash_string(F_(homeassistant))) == 0) { strcpy(topic, message->topic.c_str()); // leave topic as it is } else { snprintf(topic, MQTT_TOPIC_MAX_SIZE, "%s/%s", mqtt_base_.c_str(), message->topic.c_str()); @@ -803,46 +841,72 @@ void Mqtt::process_queue() { mqtt_messages_.pop_front(); // remove the message from the queue } +void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdevice::DeviceValueType + uint8_t tag, // EMSdevice::DeviceValueTAG + const __FlashStringHelper * name, + const uint8_t device_type, // EMSdevice::DeviceType + const __FlashStringHelper * entity, + const uint8_t uom) { // EMSdevice::DeviceValueUOM (0=NONE) + publish_ha_sensor_config(type, tag, name, device_type, entity, uom, false, false); +} + + // HA config for a sensor and binary_sensor entity // entity must match the key/value pair in the *_data topic // note: some string copying here into chars, it looks messy but does help with heap fragmentation issues -void Mqtt::publish_ha_sensor(uint8_t type, // EMSdevice::DeviceValueType - uint8_t tag, // EMSdevice::DeviceValueTAG - const __FlashStringHelper * name, - const uint8_t device_type, // EMSdevice::DeviceType - const __FlashStringHelper * entity, - const uint8_t uom, // EMSdevice::DeviceValueUOM (0=NONE) - const bool has_cmd) { +void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdevice::DeviceValueType + uint8_t tag, // EMSdevice::DeviceValueTAG + const __FlashStringHelper * name, + const uint8_t device_type, // EMSdevice::DeviceType + const __FlashStringHelper * entity, + const uint8_t uom, // EMSdevice::DeviceValueUOM (0=NONE) + const bool remove, // true if we want to remove this topic + const bool has_cmd) { // ignore if name (fullname) is empty if (name == nullptr) { return; } + // create the device name + char device_name[50]; + strlcpy(device_name, EMSdevice::device_type_2_device_name(device_type).c_str(), sizeof(device_name)); + + // create entity by add the hc/wwc tag if present, seperating with a . + char new_entity[50]; + if (tag >= DeviceValueTAG::TAG_HC1) { + snprintf(new_entity, sizeof(new_entity), "%s.%s", EMSdevice::tag_to_string(tag).c_str(), read_flash_string(entity).c_str()); + } else { + snprintf(new_entity, sizeof(new_entity), "%s", read_flash_string(entity).c_str()); + } + + // build unique identifier which will be used in the topic, replacing all . with _ as not to break HA + std::string uniq(50, '\0'); + snprintf(&uniq[0], uniq.capacity() + 1, "%s_%s", device_name, new_entity); + std::replace(uniq.begin(), uniq.end(), '.', '_'); + + // create the topic + char topic[MQTT_TOPIC_MAX_SIZE]; + if (type == DeviceValueType::BOOL) { + snprintf(topic, sizeof(topic), "binary_sensor/%s/%s/config", mqtt_base_.c_str(), uniq.c_str()); // binary sensor + } else { + snprintf(topic, sizeof(topic), "sensor/%s/%s/config", mqtt_base_.c_str(), uniq.c_str()); // normal HA sensor, not a boolean one + } + + // if we're asking to remove this topic, send an empty payload + // https://github.com/emsesp/EMS-ESP32/issues/196 + if (remove) { + LOG_WARNING(F("Device value %s gone silent. Removing HA config topic %s"), uniq.c_str(), topic); + publish_ha(topic); + return; + } + bool have_tag = !EMSdevice::tag_to_string(tag).empty(); // nested_format is 1 if nested, otherwise 2 for single topics bool is_nested = (nested_format_ == 1); - char device_name[50]; - strlcpy(device_name, EMSdevice::device_type_2_device_name(device_type).c_str(), sizeof(device_name)); - DynamicJsonDocument doc(EMSESP_JSON_SIZE_HA_CONFIG); - - doc["~"] = mqtt_base_; - - // create entity by add the hc/wwc tag if present, seperating with a . - char new_entity[50]; - if (tag >= DeviceValueTAG::TAG_HC1) { - snprintf(new_entity, sizeof(new_entity), "%s.%s", EMSdevice::tag_to_string(tag).c_str(), uuid::read_flash_string(entity).c_str()); - } else { - snprintf(new_entity, sizeof(new_entity), "%s", uuid::read_flash_string(entity).c_str()); - } - - // build unique identifier which will be used in the topic - // and replacing all . with _ as not to break HA - std::string uniq(50, '\0'); - snprintf(&uniq[0], uniq.capacity() + 1, "%s_%s", device_name, new_entity); - std::replace(uniq.begin(), uniq.end(), '.', '_'); + doc["~"] = mqtt_base_; doc["uniq_id"] = uniq; // state topic @@ -853,9 +917,9 @@ void Mqtt::publish_ha_sensor(uint8_t type, // EMSdevice::Dev // name = char new_name[80]; if (have_tag) { - snprintf(new_name, sizeof(new_name), "%s %s %s", device_name, EMSdevice::tag_to_string(tag).c_str(), uuid::read_flash_string(name).c_str()); + snprintf(new_name, sizeof(new_name), "%s %s %s", device_name, EMSdevice::tag_to_string(tag).c_str(), read_flash_string(name).c_str()); } else { - snprintf(new_name, sizeof(new_name), "%s %s", device_name, uuid::read_flash_string(name).c_str()); + snprintf(new_name, sizeof(new_name), "%s %s", device_name, read_flash_string(name).c_str()); } new_name[0] = toupper(new_name[0]); // capitalize first letter doc["name"] = new_name; @@ -866,25 +930,17 @@ void Mqtt::publish_ha_sensor(uint8_t type, // EMSdevice::Dev if (is_nested) { snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", new_entity); } else { - snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", uuid::read_flash_string(entity).c_str()); + snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", read_flash_string(entity).c_str()); } doc["val_tpl"] = val_tpl; - char topic[MQTT_TOPIC_MAX_SIZE]; - // look at the device value type if (type == DeviceValueType::BOOL) { - // binary sensor - snprintf(topic, sizeof(topic), "binary_sensor/%s/%s/config", mqtt_base_.c_str(), uniq.c_str()); // topic - // how to render boolean. HA only accepts String values char result[10]; doc[F("payload_on")] = Helpers::render_boolean(result, true); doc[F("payload_off")] = Helpers::render_boolean(result, false); } else { - // normal HA sensor, not a boolean one - snprintf(topic, sizeof(topic), "sensor/%s/%s/config", mqtt_base_.c_str(), uniq.c_str()); // topic - // set default state and device class for HA auto set_state_class = State_class::NONE; auto set_device_class = Device_class::NONE; diff --git a/src/mqtt.h b/src/mqtt.h index c198f54ff..0257fabe5 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -110,14 +110,23 @@ class Mqtt { static void publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain); static void publish_ha(const std::string & topic, const JsonObject & payload); static void publish_ha(const __FlashStringHelper * topic, const JsonObject & payload); + static void publish_ha(const std::string & topic); - static void publish_ha_sensor(uint8_t type, - uint8_t tag, - const __FlashStringHelper * name, - const uint8_t device_type, - const __FlashStringHelper * entity, - const uint8_t uom, - const bool has_cmd = false); + static void publish_ha_sensor_config(uint8_t type, + uint8_t tag, + const __FlashStringHelper * name, + const uint8_t device_type, + const __FlashStringHelper * entity, + const uint8_t uom, + const bool remove, + const bool has_cmd); + + static void publish_ha_sensor_config(uint8_t type, + uint8_t tag, + const __FlashStringHelper * name, + const uint8_t device_type, + const __FlashStringHelper * entity, + const uint8_t uom); static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type); static void show_mqtt(uuid::console::Shell & shell); diff --git a/src/system.cpp b/src/system.cpp index 130d22621..cf1ee79a7 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -830,7 +830,7 @@ void System::show_system(uuid::console::Shell & shell) { } else { shell.printfln(F("Syslog: %s"), syslog_.started() ? "started" : "stopped"); shell.print(F(" ")); - shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : uuid::read_flash_string(F_(unset)).c_str()); + shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : read_flash_string(F_(unset)).c_str()); shell.printfln(F(" IP: %s"), uuid::printable_to_string(syslog_.ip()).c_str()); shell.print(F(" ")); shell.printfln(F_(port_fmt), syslog_port_); diff --git a/src/test/test.cpp b/src/test/test.cpp index fda1ada31..9ee938159 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -471,10 +471,38 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { shell.invoke_command("call boiler entities"); } + if (command == "dv") { + shell.printfln(F("Testing device value rendering")); + + Mqtt::ha_enabled(true); + // Mqtt::ha_enabled(false); + + Mqtt::nested_format(1); + Mqtt::send_response(false); + + run_test("boiler"); + run_test("thermostat"); + + // shell.invoke_command("show"); + + // change a value to null/bogus/dormant + // homeassistant/sensor/ems-esp/boiler_wwseltemp/config + shell.invoke_command("call boiler wwseltemp"); + shell.invoke_command("call system publish"); + + // Boiler -> Me, UBAParameterWW(0x33) + // wwseltemp = goes from 52 degrees (0x34) to void (0xFF) + uart_telegram({0x08, 0x0B, 0x33, 0x00, 0x08, 0xFF, 0xFF, 0xFB, 0x00, 0x28, 0x00, 0x00, 0x46, 0x00, 0xFF, 0xFF, 0x00}); + + shell.invoke_command("call boiler wwseltemp"); + shell.invoke_command("call system publish"); + + // shell.invoke_command("show mqtt"); + } + if (command == "api") { shell.printfln(F("Testing API with MQTT and REST, standalone")); - Mqtt::ha_enabled(true); // Mqtt::ha_enabled(false); diff --git a/src/test/test.h b/src/test/test.h index d9ca69ca7..ba62d7215 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -39,8 +39,10 @@ namespace emsesp { // #define EMSESP_DEBUG_DEFAULT "shower_alert" // #define EMSESP_DEBUG_DEFAULT "310" // #define EMSESP_DEBUG_DEFAULT "render" -#define EMSESP_DEBUG_DEFAULT "api" +// #define EMSESP_DEBUG_DEFAULT "api" // #define EMSESP_DEBUG_DEFAULT "crash" +#define EMSESP_DEBUG_DEFAULT "dv" + class Test { public: diff --git a/src/version.h b/src/version.h index 05670ef95..46c699777 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.3.0b6" +#define EMSESP_APP_VERSION "3.3.0b7"