diff --git a/src/HaHelper.h b/src/HaHelper.h index 689d3ec..4b9c603 100644 --- a/src/HaHelper.h +++ b/src/HaHelper.h @@ -418,25 +418,26 @@ public: bool publishSwitchHeatingTurbo(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating_turbo")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); - doc[FPSTR(HA_NAME)] = F("Turbo heating"); - doc[FPSTR(HA_ICON)] = F("mdi:rocket-launch-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); - doc[FPSTR(HA_STATE_ON)] = true; - doc[FPSTR(HA_STATE_OFF)] = false; - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.turbo }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); - doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"heating\": {\"turbo\" : true}}"); - doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"heating\": {\"turbo\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); + return publishSwitch( + F("heating_turbo"), + F("Turbo heating"), + F("mdi:rocket-launch-outline"), + F("{{ value_json.heating.turbo }}"), + F("{\"heating\": {\"turbo\" : true}}"), + F("{\"heating\": {\"turbo\" : false}}"), + enabledByDefault + ); + } - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("heating_turbo")).c_str(), doc); + bool publishSwitchExtDevice(const String& caption, bool enabledByDefault = true) { + return publishSwitch( + F("extdev"), + caption, + F("mdi:toggle-switch-outline"), + F("{{ value_json.externalDev.state }}"), + F("{\"externalDev\": {\"state\" : true}}"), + F("{\"externalDev\": {\"state\" : false}}"), + enabledByDefault ); } bool publishInputHeatingHysteresis(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { @@ -634,26 +635,18 @@ public: bool publishSwitchPid(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); - doc[FPSTR(HA_NAME)] = F("PID"); - doc[FPSTR(HA_ICON)] = F("mdi:chart-bar-stacked"); - doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); - doc[FPSTR(HA_STATE_ON)] = true; - doc[FPSTR(HA_STATE_OFF)] = false; - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.enabled }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); - doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"pid\": {\"enabled\" : true}}"); - doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"pid\": {\"enabled\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("pid")).c_str(), doc); + return publishSwitch( + F("pid"), + F("PID"), + F("mdi:chart-bar-stacked"), + F("{{ value_json.pid.enabled }}"), + F("{\"pid\": {\"enabled\" : true}}"), + F("{\"pid\": {\"enabled\" : false}}"), + enabledByDefault + ); } + bool publishInputPidFactorP(bool enabledByDefault = true) { JsonDocument doc; @@ -819,25 +812,16 @@ public: bool publishSwitchEquitherm(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); - doc[FPSTR(HA_NAME)] = F("Equitherm"); - doc[FPSTR(HA_ICON)] = F("mdi:sun-snowflake-variant"); - doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); - doc[FPSTR(HA_STATE_ON)] = true; - doc[FPSTR(HA_STATE_OFF)] = false; - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.enabled }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); - doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"equitherm\": {\"enabled\" : true}}"); - doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"equitherm\": {\"enabled\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("equitherm")).c_str(), doc); + return publishSwitch( + F("equitherm"), + F("Equitherm"), + F("mdi:sun-snowflake-variant"), + F("{{ value_json.equitherm.enabled }}"), + F("{\"equitherm\": {\"enabled\" : true}}"), + F("{\"equitherm\": {\"enabled\" : false}}"), + enabledByDefault + ); } bool publishInputEquithermFactorN(bool enabledByDefault = true) { @@ -967,126 +951,87 @@ public: } bool publishHeatingState(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - //doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); - doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); - doc[FPSTR(HA_NAME)] = F("Heating"); - doc[FPSTR(HA_ICON)] = F("mdi:radiator"); - doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.heating.active, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("heating")).c_str(), doc); + return publishBinarySensorState( + F("heating"), + F("Heating"), + F("mdi:radiator"), + F("{{ iif(value_json.slave.heating.active, 'ON', 'OFF') }}"), + F("running"), + enabledByDefault + ); } bool publishDhwState(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("dhw")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - //doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); - doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); - doc[FPSTR(HA_NAME)] = F("DHW"); - doc[FPSTR(HA_ICON)] = F("mdi:faucet"); - doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.dhw.active, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("dhw")).c_str(), doc); + return publishBinarySensorState( + F("dhw"), + F("DHW"), + F("mdi:faucet"), + F("{{ iif(value_json.slave.dhw.active, 'ON', 'OFF') }}"), + F("running"), + enabledByDefault + ); } bool publishFlameState(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("flame")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - //doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); - doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); - doc[FPSTR(HA_NAME)] = F("Flame"); - doc[FPSTR(HA_ICON)] = F("mdi:gas-burner"); - doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.flame, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("flame")).c_str(), doc); + return publishBinarySensorState( + F("flame"), + F("Flame"), + F("mdi:gas-burner"), + F("{{ iif(value_json.slave.flame, 'ON', 'OFF') }}"), + F("running"), + enabledByDefault + ); } bool publishFaultState(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("fault")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); - doc[FPSTR(HA_DEVICE_CLASS)] = F("problem"); - doc[FPSTR(HA_NAME)] = F("Fault"); - doc[FPSTR(HA_ICON)] = F("mdi:alert-remove-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.fault.active, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("fault")).c_str(), doc); + return publishBinarySensorState( + F("fault"), + F("Fault"), + F("mdi:alert-remove-outline"), + F("{{ iif(value_json.slave.fault.active, 'ON', 'OFF') }}"), + F("problem"), + enabledByDefault + ); } bool publishDiagState(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC)); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); - doc[FPSTR(HA_DEVICE_CLASS)] = F("problem"); - doc[FPSTR(HA_NAME)] = F("Diagnostic"); - doc[FPSTR(HA_ICON)] = F("mdi:account-wrench"); - doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.diag.active, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC)).c_str(), doc); + + return publishBinarySensorState( + F("diag"), + F("Diagnostic"), + F("mdi:account-wrench"), + F("{{ iif(value_json.slave.diag.active, 'ON', 'OFF') }}"), + F("problem"), + enabledByDefault + ); } bool publishExternalPumpState(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("ext_pump")); - doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; - doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); - doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); - doc[FPSTR(HA_NAME)] = F("External pump"); - doc[FPSTR(HA_ICON)] = F("mdi:pump"); - doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.master.externalPump.state, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; - doc.shrinkToFit(); - return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("ext_pump")).c_str(), doc); + return publishBinarySensorState( + F("ext_pump"), + F("External pump"), + F("mdi:pump"), + F("{{ iif(value_json.master.externalPump.state, 'ON', 'OFF') }}"), + F("running"), + enabledByDefault + ); + } + + bool publishExtDevState(const String& caption, bool enabledByDefault = true) { + + return publishBinarySensorState( + F("extdev"), + caption, + F("mdi:toggle-switch"), + F("{{ iif(value_json.master.externalDev.state, 'ON', 'OFF') }}"), + F("running"), + enabledByDefault + ); } bool publishFaultCode(bool enabledByDefault = true) { @@ -1357,6 +1302,79 @@ public: protected: unsigned short expireAfter = 300u; String statusTopic, stateTopic, setStateTopic, settingsTopic, setSettingsTopic; + + void initCommonDocFields(JsonDocument& doc, + const String& uniqueId, + const String& name, + const String& icon, + const String& stateTopic, + const String& entityCategory, + bool enabledByDefault) { + + doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; + doc[FPSTR(HA_UNIQUE_ID)] = uniqueId; + doc[FPSTR(HA_OBJECT_ID)] = uniqueId; + doc[FPSTR(HA_ENTITY_CATEGORY)] = entityCategory; + doc[FPSTR(HA_NAME)] = name; + doc[FPSTR(HA_ICON)] = icon; + doc[FPSTR(HA_STATE_TOPIC)] = stateTopic; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; + } + + bool publishSwitch(const String& id, + const String& name, + const String& icon, + const String& valueTemplate, + const String& payloadOn, + const String& payloadOff, + bool enabledByDefault) { + JsonDocument doc; + initCommonDocFields(doc, + this->getObjectIdWithPrefix(id), + name, + icon, + this->settingsTopic.c_str(), + FPSTR(HA_ENTITY_CATEGORY_CONFIG), + enabledByDefault ); + + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = valueTemplate; + doc[FPSTR(HA_STATE_ON)] = true; + doc[FPSTR(HA_STATE_OFF)] = false; + doc[FPSTR(HA_PAYLOAD_ON)] = payloadOn; + doc[FPSTR(HA_PAYLOAD_OFF)] = payloadOff; + doc.shrinkToFit(); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), id).c_str(), doc); + } + + bool publishBinarySensorState(const String& id, + const String& name, + const String& icon, + const String& valueTemplate, + const String& deviceClass, + bool enabledByDefault) { + JsonDocument doc; + initCommonDocFields(doc, + this->getObjectIdWithPrefix(id), + name, + icon, + this->stateTopic.c_str(), + FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC), + enabledByDefault ); + + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true); + doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); + + doc[FPSTR(HA_DEVICE_CLASS)] = deviceClass; + doc[FPSTR(HA_VALUE_TEMPLATE)] = valueTemplate; + + doc.shrinkToFit(); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), id).c_str(), doc); + } + }; const char HaHelper::AVAILABILITY_OT_CONN[] = "{{ iif(value_json.slave.connected, 'online', 'offline') }}"; diff --git a/src/MainTask.h b/src/MainTask.h index af02b60..71c4878 100644 --- a/src/MainTask.h +++ b/src/MainTask.h @@ -199,6 +199,7 @@ protected: this->emergency(); this->cascadeControl(); this->externalPump(); + this->externalDev(); this->miscRunned = millis(); return true; @@ -688,4 +689,46 @@ protected: Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: anti stuck")); } } -}; \ No newline at end of file + + void externalDev() { + static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED; + + if(!settings.externalDev.use) return; + + // configure output + // if settings are different than the configured GPIO, update + if (settings.externalDev.gpio != configuredGpio) { + if (configuredGpio != GPIO_IS_NOT_CONFIGURED) { + digitalWrite(configuredGpio, LOW); + } + + if (GPIO_IS_VALID(settings.externalDev.gpio)) { + configuredGpio = settings.externalDev.gpio; + pinMode(configuredGpio, OUTPUT); + digitalWrite(configuredGpio, LOW); + + } else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) { + configuredGpio = GPIO_IS_NOT_CONFIGURED; + } + } + + if (configuredGpio == GPIO_IS_NOT_CONFIGURED) { + if (vars.externalDev.state) { + vars.externalDev.state = false; + + Log.sinfoln(FPSTR(L_EXTDEV), F("Disabled: use = off")); + } + + return; + } + + // output configured update relay if required + if(settings.externalDev.state != vars.externalDev.state ) { + digitalWrite(configuredGpio, settings.externalDev.state? HIGH:LOW ); + vars.externalDev.state = settings.externalDev.state; + } + + } + +}; + diff --git a/src/MqttTask.h b/src/MqttTask.h index da29d0a..c7c2f43 100644 --- a/src/MqttTask.h +++ b/src/MqttTask.h @@ -506,6 +506,9 @@ protected: this->haHelper->publishInputEquithermFactorK(false); this->haHelper->publishInputEquithermFactorT(false); + // ext device + this->haHelper->publishSwitchExtDevice(String(settings.externalDev.caption), false); + // states this->haHelper->publishStatusState(); this->haHelper->publishEmergencyState(); @@ -515,6 +518,8 @@ protected: this->haHelper->publishFaultState(); this->haHelper->publishDiagState(); this->haHelper->publishExternalPumpState(false); + this->haHelper->publishExtDevState(String(settings.externalDev.caption), false); + // sensors this->haHelper->publishFaultCode(); diff --git a/src/Settings.h b/src/Settings.h index af71a46..6eae133 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -167,6 +167,13 @@ struct Settings { unsigned short antiStuckTime = 300; } externalPump; + struct { + bool use = false; + byte gpio = DEFAULT_EXT_DEV_GPIO; + char caption[41] = DEFAULT_EXT_DEV_CAPTION; + bool state = false; + } externalDev; + struct { struct { bool enabled = false; @@ -279,6 +286,10 @@ struct Variables { unsigned long lastEnabledTime = 0; } externalPump; + struct { + bool state = false; + } externalDev; + struct { bool input = false; bool output = false; diff --git a/src/defines.h b/src/defines.h index f5e0de9..534025f 100644 --- a/src/defines.h +++ b/src/defines.h @@ -146,6 +146,14 @@ #define DEFAULT_EXT_PUMP_GPIO GPIO_IS_NOT_CONFIGURED #endif +#ifndef DEFAULT_EXT_DEV_GPIO + #define DEFAULT_EXT_DEV_GPIO GPIO_IS_NOT_CONFIGURED +#endif + +#ifndef DEFAULT_EXT_DEV_CAPTION + #define DEFAULT_EXT_DEV_CAPTION "Device" +#endif + #ifndef PROGMEM #define PROGMEM #endif diff --git a/src/strings.h b/src/strings.h index 74e2eaf..d7d97b9 100644 --- a/src/strings.h +++ b/src/strings.h @@ -32,6 +32,7 @@ const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHE const char L_CASCADE_INPUT[] PROGMEM = "CASCADE.INPUT"; const char L_CASCADE_OUTPUT[] PROGMEM = "CASCADE.OUTPUT"; const char L_EXTPUMP[] PROGMEM = "EXTPUMP"; +const char L_EXTDEV[] PROGMEM = "EXTDEV"; const char S_ACTIONS[] PROGMEM = "actions"; @@ -82,6 +83,8 @@ const char S_ENV[] PROGMEM = "env"; const char S_EPC[] PROGMEM = "epc"; const char S_EQUITHERM[] PROGMEM = "equitherm"; const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump"; +const char S_EXTERNAL_DEV[] PROGMEM = "externalDev"; +const char S_EXTERNAL_DEV_CAPTION[] PROGMEM = "caption"; const char S_FACTOR[] PROGMEM = "factor"; const char S_FAULT[] PROGMEM = "fault"; const char S_FREEZE_PROTECTION[] PROGMEM = "freezeProtection"; diff --git a/src/utils.h b/src/utils.h index 4913357..53e0484 100644 --- a/src/utils.h +++ b/src/utils.h @@ -561,6 +561,15 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { cascadeControlOutput[FPSTR(S_ON_LOSS_CONNECTION)] = src.cascadeControl.output.onLossConnection; cascadeControlOutput[FPSTR(S_ON_ENABLED_HEATING)] = src.cascadeControl.output.onEnabledHeating; } + + if(!safe ) { + dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_USE)] = src.externalDev.use; + dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)] = src.externalDev.gpio; + dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_EXTERNAL_DEV_CAPTION)] = src.externalDev.caption; + } + + if(src.externalDev.use) + dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)] = src.externalDev.state; } inline void safeSettingsToJson(const Settings& src, JsonVariant dst) { @@ -1531,6 +1540,42 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } + // external device + if (src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_USE)].is()) { + bool value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_USE)].as(); + + if (value != dst.externalDev.use) { + dst.externalDev.use = value; + changed = true; + } + } + + if (!src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].isNull()) { + if (src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].is() && + src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].as().size() == 0) { + if (dst.externalDev.gpio != GPIO_IS_NOT_CONFIGURED) { + dst.externalDev.gpio = GPIO_IS_NOT_CONFIGURED; + changed = true; + } + + } else { + unsigned char value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].as(); + + if (GPIO_IS_VALID(value) && value != dst.externalDev.gpio) { + dst.externalDev.gpio = value; + changed = true; + } + } + } + + if (!src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_EXTERNAL_DEV_CAPTION)].isNull()) { + String value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_EXTERNAL_DEV_CAPTION)].as(); + + if (value.length() < sizeof(dst.externalDev.caption) && !String(dst.externalDev.caption).equals(value)) { + strcpy(dst.externalDev.caption, value.c_str()); + changed = true; + } + } // cascade control if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_ENABLED)].is()) { @@ -1653,6 +1698,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } + if (dst.externalDev.use && src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)].is()) { + bool value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)].as(); + + if (value != dst.externalDev.state) { + dst.externalDev.state = value; + changed = true; + } + } + // force check emergency target { float value = !src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].isNull() ? src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].as() : dst.emergency.target; @@ -2118,6 +2172,7 @@ void varsToJson(const Variables& src, JsonVariant dst) { master[FPSTR(S_MQTT)][FPSTR(S_CONNECTED)] = src.mqtt.connected; master[FPSTR(S_EMERGENCY)][FPSTR(S_STATE)] = src.emergency.state; master[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_STATE)] = src.externalPump.state; + master[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)] = src.externalDev.state; auto mCascadeControl = master[FPSTR(S_CASCADE_CONTROL)].to(); mCascadeControl[FPSTR(S_INPUT)] = src.cascadeControl.input; diff --git a/src_data/locales/cn.json b/src_data/locales/cn.json index 51ffb01..414e2e6 100644 --- a/src_data/locales/cn.json +++ b/src_data/locales/cn.json @@ -104,6 +104,7 @@ "mMqttConnected": "MQTT服务器连接状态", "mEmergencyState": "应急模式", "mExtPumpState": "外置循环泵", + "mExtDevState": "外置设备", "mCascadeControlInput": "Cascade 控制 (input)", "mCascadeControlOutput": "Cascade 控制 (output)", @@ -289,6 +290,7 @@ "ot": "OpenTherm协议设置", "mqtt": "MQTT 服务器设置", "extPump": "外置循环泵设置", + "extDev": "外置设备设置", "cascadeControl": "Cascade 级联控制设置" }, @@ -459,6 +461,13 @@ "antiStuckTime": "防卡死运行时长(分钟)" }, + "extDev": { + "use": "使用外置设备", + "gpio": "GPIO 继电器", + "state": "状态", + "caption": "说明" + }, + "cascadeControl": { "input": { "desc": "仅当另一台锅炉发生故障时启用本锅炉加热。另一台锅炉的控制器需在故障发生时切换GPIO输入状态以触发本功能。", diff --git a/src_data/locales/en.json b/src_data/locales/en.json index c801561..ba5b011 100644 --- a/src_data/locales/en.json +++ b/src_data/locales/en.json @@ -104,6 +104,7 @@ "mMqttConnected": "MQTT connection", "mEmergencyState": "Emergency mode", "mExtPumpState": "External pump", + "mExtDevState": "External device", "mCascadeControlInput": "Cascade control (input)", "mCascadeControlOutput": "Cascade control (output)", @@ -289,6 +290,7 @@ "ot": "OpenTherm settings", "mqtt": "MQTT settings", "extPump": "External pump settings", + "extDev": "External device settings", "cascadeControl": "Cascade control settings" }, @@ -458,6 +460,13 @@ "antiStuckInterval": "Anti stuck interval (days)", "antiStuckTime": "Anti stuck time (min)" }, + + "extDev": { + "use": "Use external device", + "gpio": "Relay GPIO", + "state": "State", + "caption": "Caption" + }, "cascadeControl": { "input": { diff --git a/src_data/locales/it.json b/src_data/locales/it.json index 9a13be8..5ba664b 100644 --- a/src_data/locales/it.json +++ b/src_data/locales/it.json @@ -104,6 +104,7 @@ "mMqttConnected": "Connessione MQTT", "mEmergencyState": "Modo Emergenza", "mExtPumpState": "Pompa esterna", + "mExtDevState": "Dispositivo esterno", "mCascadeControlInput": "Controllo a cascata (input)", "mCascadeControlOutput": "Controllo a cascata (output)", @@ -289,6 +290,7 @@ "ot": "Impostazioni OpenTherm", "mqtt": "Impostazioni MQTT", "extPump": "Impostazioni pompa esterna", + "extDev": "Impostazioni dispositivo esterno", "cascadeControl": "Impostazioni controllo a cascata" }, @@ -459,6 +461,13 @@ "antiStuckTime": "Tempo antiblocco (min)" }, + "extDev": { + "use": "Usa dispositivo esterno", + "gpio": "GPIO relè", + "state": "Stato", + "caption": "Didascalia" + }, + "cascadeControl": { "input": { "desc": "Può essere attivata la caldaia se un'altra ha fallito. Il controllo dell'altra caldaia cambia lo stato dell'ingresso del GPIO in caso di errore.", diff --git a/src_data/locales/ru.json b/src_data/locales/ru.json index 28055f7..3faeec0 100644 --- a/src_data/locales/ru.json +++ b/src_data/locales/ru.json @@ -104,6 +104,7 @@ "mMqttConnected": "Подключение к MQTT", "mEmergencyState": "Аварийный режим", "mExtPumpState": "Внешний насос", + "mExtDevState": "Внешнее устройство", "mCascadeControlInput": "Каскадное управление (вход)", "mCascadeControlOutput": "Каскадное управление (выход)", @@ -289,6 +290,7 @@ "ot": "Настройки OpenTherm", "mqtt": "Настройки MQTT", "extPump": "Настройки дополнительного насоса", + "extDev": "Настройки внешнего устройства", "cascadeControl": "Настройки каскадного управления" }, @@ -459,6 +461,13 @@ "antiStuckTime": "Время работы насоса (в минутах)" }, + "extDev": { + "use": "Использовать внешнее устройство", + "gpio": "GPIO реле", + "state": "Состояние", + "caption": "Назначение" + }, + "cascadeControl": { "input": { "desc": "Может использоваться для включения отопления только при неисправности другого котла. Контроллер другого котла должен изменить состояние входа GPIO в случае неисправности.", diff --git a/src_data/pages/dashboard.html b/src_data/pages/dashboard.html index 4a85a83..cde5f48 100644 --- a/src_data/pages/dashboard.html +++ b/src_data/pages/dashboard.html @@ -135,6 +135,10 @@ dashboard.states.mExtPumpState + + dashboard.states.mExtDevState + + dashboard.states.mCascadeControlInput @@ -658,6 +662,7 @@ result.master.emergency.state ? "red" : "green" ); setState('.mExtPumpState', result.master.externalPump.state); + setState('.mExtDevState', result.master.externalDev.state); setState('.mCascadeControlInput', result.master.cascadeControl.input); setState('.mCascadeControlOutput', result.master.cascadeControl.output); diff --git a/src_data/pages/settings.html b/src_data/pages/settings.html index 49f6327..f5789c0 100644 --- a/src_data/pages/settings.html +++ b/src_data/pages/settings.html @@ -763,6 +763,42 @@
+
+ settings.section.extDev +
+
+ +
+
+ +
+
settings.section.cascadeControl
@@ -932,6 +968,13 @@ setInputValue("[name='externalPump[antiStuckTime]']", data.externalPump.antiStuckTime); setBusy('#extpump-settings-busy', '#extpump-settings', false); + // Extdev + setCheckboxValue('#extdev-use', data.externalDev.use); + setInputValue('#extdev-gpio', data.externalDev.gpio < 255 ? data.externalDev.gpio : ''); + setInputValue('#extdev-caption', data.externalDev.caption); + setCheckboxValue('#extdev-state', data.externalDev.state); + setBusy('#extdev-settings-busy', '#extdev-settings', false); + // Cascade control setCheckboxValue("[name='cascadeControl[input][enabled]']", data.cascadeControl.input.enabled); setInputValue("[name='cascadeControl[input][gpio]']", data.cascadeControl.input.gpio < 255 ? data.cascadeControl.input.gpio : ''); @@ -1085,6 +1128,7 @@ setupForm('#ot-settings', fillData); setupForm('#mqtt-settings', fillData, ['mqtt.user', 'mqtt.password', 'mqtt.prefix']); setupForm('#extpump-settings', fillData); + setupForm('#extdev-settings', fillData); setupForm('#cc-settings', fillData); } catch (error) {