Merge pull request #197 from proddy/dev

fixes #196 (HA missing entities), add row click to devices, change max limit on integer types to avoid NaN, refactored generation of device value JSON objects
This commit is contained in:
Proddy
2021-11-15 17:45:52 +01:00
committed by GitHub
36 changed files with 495 additions and 284 deletions

View File

@@ -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**

View File

@@ -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<
</TableHead>
<TableBody>
{data.sensors.map((sensorData) => (
<TableRow key={sensorData.n} hover>
<TableRow
key={sensorData.n}
hover
onClick={() => this.sendSensor(sensorData)}
>
<TableCell padding="checkbox" style={{ width: 18 }}>
{me.admin && (
<CustomTooltip title="edit" placement="left-end">
@@ -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<
<TableHead></TableHead>
<TableBody>
{deviceData.data.map((item, i) => (
<TableRow hover key={i}>
<TableRow
hover
key={i}
onClick={() => this.sendCommand(item)}
>
<TableCell padding="checkbox" style={{ width: 18 }}>
{item.c && me.admin && (
{item.c && this.props.authenticatedContext.me.admin && (
<CustomTooltip
title="change value"
placement="left-end"

View File

@@ -15,7 +15,7 @@ import {
} from '@material-ui/core';
import { FormButton } from '../components';
import { DeviceValue, DeviceValueUOM_s } from './EMSESPtypes';
import { DeviceValue, DeviceValueUOM, DeviceValueUOM_s } from './EMSESPtypes';
interface ValueFormProps {
devicevalue: DeviceValue;
@@ -26,6 +26,16 @@ interface ValueFormProps {
) => (event: React.ChangeEvent<HTMLInputElement>) => 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<ValueFormProps> {
formRef: RefObject<any> = React.createRef();
@@ -69,7 +79,7 @@ class ValueForm extends React.Component<ValueFormProps> {
{!devicevalue.l && (
<OutlinedInput
id="value"
value={devicevalue.v}
value={formatValue(devicevalue.v, devicevalue.u)}
autoFocus
fullWidth
onChange={handleValueChange('v')}

View File

@@ -296,7 +296,7 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char *
// add a command to the list, which does not return json
void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, const cmd_function_p cb, const __FlashStringHelper * description, uint8_t flags) {
// if the command already exists for that device type don't add it
if (find_command(device_type, uuid::read_flash_string(cmd).c_str()) != nullptr) {
if (find_command(device_type, read_flash_string(cmd).c_str()) != nullptr) {
return;
}
@@ -312,7 +312,7 @@ void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, co
// flag is fixed to MqttSubFlag::MQTT_SUB_FLAG_NOSUB so there will be no topic subscribed to this
void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, const cmd_json_function_p cb, const __FlashStringHelper * description, uint8_t flags) {
// if the command already exists for that device type don't add it
if (find_command(device_type, uuid::read_flash_string(cmd).c_str()) != nullptr) {
if (find_command(device_type, read_flash_string(cmd).c_str()) != nullptr) {
return;
}
@@ -334,7 +334,7 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, const ch
}
for (auto & cf : cmdfunctions_) {
if (!strcmp(lowerCmd, Helpers::toLower(uuid::read_flash_string(cf.cmd_)).c_str()) && (cf.device_type_ == device_type)) {
if (!strcmp(lowerCmd, Helpers::toLower(read_flash_string(cf.cmd_)).c_str()) && (cf.device_type_ == device_type)) {
return &cf;
}
}
@@ -353,14 +353,14 @@ bool Command::list(const uint8_t device_type, JsonObject & output) {
std::list<std::string> 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<std::string> 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);

View File

@@ -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) {

View File

@@ -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<EMSESP_JSON_SIZE_HA_CONFIG> 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;

View File

@@ -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_;

View File

@@ -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;
}

View File

@@ -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_;

View File

@@ -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;
}

View File

@@ -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_;

View File

@@ -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;
}

View File

@@ -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_;

View File

@@ -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;
}

View File

@@ -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_;

View File

@@ -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<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(heatpump);
doc["ic"] = F_(icondevice);

View File

@@ -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_;

View File

@@ -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;

View File

@@ -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_;

View File

@@ -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<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(solar);
doc["ic"] = F_(icondevice);

View File

@@ -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_;

View File

@@ -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;

View File

@@ -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_;

View File

@@ -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<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(thermostat);
doc["ic"] = F_(icondevice);
@@ -384,7 +384,7 @@ std::shared_ptr<Thermostat::HeatingCircuit> 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<Thermostat::HeatingCircuit> hc) {
uint8_t hc_num = hc->hc_num();
uint8_t hc_num = hc->hc_num();
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
char str1[20];
@@ -437,7 +437,7 @@ void Thermostat::publish_ha_config_hc(std::shared_ptr<Thermostat::HeatingCircuit
// the HA climate component only responds to auto, heat and off
JsonArray modes = doc.createNestedArray("modes");
if (hc->get_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;

View File

@@ -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_;

View File

@@ -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::DeviceValue> 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<const Telegram>
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<const Telegram> 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

View File

@@ -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<const Telegram> 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<DeviceValue> 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

View File

@@ -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<JsonObject>();
@@ -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;

View File

@@ -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("<empty>"));
return read_flash_string(F("<empty>"));
}
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;
}

View File

@@ -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

View File

@@ -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<const MqttMessage> 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<MqttMessage> message;
@@ -622,7 +642,11 @@ std::shared_ptr<const MqttMessage> 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 = <device> <tag> <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;

View File

@@ -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);

View File

@@ -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_);

View File

@@ -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);

View File

@@ -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:

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.3.0b6"
#define EMSESP_APP_VERSION "3.3.0b7"