commit 72d454cf57e18ade2f16262847fd29ea2d8a8296 Author: Yurii Date: Sun Jun 26 07:59:16 2022 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64fa8cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.vscode +/bin/* +!/bin/src.ino.bin \ No newline at end of file diff --git a/bin/src.ino.bin b/bin/src.ino.bin new file mode 100644 index 0000000..2425dc9 Binary files /dev/null and b/bin/src.ino.bin differ diff --git a/src/HomeAssistantHelper.h b/src/HomeAssistantHelper.h new file mode 100644 index 0000000..16e3106 --- /dev/null +++ b/src/HomeAssistantHelper.h @@ -0,0 +1,1280 @@ +extern PubSubClient client; + +class HomeAssistantHelper { +public: + void setPrefix(String value) { + _prefix = value; + } + + void setDeviceVersion(String value) { + _deviceVersion = value; + } + + void setDeviceManufacturer(String value) { + _deviceManufacturer = value; + } + + void setDeviceModel(String value) { + _deviceModel = value; + } + + void setDeviceName(String value) { + _deviceName = value; + } + + void setDeviceConfigUrl(String value) { + _deviceConfigUrl = value; + } + + bool publishSelectOutdoorTempSource(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = F("{\"outdoorTempSource\": {% if value == 'Boiler' %}0{% elif value == 'Manual' %}1{% elif value == 'External' %}2{% endif %}}"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_outdoorTempSource"; + doc["object_id"] = _prefix + "_outdoorTempSource"; + doc["entity_category"] = "config"; + doc["name"] = "Outdoor temperature source"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = F("{% if value_json.outdoorTempSource == 0 %}Boiler{% elif value_json.outdoorTempSource == 1 %}Manual{% elif value_json.outdoorTempSource == 2 %}External{% endif %}"); + doc["options"][0] = F("Boiler"); + doc["options"][1] = F("Manual"); + doc["options"][2] = F("External"); + + client.beginPublish((F("homeassistant/select/") + _prefix + "/outdoorTempSource/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSwitchDebug(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_debug"; + doc["object_id"] = _prefix + "_debug"; + doc["entity_category"] = "config"; + doc["name"] = "Debug"; + doc["icon"] = "mdi:code-braces"; + doc["state_topic"] = _prefix + F("/settings"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.debug }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["payload_on"] = "{\"debug\": true}"; + doc["payload_off"] = "{\"debug\": false}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/debug/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishSwitchEmergency(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_emergency"; + doc["object_id"] = _prefix + "_emergency"; + doc["entity_category"] = "config"; + doc["name"] = "Use emergency"; + doc["icon"] = "mdi:sun-snowflake-variant"; + doc["state_topic"] = _prefix + F("/settings"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.emergency.enable }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["payload_on"] = "{\"emergency\": {\"enable\" : true}}"; + doc["payload_off"] = "{\"emergency\": {\"enable\" : false}}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/emergency/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberEmergencyTarget(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_emergency_target"; + doc["object_id"] = _prefix + "_emergency_target"; + doc["entity_category"] = "config"; + doc["device_class"] = "temperature"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Emergency target temp"; + doc["icon"] = "mdi:thermometer-alert"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.emergency.target|float(0)|round(1) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"emergency\": {\"target\" : {{ value }}}}"; + doc["min"] = 5; + doc["max"] = 50; + doc["step"] = 0.5; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/emergency_target/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSwitchEmergencyUseEquitherm(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/settings"); + doc["availability"]["value_template"] = F("{{ iif(value_json.outdoorTempSource != 1, 'online', 'offline') }}"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_emergency_use_equitherm"; + doc["object_id"] = _prefix + "_emergency_use_equitherm"; + doc["entity_category"] = "config"; + doc["name"] = "Use equitherm in emergency"; + doc["icon"] = "mdi:snowflake-alert"; + doc["state_topic"] = _prefix + F("/settings"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.emergency.useEquitherm }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["payload_on"] = "{\"emergency\": {\"useEquitherm\" : true}}"; + doc["payload_off"] = "{\"emergency\": {\"useEquitherm\" : false}}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/emergency_use_equitherm/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishSwitchHeating(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_heating"; + doc["object_id"] = _prefix + "_heating"; + doc["entity_category"] = "config"; + doc["name"] = "Heating"; + doc["icon"] = "mdi:radiator"; + doc["state_topic"] = _prefix + F("/settings"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.heating.enable }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["payload_on"] = "{\"heating\": {\"enable\" : true}}"; + doc["payload_off"] = "{\"heating\": {\"enable\" : false}}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/heating/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberHeatingTarget(byte minTemp = 20, byte maxTemp = 90, bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_heating_target"; + doc["object_id"] = _prefix + "_heating_target"; + doc["entity_category"] = "config"; + doc["device_class"] = "temperature"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Heating target"; + doc["icon"] = "mdi:radiator"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.heating.target|float(0)|round(1) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"heating\": {\"target\" : {{ value }}}}"; + doc["min"] = minTemp; + doc["max"] = maxTemp; + doc["step"] = 0.5; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/heating_target/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberHeatingHysteresis(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_heating_hysteresis"; + doc["object_id"] = _prefix + "_heating_hysteresis"; + doc["entity_category"] = "config"; + doc["device_class"] = "temperature"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Heating hysteresis"; + doc["icon"] = "mdi:altimeter"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.heating.hysteresis|float(0)|round(1) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"heating\": {\"hysteresis\" : {{ value }}}}"; + doc["min"] = 0; + doc["max"] = 5; + doc["step"] = 0.1; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/heating_hysteresis/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSensorHeatingSetpoint(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_heating_setpoint"; + doc["object_id"] = _prefix + "_heating_setpoint"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "temperature"; + doc["state_class"] = "measurement"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Heating setpoint"; + doc["icon"] = "mdi:coolant-temperature"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.parameters.heatingSetpoint|int(0) }}"; + + client.beginPublish((F("homeassistant/sensor/") + _prefix + "/heating_setpoint/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishSwitchDHW(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_dhw"; + doc["object_id"] = _prefix + "_dhw"; + doc["entity_category"] = "config"; + doc["name"] = "DHW"; + doc["icon"] = "mdi:water-pump"; + doc["state_topic"] = _prefix + F("/settings"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.dhw.enable }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["payload_on"] = "{\"dhw\": {\"enable\" : true}}"; + doc["payload_off"] = "{\"dhw\": {\"enable\" : false}}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/dhw/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberDHWTarget(byte minTemp = 40, byte maxTemp = 60, bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_dhw_target"; + doc["object_id"] = _prefix + "_dhw_target"; + doc["entity_category"] = "config"; + doc["device_class"] = "temperature"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "DHW target"; + doc["icon"] = "mdi:water-pump"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.dhw.target|int(0) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"dhw\": {\"target\" : {{ value|int(0) }}}}"; + doc["min"] = minTemp; + doc["max"] = maxTemp; + doc["step"] = 1; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/dhw_target/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishSwitchPID(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_pid"; + doc["object_id"] = _prefix + "_pid"; + doc["entity_category"] = "config"; + doc["name"] = "PID"; + doc["icon"] = "mdi:chart-bar-stacked"; + doc["state_topic"] = _prefix + F("/settings"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.pid.enable }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["payload_on"] = "{\"pid\": {\"enable\" : true}}"; + doc["payload_off"] = "{\"pid\": {\"enable\" : false}}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/pid/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberPIDFactorP(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["unique_id"] = _prefix + "_pid_p"; + doc["object_id"] = _prefix + "_pid_p"; + doc["entity_category"] = "config"; + doc["name"] = "PID factor P"; + doc["icon"] = "mdi:alpha-p-circle-outline"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.pid.p_factor|float(0)|round(3) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"pid\": {\"p_factor\" : {{ value }}}}"; + doc["min"] = 0.001; + doc["max"] = 3; + doc["step"] = 0.001; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/pid_p_factor/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberPIDFactorI(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["unique_id"] = _prefix + "_pid_i"; + doc["object_id"] = _prefix + "_pid_i"; + doc["entity_category"] = "config"; + doc["name"] = "PID factor I"; + doc["icon"] = "mdi:alpha-i-circle-outline"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.pid.i_factor|float(0)|round(3) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"pid\": {\"i_factor\" : {{ value }}}}"; + doc["min"] = 0; + doc["max"] = 3; + doc["step"] = 0.001; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/pid_i_factor/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberPIDFactorD(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["unique_id"] = _prefix + "_pid_d"; + doc["object_id"] = _prefix + "_pid_d"; + doc["entity_category"] = "config"; + doc["name"] = "PID factor D"; + doc["icon"] = "mdi:alpha-d-circle-outline"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.pid.d_factor|float(0)|round(3) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"pid\": {\"d_factor\" : {{ value }}}}"; + doc["min"] = 0; + doc["max"] = 3; + doc["step"] = 0.001; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/pid_d_factor/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishSwitchEquitherm(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_equitherm"; + doc["object_id"] = _prefix + "_equitherm"; + doc["entity_category"] = "config"; + doc["name"] = "Equitherm"; + doc["icon"] = "mdi:sun-snowflake-variant"; + doc["state_topic"] = _prefix + F("/settings"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.equitherm.enable }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["payload_on"] = "{\"equitherm\": {\"enable\" : true}}"; + doc["payload_off"] = "{\"equitherm\": {\"enable\" : false}}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/equitherm/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberEquithermFactorN(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["unique_id"] = _prefix + "_equitherm_n"; + doc["object_id"] = _prefix + "_equitherm_n"; + doc["entity_category"] = "config"; + doc["name"] = "Equitherm factor N"; + doc["icon"] = "mdi:alpha-n-circle-outline"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.equitherm.n_factor|float(0)|round(3) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"equitherm\": {\"n_factor\" : {{ value }}}}"; + doc["min"] = 0.001; + doc["max"] = 5; + doc["step"] = 0.001; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/equitherm_n_factor/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberEquithermFactorK(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["unique_id"] = _prefix + "_equitherm_k"; + doc["object_id"] = _prefix + "_equitherm_k"; + doc["entity_category"] = "config"; + doc["name"] = "Equitherm factor K"; + doc["icon"] = "mdi:alpha-k-circle-outline"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.equitherm.k_factor|float(0)|round(2) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"equitherm\": {\"k_factor\" : {{ value }}}}"; + doc["min"] = 0; + doc["max"] = 10; + doc["step"] = 0.01; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/equitherm_k_factor/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberEquithermFactorT(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["unique_id"] = _prefix + "_equitherm_t"; + doc["object_id"] = _prefix + "_equitherm_t"; + doc["entity_category"] = "config"; + doc["name"] = "Equitherm factor T"; + doc["icon"] = "mdi:alpha-t-circle-outline"; + doc["state_topic"] = _prefix + F("/settings"); + doc["value_template"] = "{{ value_json.equitherm.t_factor|float(0)|round(2) }}"; + doc["command_topic"] = _prefix + "/settings/set"; + doc["command_template"] = "{\"equitherm\": {\"t_factor\" : {{ value }}}}"; + doc["min"] = 0; + doc["max"] = 10; + doc["step"] = 0.01; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/equitherm_t_factor/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishSwitchTuning(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_tuning"; + doc["object_id"] = _prefix + "_tuning"; + doc["entity_category"] = "config"; + doc["name"] = "Tuning"; + doc["icon"] = "mdi:tune-vertical"; + doc["state_topic"] = _prefix + F("/state"); + doc["state_on"] = true; + doc["state_off"] = false; + doc["value_template"] = "{{ value_json.tuning.enable }}"; + doc["command_topic"] = _prefix + "/state/set"; + doc["payload_on"] = "{\"tuning\": {\"enable\" : true}}"; + doc["payload_off"] = "{\"tuning\": {\"enable\" : false}}"; + + client.beginPublish((F("homeassistant/switch/") + _prefix + "/tuning/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSelectTuningRegulator(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["availability_mode"] = F("all"); + doc["command_topic"] = _prefix + "/state/set"; + doc["command_template"] = F("{\"tuning\": {\"regulator\": {% if value == 'Equitherm' %}0{% elif value == 'PID' %}1{% endif %}}}"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_tuning_regulator"; + doc["object_id"] = _prefix + "_tuning_regulator"; + doc["entity_category"] = "config"; + doc["name"] = "Tuning regulator"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = F("{% if value_json.tuning.regulator == 0 %}Equitherm{% elif value_json.tuning.regulator == 1 %}PID{% endif %}"); + doc["options"][0] = F("Equitherm"); + doc["options"][1] = F("PID"); + + client.beginPublish((F("homeassistant/select/") + _prefix + "/tuning_regulator/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishBinSensorStatus(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_status"; + doc["object_id"] = _prefix + "_status"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "problem"; + doc["name"] = "Status"; + doc["icon"] = "mdi:list-status"; + doc["state_topic"] = _prefix + F("/status"); + doc["value_template"] = "{{ iif(value == 'online', 'OFF', 'ON') }}"; + doc["expire_after"] = 60; + + client.beginPublish((F("homeassistant/binary_sensor/") + _prefix + "/status/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishBinSensorOtStatus(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_ot_status"; + doc["object_id"] = _prefix + "_ot_status"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "problem"; + doc["name"] = "Opentherm status"; + doc["icon"] = "mdi:list-status"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ iif(value_json.states.otStatus, 'OFF', 'ON') }}"; + + client.beginPublish((F("homeassistant/binary_sensor/") + _prefix + "/ot_status/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishBinSensorHeating(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_heating"; + doc["object_id"] = _prefix + "_heating"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "running"; + doc["name"] = "Heating"; + doc["icon"] = "mdi:radiator"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = F("{{ iif(value_json.states.heating, 'ON', 'OFF') }}"); + + client.beginPublish((F("homeassistant/binary_sensor/") + _prefix + "/heating/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishBinSensorDHW(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_dhw"; + doc["object_id"] = _prefix + "_dhw"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "running"; + doc["name"] = "DHW"; + doc["icon"] = "mdi:water-pump"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = F("{{ iif(value_json.states.dhw, 'ON', 'OFF') }}"); + + client.beginPublish((F("homeassistant/binary_sensor/") + _prefix + "/dhw/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishBinSensorFlame(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_flame"; + doc["object_id"] = _prefix + "_flame"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "running"; + doc["name"] = "Flame"; + doc["icon"] = "mdi:fire"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = F("{{ iif(value_json.states.flame, 'ON', 'OFF') }}"); + + client.beginPublish((F("homeassistant/binary_sensor/") + _prefix + "/flame/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishBinSensorFault(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/state"); + doc["availability"]["value_template"] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_fault"; + doc["object_id"] = _prefix + "_fault"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "problem"; + doc["name"] = "Fault"; + doc["icon"] = "mdi:water-boiler-alert"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ iif(value_json.states.fault, 'ON', 'OFF') }}"; + + client.beginPublish((F("homeassistant/binary_sensor/") + _prefix + "/fault/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishBinSensorDiagnostic(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_diagnostic"; + doc["object_id"] = _prefix + "_diagnostic"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "problem"; + doc["name"] = "Diagnostic"; + doc["icon"] = "mdi:account-wrench"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ iif(value_json.states.diagnostic, 'ON', 'OFF') }}"; + + client.beginPublish((F("homeassistant/binary_sensor/") + _prefix + "/diagnostic/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSensorFaultCode(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/state"); + doc["availability"]["value_template"] = F("{{ iif(value_json.states.fault, 'online', 'offline') }}"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_fault_code"; + doc["object_id"] = _prefix + "_fault_code"; + doc["entity_category"] = "diagnostic"; + doc["name"] = "Fault code"; + doc["icon"] = "mdi:chat-alert-outline"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = F("{{ \"E%02d\"|format(value_json.states.faultCode) }}"); + + client.beginPublish((F("homeassistant/sensor/") + _prefix + "/fault_code/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishSensorModulation(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_modulation_level"; + doc["object_id"] = _prefix + "_modulation_level"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "power_factor"; + doc["state_class"] = "measurement"; + doc["unit_of_measurement"] = "%"; + doc["name"] = "Modulation level"; + doc["icon"] = "mdi:fire-circle"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.sensors.modulation|float(0)|round(0) }}"; + + client.beginPublish((F("homeassistant/sensor/") + _prefix + "/modulation/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSensorPressure(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_pressure"; + doc["object_id"] = _prefix + "_pressure"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "pressure"; + doc["state_class"] = "measurement"; + doc["unit_of_measurement"] = "bar"; + doc["name"] = "Pressure"; + doc["icon"] = "mdi:gauge"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.sensors.pressure|float(0)|round(2) }}"; + + client.beginPublish((F("homeassistant/sensor/") + _prefix + "/pressure/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishNumberIndoorTemp(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + //doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_indoor_temp"; + doc["object_id"] = _prefix + "_indoor_temp"; + doc["entity_category"] = "config"; + //doc["entity_registry_visible_default"] = false; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Indoor temperature"; + doc["icon"] = "mdi:home-thermometer"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.temperatures.indoor|float(0)|round(1) }}"; + doc["command_topic"] = _prefix + "/state/set"; + doc["command_template"] = "{\"temperatures\": {\"indoor\":{{ value }}}}"; + doc["min"] = -70; + doc["max"] = 50; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/indoor_temp/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishNumberOutdoorTemp(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_outdoor_temp"; + doc["object_id"] = _prefix + "_outdoor_temp"; + doc["entity_category"] = "config"; + //doc["entity_registry_visible_default"] = false; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Outdoor temperature"; + doc["icon"] = "mdi:home-thermometer-outline"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.temperatures.outdoor|float(0)|round(1) }}"; + doc["command_topic"] = _prefix + "/state/set"; + doc["command_template"] = "{\"temperatures\": {\"outdoor\":{{ value }}}}"; + doc["min"] = -70; + doc["max"] = 50; + + client.beginPublish((F("homeassistant/number/") + _prefix + "/outdoor_temp/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSensorOutdoorTemp(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"][0]["topic"] = _prefix + F("/status"); + doc["availability"][1]["topic"] = _prefix + F("/settings"); + doc["availability"][1]["value_template"] = F("{{ iif(value_json.outdoorTempSource == 2, 'online', 'offline') }}"); + doc["availability_mode"] = "any"; + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_outdoor_temp"; + doc["object_id"] = _prefix + "_outdoor_temp"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "temperature"; + doc["state_class"] = "measurement"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Outdoor temperature"; + doc["icon"] = "mdi:home-thermometer-outline"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.temperatures.outdoor|float(0)|round(1) }}"; + + client.beginPublish((F("homeassistant/sensor/") + _prefix + "/outdoor_temp/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSensorHeatingTemp(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_heating_temp"; + doc["object_id"] = _prefix + "_heating_temp"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "temperature"; + doc["state_class"] = "measurement"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "Heating temperature"; + doc["icon"] = "mdi:radiator"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.temperatures.heating|float(0)|round(2) }}"; + + client.beginPublish((F("homeassistant/sensor/") + _prefix + "/heating_temp/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishSensorDHWTemp(bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_dhw_temp"; + doc["object_id"] = _prefix + "_dhw_temp"; + doc["entity_category"] = "diagnostic"; + doc["device_class"] = "temperature"; + doc["state_class"] = "measurement"; + doc["unit_of_measurement"] = "°C"; + doc["name"] = "DHW temperature"; + doc["icon"] = "mdi:water-pump"; + doc["state_topic"] = _prefix + F("/state"); + doc["value_template"] = "{{ value_json.temperatures.dhw|float(0)|round(2) }}"; + + client.beginPublish((F("homeassistant/sensor/") + _prefix + "/dhw_temp/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool publishClimateHeating(byte minTemp = 20, byte maxTemp = 90, bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_heating"; + doc["object_id"] = _prefix + "_heating"; + doc["name"] = "Heating"; + doc["icon"] = "mdi:radiator"; + + doc["current_temperature_topic"] = _prefix + F("/state"); + doc["value_template"] = F("{{ value_json.temperatures.indoor|float(0)|round(2) }}"); + + doc["temperature_command_topic"] = _prefix + "/settings/set"; + doc["temperature_command_template"] = "{\"heating\": {\"target\" : {{ value }}}}"; + + doc["temperature_state_topic"] = _prefix + F("/settings"); + doc["temperature_state_template"] = F("{{ value_json.heating.target|float(0)|round(1) }}"); + + doc["mode_command_topic"] = _prefix + "/settings/set"; + doc["mode_command_template"] = F("{% if value == 'heat' %}{\"heating\": {\"enable\" : true}}" + "{% elif value == 'off' %}{\"heating\": {\"enable\" : false}}{% endif %}"); + doc["mode_state_topic"] = _prefix + F("/settings"); + doc["mode_state_template"] = F("{{ iif(value_json.heating.enable, 'heat', 'off') }}"); + doc["modes"][0] = "off"; + doc["modes"][1] = "heat"; + doc["min_temp"] = minTemp; + doc["max_temp"] = maxTemp; + doc["temp_step"] = 0.5; + + client.beginPublish((F("homeassistant/climate/") + _prefix + "_heating/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + bool publishClimateDHW(byte minTemp = 40, byte maxTemp = 60, bool enabledByDefault = true) { + StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/status"); + doc["device"]["identifiers"][0] = _prefix; + doc["device"]["sw_version"] = _deviceVersion; + doc["device"]["manufacturer"] = _deviceManufacturer; + doc["device"]["model"] = _deviceModel; + doc["device"]["name"] = _deviceName; + if (_deviceConfigUrl) { + doc["device"]["configuration_url"] = _deviceConfigUrl; + } + + doc["enabled_by_default"] = enabledByDefault; + doc["unique_id"] = _prefix + "_dhw"; + doc["object_id"] = _prefix + "_dhw"; + doc["name"] = "DHW"; + doc["icon"] = "mdi:water-pump"; + + doc["current_temperature_topic"] = _prefix + F("/state"); + doc["value_template"] = F("{{ value_json.temperatures.dhw|float(0)|round(1) }}"); + + doc["temperature_command_topic"] = _prefix + "/settings/set"; + doc["temperature_command_template"] = "{\"dhw\": {\"target\" : {{ value|int(0) }}}}"; + + doc["temperature_state_topic"] = _prefix + F("/settings"); + doc["temperature_state_template"] = F("{{ value_json.dhw.target|int(0) }}"); + + doc["mode_command_topic"] = _prefix + "/settings/set"; + doc["mode_command_template"] = F("{% if value == 'heat' %}{\"dhw\": {\"enable\" : true}}" + "{% elif value == 'off' %}{\"dhw\": {\"enable\" : false}}{% endif %}"); + doc["mode_state_topic"] = _prefix + F("/settings"); + doc["mode_state_template"] = F("{{ iif(value_json.dhw.enable, 'heat', 'off') }}"); + doc["modes"][0] = "off"; + doc["modes"][1] = "heat"; + doc["min_temp"] = minTemp; + doc["max_temp"] = maxTemp; + + client.beginPublish((F("homeassistant/climate/") + _prefix + "_dhw/config").c_str(), measureJson(doc), true); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + + bool deleteNumberOutdoorTemp() { + return client.publish((F("homeassistant/number/") + _prefix + "/outdoor_temp/config").c_str(), NULL, true); + } + + bool deleteSensorOutdoorTemp() { + return client.publish((F("homeassistant/sensor/") + _prefix + "/outdoor_temp/config").c_str(), NULL, true); + } + +private: + String _prefix = "opentherm"; + String _deviceVersion = "1.0"; + String _deviceManufacturer = "Community"; + String _deviceModel = "Opentherm Gateway"; + String _deviceName = "Opentherm Gateway"; + String _deviceConfigUrl = ""; +}; diff --git a/src/MainTask.h b/src/MainTask.h new file mode 100644 index 0000000..d359176 --- /dev/null +++ b/src/MainTask.h @@ -0,0 +1,83 @@ +#include "lib/MiniTask.h" +#include "SensorsTask.h" +#include "RegulatorTask.h" + +extern MqttTask* tMqtt; + +class MainTask : public CustomTask { +public: + MainTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {} + +protected: + //HttpServerTask* tHttpServer; + SensorsTask* tSensors; + RegulatorTask* tRegulator; + + void setup() { + //tHttpServer = new HttpServerTask(false); + tSensors = new SensorsTask(false, DS18B20_INTERVAL); + tRegulator = new RegulatorTask(true, 10000); + } + + void loop() { + static unsigned long lastHeapInfo = 0; + static unsigned short minFreeHeapSize = 65535; + + if (eeSettings.tick()) { + INFO("Settings updated (EEPROM)"); + } + + if (WiFi.status() == WL_CONNECTED) { + if (!tMqtt->isEnabled()) { + tMqtt->enable(); + } + + } else { + if (tMqtt->isEnabled()) { + tMqtt->disable(); + } + + vars.states.emergency = true; + } + + if (!tSensors->isEnabled() && settings.outdoorTempSource == 2) { + tSensors->enable(); + } else if (tSensors->isEnabled() && settings.outdoorTempSource != 2) { + tSensors->disable(); + } + + //tHttpServer->loopWrapper(); + //yield(); + tSensors->loopWrapper(); + yield(); + tRegulator->loopWrapper(); + +#ifdef USE_TELNET + yield(); + + // anti memory leak + TelnetStream.flush(); + while (TelnetStream.available() > 0) { + TelnetStream.read(); + } +#endif + + if (settings.debug) { + unsigned short freeHeapSize = ESP.getFreeHeap(); + unsigned short minFreeHeapSizeDiff = 0; + + if (freeHeapSize < minFreeHeapSize) { + minFreeHeapSizeDiff = minFreeHeapSize - freeHeapSize; + minFreeHeapSize = freeHeapSize; + } + if (millis() - lastHeapInfo > 10000 || minFreeHeapSizeDiff > 0) { + DEBUG_F("Free heap size: %hu bytes, min: %hu bytes (diff: %hu bytes)\n", freeHeapSize, minFreeHeapSize, minFreeHeapSizeDiff); + lastHeapInfo = millis(); + } + } + } + + /*char[] getUptime() { + uint64_t = esp_timer_get_time(); + }*/ +}; \ No newline at end of file diff --git a/src/MqttTask.h b/src/MqttTask.h new file mode 100644 index 0000000..a6910e0 --- /dev/null +++ b/src/MqttTask.h @@ -0,0 +1,474 @@ +#include +#include +#include +#include "HomeAssistantHelper.h" + +WiFiClient espClient; +PubSubClient client(espClient); +HomeAssistantHelper haHelper; + + +class MqttTask : public CustomTask { +public: + MqttTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {} + +protected: + unsigned long lastReconnectAttempt = 0; + unsigned short int reconnectAttempts = 0; + + void setup() { + client.setServer(settings.mqtt.server, settings.mqtt.port); + client.setCallback(__callback); + haHelper.setPrefix(settings.mqtt.prefix); + haHelper.setDeviceVersion(OT_GATEWAY_VERSION); + + sprintf(buffer, CONFIG_URL, WiFi.localIP().toString().c_str()); + haHelper.setDeviceConfigUrl(buffer); + } + + void loop() { + if (!client.connected() && millis() - lastReconnectAttempt >= MQTT_RECONNECT_INTERVAL) { + INFO_F("Mqtt not connected, state: %i, connecting to server %s...\n", client.state(), settings.mqtt.server); + + if (client.connect(settings.hostname, settings.mqtt.user, settings.mqtt.password)) { + INFO("Connected to MQTT server"); + + client.subscribe(getTopicPath("settings/set").c_str()); + client.subscribe(getTopicPath("state/set").c_str()); + publishHaEntities(); + publishNonStaticHaEntities(true); + + reconnectAttempts = 0; + lastReconnectAttempt = 0; + + } else { + INFO("Failed to connect to MQTT server\n"); + + if (!vars.states.emergency && ++reconnectAttempts >= EMERGENCY_TRESHOLD) { + vars.states.emergency = true; + INFO("Emergency mode enabled"); + } + + forceARP(); + lastReconnectAttempt = millis(); + } + } + + + if (client.connected()) { + if (vars.states.emergency) { + vars.states.emergency = false; + + INFO("Emergency mode disabled"); + } + + client.loop(); + bool published = publishNonStaticHaEntities(); + publish(published); + } + } + + + static void forceARP() { + struct netif* netif = netif_list; + while (netif) { + etharp_gratuitous(netif); + netif = netif->next; + } + } + + static bool updateSettings(JsonDocument& doc) { + bool flag = false; + + if (!doc["debug"].isNull() && doc["debug"].is()) { + settings.debug = doc["debug"].as(); + flag = true; + } + + if (!doc["outdoorTempSource"].isNull() && doc["outdoorTempSource"].is() && doc["outdoorTempSource"] >= 0 && doc["outdoorTempSource"] <= 2) { + settings.outdoorTempSource = doc["outdoorTempSource"]; + flag = true; + } + + if (!doc["mqtt"]["interval"].isNull() && doc["mqtt"]["interval"].is() && doc["mqtt"]["interval"] >= 1000 && doc["mqtt"]["interval"] <= 120000) { + settings.mqtt.interval = doc["mqtt"]["interval"].as(); + flag = true; + } + + // emergency + if (!doc["emergency"]["enable"].isNull() && doc["emergency"]["enable"].is()) { + settings.emergency.enable = doc["emergency"]["enable"].as(); + flag = true; + } + + if (!doc["emergency"]["target"].isNull() && (doc["emergency"]["target"].is() || doc["emergency"]["target"].is())) { + settings.emergency.target = round(doc["emergency"]["target"].as() * 10) / 10; + flag = true; + } + + if (!doc["emergency"]["useEquitherm"].isNull() && doc["emergency"]["useEquitherm"].is()) { + settings.emergency.useEquitherm = doc["emergency"]["useEquitherm"].as(); + flag = true; + } + + // heating + if (!doc["heating"]["enable"].isNull() && doc["heating"]["enable"].is()) { + settings.heating.enable = doc["heating"]["enable"].as(); + flag = true; + } + + if (!doc["heating"]["target"].isNull() && (doc["heating"]["target"].is() || doc["heating"]["target"].is())) { + settings.heating.target = round(doc["heating"]["target"].as() * 10) / 10; + flag = true; + } + + if (!doc["heating"]["hysteresis"].isNull() && (doc["heating"]["hysteresis"].is() || doc["heating"]["hysteresis"].is())) { + settings.heating.hysteresis = round(doc["heating"]["hysteresis"].as() * 10) / 10; + flag = true; + } + + // dhw + if (!doc["dhw"]["enable"].isNull() && doc["dhw"]["enable"].is()) { + settings.dhw.enable = doc["dhw"]["enable"].as(); + flag = true; + } + + if (!doc["dhw"]["target"].isNull() && doc["dhw"]["target"].is()) { + settings.dhw.target = doc["dhw"]["target"].as(); + flag = true; + } + + // pid + if (!doc["pid"]["enable"].isNull() && doc["pid"]["enable"].is()) { + settings.pid.enable = doc["pid"]["enable"].as(); + flag = true; + } + + if (!doc["pid"]["p_factor"].isNull() && (doc["pid"]["p_factor"].is() || doc["pid"]["p_factor"].is())) { + settings.pid.p_factor = round(doc["pid"]["p_factor"].as() * 1000) / 1000; + flag = true; + } + + if (!doc["pid"]["i_factor"].isNull() && (doc["pid"]["i_factor"].is() || doc["pid"]["i_factor"].is())) { + settings.pid.i_factor = round(doc["pid"]["i_factor"].as() * 1000) / 1000; + flag = true; + } + + if (!doc["pid"]["d_factor"].isNull() && (doc["pid"]["d_factor"].is() || doc["pid"]["d_factor"].is())) { + settings.pid.d_factor = round(doc["pid"]["d_factor"].as() * 1000) / 1000; + flag = true; + } + + // equitherm + if (!doc["equitherm"]["enable"].isNull() && doc["equitherm"]["enable"].is()) { + settings.equitherm.enable = doc["equitherm"]["enable"].as(); + flag = true; + } + + if (!doc["equitherm"]["n_factor"].isNull() && (doc["equitherm"]["n_factor"].is() || doc["equitherm"]["n_factor"].is())) { + settings.equitherm.n_factor = round(doc["equitherm"]["n_factor"].as() * 1000) / 1000; + flag = true; + } + + if (!doc["equitherm"]["k_factor"].isNull() && (doc["equitherm"]["k_factor"].is() || doc["equitherm"]["k_factor"].is())) { + settings.equitherm.k_factor = round(doc["equitherm"]["k_factor"].as() * 1000) / 1000; + flag = true; + } + + if (!doc["equitherm"]["t_factor"].isNull() && (doc["equitherm"]["t_factor"].is() || doc["equitherm"]["t_factor"].is())) { + settings.equitherm.t_factor = round(doc["equitherm"]["t_factor"].as() * 1000) / 1000; + flag = true; + } + + if (flag) { + eeSettings.update(); + publish(true); + + return true; + } + + return false; + } + + static bool updateVariables(const JsonDocument& doc) { + bool flag = false; + + if (!doc["ping"].isNull() && doc["ping"]) { + flag = true; + } + + if (!doc["tuning"]["enable"].isNull() && doc["tuning"]["enable"].is()) { + vars.tuning.enable = doc["tuning"]["enable"].as(); + flag = true; + } + + if (!doc["tuning"]["regulator"].isNull() && doc["tuning"]["regulator"].is() && doc["tuning"]["regulator"] >= 0 && doc["tuning"]["regulator"] <= 1) { + vars.tuning.regulator = doc["tuning"]["regulator"]; + flag = true; + } + + if (!doc["temperatures"]["indoor"].isNull() && (doc["temperatures"]["indoor"].is() || doc["temperatures"]["indoor"].is())) { + vars.temperatures.indoor = round(doc["temperatures"]["indoor"].as() * 100) / 100; + flag = true; + } + + if (!doc["temperatures"]["outdoor"].isNull() && (doc["temperatures"]["outdoor"].is() || doc["temperatures"]["outdoor"].is()) && settings.outdoorTempSource == 1) { + vars.temperatures.outdoor = round(doc["temperatures"]["outdoor"].as() * 100) / 100; + flag = true; + } + + if (!doc["restart"].isNull() && doc["restart"].is() && doc["restart"]) { + eeSettings.updateNow(); + ESP.restart(); + } + + if (flag) { + publish(true); + + return true; + } + + return false; + } + + static void publish(bool force = false) { + static unsigned int prevPubVars = 0; + static unsigned int prevPubSettings = 0; + + // publish variables and status + if (force || millis() - prevPubVars > settings.mqtt.interval) { + publishVariables(getTopicPath("state").c_str()); + + if (vars.states.fault) { + client.publish(getTopicPath("status").c_str(), "fault"); + } else { + client.publish(getTopicPath("status").c_str(), vars.states.otStatus ? "online" : "offline"); + } + + forceARP(); + prevPubVars = millis(); + } + + // publish settings + if (force || millis() - prevPubSettings > settings.mqtt.interval * 10) { + publishSettings(getTopicPath("settings").c_str()); + prevPubSettings = millis(); + } + } + + static void publishHaEntities() { + // main + haHelper.publishSelectOutdoorTempSource(); + haHelper.publishSwitchDebug(false); + + // emergency + haHelper.publishSwitchEmergency(); + haHelper.publishNumberEmergencyTarget(); + haHelper.publishSwitchEmergencyUseEquitherm(); + + // heating + haHelper.publishSwitchHeating(false); + //haHelper.publishNumberHeatingTarget(false); + haHelper.publishNumberHeatingHysteresis(); + haHelper.publishSensorHeatingSetpoint(false); + + // dhw + haHelper.publishSwitchDHW(false); + //haHelper.publishNumberDHWTarget(false); + + // pid + haHelper.publishSwitchPID(); + haHelper.publishNumberPIDFactorP(); + haHelper.publishNumberPIDFactorI(); + haHelper.publishNumberPIDFactorD(); + + // equitherm + haHelper.publishSwitchEquitherm(); + haHelper.publishNumberEquithermFactorN(); + haHelper.publishNumberEquithermFactorK(); + haHelper.publishNumberEquithermFactorT(); + + // tuning + haHelper.publishSwitchTuning(); + haHelper.publishSelectTuningRegulator(); + + // states + haHelper.publishBinSensorStatus(); + haHelper.publishBinSensorOtStatus(); + haHelper.publishBinSensorHeating(); + haHelper.publishBinSensorDHW(); + haHelper.publishBinSensorFlame(); + haHelper.publishBinSensorFault(); + haHelper.publishBinSensorDiagnostic(); + haHelper.publishSensorFaultCode(); + + // sensors + haHelper.publishSensorModulation(false); + haHelper.publishSensorPressure(false); + + // temperatures + haHelper.publishNumberIndoorTemp(); + //haHelper.publishNumberOutdoorTemp(); + haHelper.publishSensorHeatingTemp(); + haHelper.publishSensorDHWTemp(); + } + + static bool publishNonStaticHaEntities(bool force = false) { + static byte _heatingMinTemp; + static byte _heatingMaxTemp; + static byte _dhwMinTemp; + static byte _dhwMaxTemp; + static bool _editableOutdoorTemp; + + bool published = false; + bool isStupidMode = !settings.pid.enable && !settings.equitherm.enable; + byte heatingMinTemp = isStupidMode ? vars.parameters.heatingMinTemp : 10; + byte heatingMaxTemp = isStupidMode ? vars.parameters.heatingMaxTemp : 30; + bool editableOutdoorTemp = settings.outdoorTempSource == 1; + + + if (force || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) { + if (settings.heating.target < heatingMinTemp || settings.heating.target > heatingMaxTemp) { + settings.heating.target = constrain(settings.heating.target, heatingMinTemp, heatingMaxTemp); + } + + _heatingMinTemp = heatingMinTemp; + _heatingMaxTemp = heatingMaxTemp; + + haHelper.publishNumberHeatingTarget(heatingMinTemp, heatingMaxTemp, false); + haHelper.publishClimateHeating(heatingMinTemp, heatingMaxTemp); + + published = true; + } + + if (force || _dhwMinTemp != vars.parameters.dhwMinTemp || _dhwMaxTemp != vars.parameters.dhwMaxTemp) { + _dhwMinTemp = vars.parameters.dhwMinTemp; + _dhwMaxTemp = vars.parameters.dhwMaxTemp; + + haHelper.publishNumberDHWTarget(vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp, false); + haHelper.publishClimateDHW(vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp); + + published = true; + } + + if (force || _editableOutdoorTemp != editableOutdoorTemp) { + _editableOutdoorTemp = editableOutdoorTemp; + + if (editableOutdoorTemp) { + haHelper.deleteSensorOutdoorTemp(); + haHelper.publishNumberOutdoorTemp(); + } else { + haHelper.deleteNumberOutdoorTemp(); + haHelper.publishSensorOutdoorTemp(); + } + + published = true; + } + + return published; + } + + static bool publishSettings(const char* topic) { + StaticJsonDocument<2048> doc; + + doc["debug"] = settings.debug; + doc["outdoorTempSource"] = settings.outdoorTempSource; + + doc["emergency"]["enable"] = settings.emergency.enable; + doc["emergency"]["target"] = settings.emergency.target; + doc["emergency"]["useEquitherm"] = settings.emergency.useEquitherm; + + doc["heating"]["enable"] = settings.heating.enable; + doc["heating"]["target"] = settings.heating.target; + doc["heating"]["hysteresis"] = settings.heating.hysteresis; + + doc["dhw"]["enable"] = settings.dhw.enable; + doc["dhw"]["target"] = settings.dhw.target; + + doc["pid"]["enable"] = settings.pid.enable; + doc["pid"]["p_factor"] = settings.pid.p_factor; + doc["pid"]["i_factor"] = settings.pid.i_factor; + doc["pid"]["d_factor"] = settings.pid.d_factor; + + doc["equitherm"]["enable"] = settings.equitherm.enable; + doc["equitherm"]["n_factor"] = settings.equitherm.n_factor; + doc["equitherm"]["k_factor"] = settings.equitherm.k_factor; + doc["equitherm"]["t_factor"] = settings.equitherm.t_factor; + + client.beginPublish(topic, measureJson(doc), false); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + static bool publishVariables(const char* topic) { + StaticJsonDocument<2048> doc; + + doc["tuning"]["enable"] = vars.tuning.enable; + doc["tuning"]["regulator"] = vars.tuning.regulator; + + doc["states"]["otStatus"] = vars.states.otStatus; + doc["states"]["heating"] = vars.states.heating; + doc["states"]["dhw"] = vars.states.dhw; + doc["states"]["flame"] = vars.states.flame; + doc["states"]["fault"] = vars.states.fault; + doc["states"]["diagnostic"] = vars.states.diagnostic; + doc["states"]["faultCode"] = vars.states.faultCode; + + doc["sensors"]["modulation"] = vars.sensors.modulation; + doc["sensors"]["pressure"] = vars.sensors.pressure; + + doc["temperatures"]["indoor"] = vars.temperatures.indoor; + doc["temperatures"]["outdoor"] = vars.temperatures.outdoor; + doc["temperatures"]["heating"] = vars.temperatures.heating; + doc["temperatures"]["dhw"] = vars.temperatures.dhw; + + doc["parameters"]["heatingMinTemp"] = vars.parameters.heatingMinTemp; + doc["parameters"]["heatingMaxTemp"] = vars.parameters.heatingMaxTemp; + doc["parameters"]["heatingSetpoint"] = vars.parameters.heatingSetpoint; + doc["parameters"]["dhwMinTemp"] = vars.parameters.dhwMinTemp; + doc["parameters"]["dhwMaxTemp"] = vars.parameters.dhwMaxTemp; + + client.beginPublish(topic, measureJson(doc), false); + //BufferingPrint bufferedClient(client, 32); + //serializeJson(doc, bufferedClient); + //bufferedClient.flush(); + serializeJson(doc, client); + return client.endPublish(); + } + + static std::string getTopicPath(const char* topic) { + return std::string(settings.mqtt.prefix) + "/" + std::string(topic); + } + + static void __callback(char* topic, byte* payload, unsigned int length) { + if (!length) { + return; + } + + if (settings.debug) { + DEBUG_F("MQTT received message\n\r Topic: %s\n\r Data: ", topic); + for (int i = 0; i < length; i++) { + DEBUG_STREAM.print((char)payload[i]); + } + DEBUG_STREAM.print("\n"); + } + + StaticJsonDocument<2048> doc; + DeserializationError dErr = deserializeJson(doc, (const byte*)payload, length); + if (dErr != DeserializationError::Ok || doc.isNull()) { + return; + } + + if (getTopicPath("state/set").compare(topic) == 0) { + updateVariables(doc); + client.publish(getTopicPath("state/set").c_str(), NULL, true); + + } else if (getTopicPath("settings/set").compare(topic) == 0) { + updateSettings(doc); + client.publish(getTopicPath("settings/set").c_str(), NULL, true); + } + } +}; \ No newline at end of file diff --git a/src/OpenThermTask.h b/src/OpenThermTask.h new file mode 100644 index 0000000..6ae8a8d --- /dev/null +++ b/src/OpenThermTask.h @@ -0,0 +1,542 @@ +#include "lib/CustomOpenTherm.h" + +CustomOpenTherm ot(OPENTHERM_IN_PIN, OPENTHERM_OUT_PIN); + +class OpenThermTask : public CustomTask { +public: + OpenThermTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {} + +protected: + void setup() { + ot.begin(handleInterrupt, responseCallback); + ot.setHandleSendRequestCallback(sendRequestCallback); + } + + void loop() { + static byte currentHeatingTemp, currentDHWTemp = 0; + byte newHeatingTemp, newDHWTemp = 0; + unsigned long localResponse; + + setMasterMemberIdCode(); + DEBUG_F("Slave member id code: %u \n", vars.parameters.slaveMemberIdCode); + + localResponse = ot.setBoilerStatus( + settings.heating.enable && pump, + settings.dhw.enable + ); + + if (!ot.isValidResponse(localResponse)) { + return; + } + + vars.states.heating = ot.isCentralHeatingActive(localResponse); + vars.states.dhw = ot.isHotWaterActive(localResponse); + vars.states.flame = ot.isFlameOn(localResponse); + vars.states.fault = ot.isFault(localResponse); + vars.states.diagnostic = ot.isDiagnostic(localResponse); + + /*if (vars.dump_request.value) + { + testSupportedIDs(); + vars.dump_request.value = false; + }*/ + + + + /*if ( ot.isValidResponse(localResponse) ) { + vars.SlaveMemberIDcode.value = localResponse >> 0 & 0xFF; + uint8_t flags = (localResponse & 0xFFFF) >> 8 & 0xFF; + vars.dhw_present.value = flags & 0x01; + vars.control_type.value = flags & 0x02; + vars.cooling_present.value = flags & 0x04; + vars.dhw_tank_present.value = flags & 0x08; + vars.pump_control_present.value = flags & 0x10; + vars.ch2_present.value = flags & 0x20; + }*/ + + // Команды чтения данных котла + if (millis() - prevUpdateNonEssentialVars > 30000) { + updateSlaveParameters(); + updateMasterParameters(); + // crash? + DEBUG_F("Master type: %u, version: %u \n", vars.parameters.masterType, vars.parameters.masterVersion); + DEBUG_F("Slave type: %u, version: %u \n", vars.parameters.slaveType, vars.parameters.slaveVersion); + + updateMinMaxDhwTemp(); + updateMinMaxHeatingTemp(); + if (settings.outdoorTempSource == 0) { + updateOutsideTemp(); + } + if (vars.states.fault) { + updateFaultCode(); + } + updatePressure(); + + prevUpdateNonEssentialVars = millis(); + } + updateHeatingTemp(); + updateDHWTemp(); + updateModulationLevel(); + + // + // Температура ГВС + newDHWTemp = settings.dhw.target; + if (newDHWTemp != currentDHWTemp) { + if (newDHWTemp < vars.parameters.dhwMinTemp || newDHWTemp > vars.parameters.dhwMaxTemp) { + newDHWTemp = constrain(newDHWTemp, vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp); + } + + INFO_F("Set DHW temp = %u \n", newDHWTemp); + + // Записываем заданную температуру ГВС + if (ot.setDHWSetpoint(newDHWTemp)) { + currentDHWTemp = newDHWTemp; + } + } + + // + // Температура отопления + if (fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001) { + INFO_F("Set heating temp = %u \n", vars.parameters.heatingSetpoint); + + // Записываем заданную температуру + if (ot.setBoilerTemperature(vars.parameters.heatingSetpoint)) { + currentHeatingTemp = vars.parameters.heatingSetpoint; + } + } + + // коммутационная разность (hysteresis) + // только для pid и/или equitherm + if (settings.heating.hysteresis > 0 && !vars.states.emergency && (settings.equitherm.enable || settings.pid.enable)) { + float halfHyst = settings.heating.hysteresis / 2; + if (pump && vars.temperatures.indoor - settings.heating.target + 0.0001 >= halfHyst) { + pump = false; + } else if (!pump && vars.temperatures.indoor - settings.heating.target - 0.0001 <= -(halfHyst)) { + pump = true; + } + } else if (!pump) { + pump = true; + } + } + + void static IRAM_ATTR handleInterrupt() { + ot.handleInterrupt(); + } + + void static sendRequestCallback(unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) { + printRequestDetail(ot.getDataID(request), status, request, response, attempt); + } + + void static responseCallback(unsigned long result, OpenThermResponseStatus status) { + static byte attempt = 0; + switch (status) { + case OpenThermResponseStatus::TIMEOUT: + if (++attempt > OPENTHERM_OFFLINE_TRESHOLD) { + vars.states.otStatus = false; + attempt = OPENTHERM_OFFLINE_TRESHOLD; + } + break; + case OpenThermResponseStatus::SUCCESS: + attempt = 0; + vars.states.otStatus = true; + break; + default: + break; + } + } + +protected: + bool pump = true; + unsigned long prevUpdateNonEssentialVars = 0; + + void static printRequestDetail(OpenThermMessageID id, OpenThermResponseStatus status, unsigned long request, unsigned long response, byte attempt) { + sprintf(buffer, "OT REQUEST ID: %4d Request: %8x Response: %8x Attempt: %2d Status: %s", id, request, response, attempt, ot.statusToString(status)); + if (status != OpenThermResponseStatus::SUCCESS) { + //WARN(buffer); + DEBUG(buffer); + } else { + DEBUG(buffer); + } + } + + /* + bool getBoilerTemp() + { + unsigned long response; + return sendRequest(ot.buildGetBoilerTemperatureRequest(),response); + } + + bool getDHWTemp() + { + unsigned long response; + unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tdhw, 0); + return sendRequest(request,response); + } + + bool getOutsideTemp() + { + unsigned long response; + unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0); + return sendRequest(request,response); + } + + bool setDHWTemp(float val) + { + unsigned long request = ot.buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::TdhwSet, ot.temperatureToData(val)); + unsigned long response; + return sendRequest(request,response); + } + + bool getFaultCode() + { + unsigned long response; + unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0); + return sendRequest(request,response); + } + + bool getModulationLevel() { + unsigned long response; + unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0); + return sendRequest(request,response); + } + + bool getPressure() { + unsigned long response; + unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0); + return sendRequest(request,response); + } + + bool sendRequest(unsigned long request, unsigned long& response) + { + send_newts = millis(); + if (send_newts - send_ts < 200) { + // Преждем чем слать что то - надо подождать 100ms согласно специфиикации протокола ОТ + delay(200 - (send_newts - send_ts)); + } + + bool result = ot.sendRequestAync(request); + if(!result) { + WARN("Не могу отправить запрос"); + WARN("Шина " + ot.isReady() ? "готова" : "не готова"); + return false; + } + while (!ot.isReady()) + { + ot.process(); + yield(); // This is local Task yield() call which allow us to switch to another task in scheduler + } + send_ts = millis(); + response = ot_response; + //printRequestDetail(ot.getDataID(request), request, response); + + return true; // Response is global variable + } + + void testSupportedIDs() + { + // Basic + unsigned long request; + unsigned long response; + OpenThermMessageID id; + //Command + id = OpenThermMessageID::Command; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + //ASFlags + id = OpenThermMessageID::ASFflags; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //TrOverride + id = OpenThermMessageID::TrOverride; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //TSP + id = OpenThermMessageID::TSP; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //TSPindexTSPvalue + id = OpenThermMessageID::TSPindexTSPvalue; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //FHBsize + id = OpenThermMessageID::FHBsize; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //FHBindexFHBvalue + id = OpenThermMessageID::FHBindexFHBvalue; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //MaxCapacityMinModLevel + id = OpenThermMessageID::MaxCapacityMinModLevel; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //TrSet + id = OpenThermMessageID::TrSet; + request = ot.buildRequest(OpenThermRequestType::WRITE, id, ot.temperatureToData(21)); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //RelModLevel + id = OpenThermMessageID::RelModLevel; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //CHPressure + id = OpenThermMessageID::CHPressure; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //DHWFlowRate + id = OpenThermMessageID::DHWFlowRate; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //DayTime + id = OpenThermMessageID::DayTime; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //Date + id = OpenThermMessageID::Date; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //Year + id = OpenThermMessageID::Year; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //TrSetCH2 + id = OpenThermMessageID::TrSetCH2; + request = ot.buildRequest(OpenThermRequestType::WRITE, id, ot.temperatureToData(21)); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //Tr + id = OpenThermMessageID::Tr; + request = ot.buildRequest(OpenThermRequestType::WRITE, id, ot.temperatureToData(21)); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //Tret + id = OpenThermMessageID::Tret; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //Texhaust + id = OpenThermMessageID::Texhaust; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //Hcratio + id = OpenThermMessageID::Hcratio; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //RemoteOverrideFunction + id = OpenThermMessageID::RemoteOverrideFunction; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //OEMDiagnosticCode + id = OpenThermMessageID::OEMDiagnosticCode; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //BurnerStarts + id = OpenThermMessageID::BurnerStarts; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //CHPumpStarts + id = OpenThermMessageID::CHPumpStarts; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //DHWPumpValveStarts + id = OpenThermMessageID::DHWPumpValveStarts; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //DHWBurnerStarts + id = OpenThermMessageID::DHWBurnerStarts; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //BurnerOperationHours + id = OpenThermMessageID::BurnerOperationHours; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //CHPumpOperationHours + id = OpenThermMessageID::CHPumpOperationHours; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //DHWPumpValveOperationHours + id = OpenThermMessageID::DHWPumpValveOperationHours; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + + //DHWBurnerOperationHours + id = OpenThermMessageID::DHWBurnerOperationHours; + request = ot.buildRequest(OpenThermRequestType::READ, id, 0); + if(sendRequest(request,response)) + printRequestDetail(id, ot.getLastResponseStatus(), request, response); + } + */ + + void setMasterMemberIdCode() { + //======================================================================================= + // Эта группа элементов данных определяет информацию о конфигурации как на ведомых, так + // и на главных сторонах. Каждый из них имеет группу флагов конфигурации (8 бит) + // и код MemberID (1 байт). Перед передачей информации об управлении и состоянии + // рекомендуется обмен сообщениями о допустимой конфигурации ведомого устройства + // чтения и основной конфигурации записи. Нулевой код MemberID означает клиентское + // неспецифическое устройство. Номер/тип версии продукта следует использовать в сочетании + // с "кодом идентификатора участника", который идентифицирует производителя устройства. + //======================================================================================= + + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SConfigSMemberIDcode, 0)); // 0xFFFF + if (!ot.isValidResponse(response)) { + return; + } + + vars.parameters.slaveMemberIdCode = response >> 0 & 0xFF; + ot.sendRequest(ot.buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MConfigMMemberIDcode, vars.parameters.slaveMemberIdCode)); + } + + void updateMasterParameters() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MasterVersion, 0x013F)); + if (!ot.isValidResponse(response)) { + return; + } + + vars.parameters.masterType = (response & 0xFFFF) >> 8; + vars.parameters.masterVersion = response & 0xFF; + } + + void updateSlaveParameters() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SlaveVersion, 0)); + if (!ot.isValidResponse(response)) { + return; + } + + vars.parameters.slaveType = (response & 0xFFFF) >> 8; + vars.parameters.slaveVersion = response & 0xFF; + } + + void updateMinMaxDhwTemp() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0)); + + if (!ot.isValidResponse(response)) { + return; + } + + byte minTemp = response & 0xFF; + byte maxTemp = (response & 0xFFFF) >> 8; + + if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) { + vars.parameters.dhwMinTemp = minTemp; + vars.parameters.dhwMaxTemp = maxTemp; + } + } + + void updateMinMaxHeatingTemp() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0)); + + if (!ot.isValidResponse(response)) { + return; + } + + byte minTemp = response & 0xFF; + byte maxTemp = (response & 0xFFFF) >> 8; + + if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) { + vars.parameters.heatingMinTemp = minTemp; + vars.parameters.heatingMaxTemp = maxTemp; + } + } + + void updateOutsideTemp() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0)); + + if (ot.isValidResponse(response)) { + vars.temperatures.outdoor = ot.getFloat(response); + } + } + + void updateHeatingTemp() { + unsigned long response = ot.sendRequest(ot.buildGetBoilerTemperatureRequest()); + + if (ot.isValidResponse(response)) { + vars.temperatures.heating = ot.getFloat(response); + } + } + + + void updateDHWTemp() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0)); + + if (ot.isValidResponse(response)) { + vars.temperatures.dhw = ot.getFloat(response); + } + } + + void updateFaultCode() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)); + + if (ot.isValidResponse(response)) { + vars.states.faultCode = response & 0xFF; + } + } + + void updateModulationLevel() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0)); + + if (ot.isValidResponse(response)) { + vars.sensors.modulation = ot.getFloat(response); + } + } + + void updatePressure() { + unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0)); + + if (ot.isValidResponse(response)) { + vars.sensors.pressure = ot.getFloat(response); + } + } +}; diff --git a/src/RegulatorTask.h b/src/RegulatorTask.h new file mode 100644 index 0000000..91f23e0 --- /dev/null +++ b/src/RegulatorTask.h @@ -0,0 +1,240 @@ +#include "lib/Equitherm.h" +#include +#include + +Equitherm etRegulator; +GyverPID pidRegulator(0, 0, 0, 10000); +PIDtuner pidTuner; + +class RegulatorTask : public MiniTask { +public: + RegulatorTask(bool enabled = false, unsigned long interval = 0) : MiniTask(enabled, interval) {} + +protected: + bool tunerInit = false; + byte tunerState = 0; + float prevHeatingTarget = 0; + float prevEtResult = 0; + float prevPidResult = 0; + + + void setup() {} + void loop() { + byte newTemp; + + if (vars.states.emergency) { + newTemp = getEmergencyModeTemp(); + } else { + newTemp = getNormalModeTemp(); + } + + // Ограничиваем, если до этого не ограничило + if (newTemp < vars.parameters.heatingMinTemp || newTemp > vars.parameters.heatingMaxTemp) { + newTemp = constrain(newTemp, vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); + } + + if (abs(vars.parameters.heatingSetpoint - newTemp) + 0.0001 >= 1) { + vars.parameters.heatingSetpoint = newTemp; + } + } + + + byte getEmergencyModeTemp() { + byte newTemp = vars.parameters.heatingSetpoint; + + // if use equitherm + if (settings.emergency.useEquitherm && settings.outdoorTempSource != 1) { + etRegulator.Kn = settings.equitherm.n_factor; + etRegulator.Kk = settings.equitherm.k_factor; + etRegulator.Kt = 0; + etRegulator.indoorTemp = 0; + etRegulator.outdoorTemp = vars.temperatures.outdoor; + + etRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); + etRegulator.targetTemp = settings.emergency.target; + + float etResult = etRegulator.getResult(); + if (fabs(prevEtResult - etResult) + 0.0001 >= 1) { + prevEtResult = etResult; + newTemp = round(etResult); + + INFO_F("New emergency equitherm result: %u (%f) \n", newTemp, etResult); + } + + } else { + // default temp, manual mode + newTemp = round(settings.emergency.target); + } + + return newTemp; + } + + byte getNormalModeTemp() { + bool updateIntegral = false; + byte newTemp = vars.parameters.heatingSetpoint; + + if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) { + prevHeatingTarget = settings.heating.target; + updateIntegral = true; + + INFO_F("New heating target: %f \n", settings.heating.target); + } + + // if use equitherm + if (settings.equitherm.enable) { + if (vars.tuning.enable && vars.tuning.regulator == 0) { + if (settings.pid.enable) { + settings.pid.enable = false; + } + + etRegulator.Kn = tuneEquithermN(etRegulator.Kn, vars.temperatures.indoor, settings.heating.target, 300, 1800, 0.01, 1); + } else { + etRegulator.Kn = settings.equitherm.n_factor; + } + + if (settings.pid.enable) { + etRegulator.Kt = 0; + etRegulator.indoorTemp = round(vars.temperatures.indoor); + etRegulator.outdoorTemp = round(vars.temperatures.outdoor); + } else { + etRegulator.Kt = settings.equitherm.t_factor; + etRegulator.indoorTemp = vars.temperatures.indoor; + etRegulator.outdoorTemp = vars.temperatures.outdoor; + } + + etRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); + etRegulator.Kk = settings.equitherm.k_factor; + etRegulator.targetTemp = settings.heating.target; + + float etResult = etRegulator.getResult(); + if (fabs(prevEtResult - etResult) + 0.0001 >= 1) { + prevEtResult = etResult; + updateIntegral = true; + newTemp = round(etResult); + + INFO_F("New equitherm result: %u (%f) \n", newTemp, etResult); + + } else { + updateIntegral = false; + } + } + + // if use pid + if (settings.pid.enable && tunerInit && (!vars.tuning.enable || vars.tuning.regulator != 1)) { + pidTuner.reset(); + tunerState = 0; + tunerInit = false; + INFO(F("Tuning stopped")); + + } else if (settings.pid.enable && vars.tuning.enable && vars.tuning.regulator == 1) { + if (tunerInit && pidTuner.getState() == 3) { + INFO(F("Tuning finished")); + pidTuner.debugText(&INFO_STREAM); + + if (pidTuner.getAccuracy() < 90) { + WARN(F("Tuning bad result, restart...")); + + } else { + settings.pid.p_factor = pidTuner.getPID_p(); + settings.pid.i_factor = pidTuner.getPID_i(); + settings.pid.d_factor = pidTuner.getPID_d(); + vars.tuning.enable = false; + } + + pidTuner.reset(); + tunerState = 0; + tunerInit = false; + + } else { + if (!tunerInit) { + INFO(F("Tuning start")); + + float step; + if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) { + step = ceil(vars.parameters.heatingSetpoint / vars.temperatures.indoor * 2); + } else { + step = 5.0f; + } + + float startTemp = newTemp + step; + if (startTemp >= vars.parameters.heatingMaxTemp) { + startTemp = vars.parameters.heatingMaxTemp - 10; + } + + INFO_F("Tuning started. Start temp: %f, step: %f \n", startTemp, step); + pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000); + tunerInit = true; + } + + pidTuner.setInput(vars.temperatures.indoor); + pidTuner.compute(); + + if (tunerState > 0 && pidTuner.getState() != tunerState) { + INFO(F("Tuning log:")); + pidTuner.debugText(&INFO_STREAM); + tunerState = pidTuner.getState(); + } + + newTemp = round(pidTuner.getOutput()); + } + } + + if (settings.pid.enable && (!vars.tuning.enable || vars.tuning.enable && vars.tuning.regulator != 1)) { + if (updateIntegral) { + pidRegulator.integral = settings.heating.target; + } + + pidRegulator.Kp = settings.pid.p_factor; + pidRegulator.Ki = settings.pid.i_factor; + pidRegulator.Kd = settings.pid.d_factor; + + pidRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); + pidRegulator.input = vars.temperatures.indoor; + pidRegulator.setpoint = settings.heating.target; + + float pidResult = pidRegulator.getResultTimer(); + if (abs(prevPidResult - pidResult) >= 0.5) { + prevPidResult = pidResult; + newTemp = round(pidResult); + + INFO_F("New PID result: %u (%f) \n", newTemp, pidResult); + } + } + + // default temp, manual mode + if (!settings.equitherm.enable && !settings.pid.enable) { + newTemp = round(settings.heating.target); + } + + return newTemp; + } + + + float tuneEquithermN(float ratio, float currentTemp, float setTemp, unsigned int dirtyInterval = 60, unsigned int accurateInterval = 1800, float accurateStep = 0.01, float accurateStepAfter = 1) { + static uint32_t _prevIteration = millis(); + + if (abs(currentTemp - setTemp) < accurateStepAfter) { + if (millis() - _prevIteration < (accurateInterval * 1000)) { + return ratio; + } + + if (currentTemp - setTemp > 0.1f) { + ratio -= accurateStep; + + } else if (currentTemp - setTemp < -0.1f) { + ratio += accurateStep; + } + + } else { + if (millis() - _prevIteration < (dirtyInterval * 1000)) { + return ratio; + } + + ratio = ratio * (setTemp / currentTemp); + } + + _prevIteration = millis(); + return ratio; + } + +}; diff --git a/src/SensorsTask.h b/src/SensorsTask.h new file mode 100644 index 0000000..c7951b7 --- /dev/null +++ b/src/SensorsTask.h @@ -0,0 +1,27 @@ +#include + +MicroDS18B20 outdoorSensor; + +class SensorsTask : public MiniTask { +public: + SensorsTask(bool enabled = false, unsigned long interval = 0) : MiniTask(enabled, interval) {} + +protected: + void setup() {} + + void loop() { + // DS18B20 sensor + if (outdoorSensor.online()) { + if (outdoorSensor.readTemp()) { + vars.temperatures.outdoor = outdoorSensor.getTemp(); + + } else { + DEBUG("Invalid data from outdoor sensor (DS18B20)"); + } + + outdoorSensor.requestTemp(); + } else { + WARN("Failed to connect to outdoor sensor (DS18B20)"); + } + } +}; \ No newline at end of file diff --git a/src/Settings.h b/src/Settings.h new file mode 100644 index 0000000..951d2ef --- /dev/null +++ b/src/Settings.h @@ -0,0 +1,90 @@ +struct Settings { + bool debug = false; + // 0 - boiler, 1 - manual, 2 - ds18b20 + byte outdoorTempSource = 0; + char hostname[80] = "opentherm"; + + struct { + char server[80]; + int port = 1883; + char user[32]; + char password[32]; + char prefix[80] = "opentherm"; + unsigned int interval = 5000; + } mqtt; + + struct { + bool enable = true; + float target = 40.0f; + bool useEquitherm = false; + } emergency; + + struct { + bool enable = true; + float target = 40.0f; + float hysteresis = 0.5f; + } heating; + + struct { + bool enable = true; + byte target = 40; + } dhw; + + struct { + bool enable = false; + float p_factor = 3; + float i_factor = 0.2f; + float d_factor = 0; + } pid; + + struct { + bool enable = false; + float n_factor = 0.67f; + float k_factor = 1.0f; + float t_factor = 0.0f; + } equitherm; + +} settings; + +struct Variables { + struct { + bool enable = false; + byte regulator = 0; + } tuning; + + struct { + bool otStatus = false; + bool emergency = false; + bool heating = false; + bool dhw = false; + bool flame = false; + bool fault = false; + bool diagnostic = false; + byte faultCode = 0; + } states; + + struct { + float modulation = 0.0f; + float pressure = 0.0f; + } sensors; + + struct { + float indoor = 0.0f; + float outdoor = 0.0f; + float heating = 0.0f; + float dhw = 0.0f; + } temperatures; + + struct { + byte heatingMinTemp = 20; + byte heatingMaxTemp = 90; + byte heatingSetpoint = 0.0f; + byte dhwMinTemp = 30; + byte dhwMaxTemp = 60; + uint8_t slaveMemberIdCode; + uint8_t slaveType; + uint8_t slaveVersion; + uint8_t masterType; + uint8_t masterVersion; + } parameters; +} vars; \ No newline at end of file diff --git a/src/WifiManagerTask.h b/src/WifiManagerTask.h new file mode 100644 index 0000000..2fce4e3 --- /dev/null +++ b/src/WifiManagerTask.h @@ -0,0 +1,77 @@ +// #include +#define WM_MDNS +#include +//#include +//#include + +// Wifimanager +WiFiManager wm; +WiFiManagerParameter *wmHostname; +WiFiManagerParameter *wmMqttServer; +WiFiManagerParameter *wmMqttPort; +WiFiManagerParameter *wmMqttUser; +WiFiManagerParameter *wmMqttPassword; +WiFiManagerParameter *wmMqttPrefix; + +class WifiManagerTask : public CustomTask { +public: + WifiManagerTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {} + +protected: + void setup() { + WiFi.mode(WIFI_STA); + wm.setDebugOutput(settings.debug); + + wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80); + wm.addParameter(wmHostname); + + wmMqttServer = new WiFiManagerParameter("mqtt_server", "MQTT server", settings.mqtt.server, 80); + wm.addParameter(wmMqttServer); + + //char mqttPort[6]; + sprintf(buffer, "%d", settings.mqtt.port); + wmMqttPort = new WiFiManagerParameter("mqtt_port", "MQTT port", buffer, 6); + wm.addParameter(wmMqttPort); + + wmMqttUser = new WiFiManagerParameter("mqtt_user", "MQTT username", settings.mqtt.user, 32); + wm.addParameter(wmMqttUser); + + wmMqttPassword = new WiFiManagerParameter("mqtt_password", "MQTT password", settings.mqtt.password, 32); + wm.addParameter(wmMqttPassword); + + wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "MQTT prefix", settings.mqtt.prefix, 32); + wm.addParameter(wmMqttPrefix); + + wm.setHostname(settings.hostname); + wm.setWiFiAutoReconnect(true); + wm.setConfigPortalBlocking(false); + wm.setSaveParamsCallback(saveParamsCallback); + wm.setConfigPortalTimeout(300); + wm.setDisableConfigPortal(false); + + if (wm.autoConnect(AP_SSID)) { + INFO_F("Wifi connected. IP: %s, RSSI: %d\n", WiFi.localIP().toString().c_str(), WiFi.RSSI()); + wm.startWebPortal(); + + } else { + INFO(F("Failed to connect to WIFI, start the configuration portal...")); + } + } + + void loop() { + wm.process(); + } + + void static saveParamsCallback() { + strcpy(settings.hostname, (*wmHostname).getValue()); + strcpy(settings.mqtt.server, (*wmMqttServer).getValue()); + settings.mqtt.port = atoi((*wmMqttPort).getValue()); + strcpy(settings.mqtt.user, (*wmMqttUser).getValue()); + strcpy(settings.mqtt.password, (*wmMqttPassword).getValue()); + strcpy(settings.mqtt.prefix, (*wmMqttPrefix).getValue()); + + INFO_F("Settings\nHostname: %s, Server: %s, port: %d, user: %s, pass: %s\n", settings.hostname, settings.mqtt.server, settings.mqtt.port, settings.mqtt.user, settings.mqtt.password); + eeSettings.updateNow(); + INFO(F("Settings saved")); + } +}; \ No newline at end of file diff --git a/src/defines.h b/src/defines.h new file mode 100644 index 0000000..0f2918b --- /dev/null +++ b/src/defines.h @@ -0,0 +1,43 @@ +#define OT_GATEWAY_VERSION "1.0.4" +#define AP_SSID "OpenTherm Gateway" +//#define USE_TELNET + +#define EMERGENCY_TRESHOLD 10 +#define MQTT_RECONNECT_INTERVAL 5000 +#define MQTT_KEEPALIVE 30 + +#define OPENTHERM_IN_PIN 4 +#define OPENTHERM_OUT_PIN 5 +#define OPENTHERM_OFFLINE_TRESHOLD 10 + +#define DS18B20_PIN 2 +#define DS18B20_INTERVAL 1000 +#define DS_CHECK_CRC true +#define DS_CRC_USE_TABLE true + +#define CONFIG_URL "http://%s/" + + +#ifdef USE_TELNET + #define INFO_STREAM TelnetStream + #define WARN_STREAM TelnetStream + #define ERROR_STREAM TelnetStream + #define DEBUG_STREAM if (settings.debug) TelnetStream + #define WM_DEBUG_PORT TelnetStream +#else + #define INFO_STREAM Serial + #define WARN_STREAM Serial + #define ERROR_STREAM Serial + #define DEBUG_STREAM if (settings.debug) Serial + #define WM_DEBUG_PORT Serial +#endif + +#define INFO(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.println(__VA_ARGS__); +#define INFO_F(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.printf(__VA_ARGS__); +#define WARN(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.println(__VA_ARGS__); +#define WARN_F(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.printf(__VA_ARGS__); +#define ERROR(...) ERROR_STREAM.print("\r[ERROR] "); ERROR_STREAM.println(__VA_ARGS__); +#define DEBUG(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.println(__VA_ARGS__); +#define DEBUG_F(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.printf(__VA_ARGS__); + +char buffer[120]; \ No newline at end of file diff --git a/src/lib/CustomOpenTherm.h b/src/lib/CustomOpenTherm.h new file mode 100644 index 0000000..29a1f41 --- /dev/null +++ b/src/lib/CustomOpenTherm.h @@ -0,0 +1,46 @@ +#include + +extern SchedulerClass Scheduler; + +class CustomOpenTherm : public OpenTherm { +private: + unsigned long send_ts = millis(); + void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt); + +public: + CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {} + void setHandleSendRequestCallback(void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt)) { + this->handleSendRequestCallback = handleSendRequestCallback; + } + + unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) { + _attempt++; + while (send_ts > 0 && millis() - send_ts < 200) { + Scheduler.yield(); + } + + //unsigned long response = OpenTherm::sendRequest(request); + unsigned long _response; + if (!sendRequestAync(request)) { + _response = 0; + } else { + while (!isReady()) { + Scheduler.yield(); + process(); + } + + _response = getLastResponse(); + } + + if (handleSendRequestCallback != NULL) { + handleSendRequestCallback(request, _response, getLastResponseStatus(), _attempt); + } + + send_ts = millis(); + if (getLastResponseStatus() == OpenThermResponseStatus::SUCCESS || _attempt >= attempts) { + return _response; + } else { + return sendRequest(request, attempts, _attempt); + } + } +}; diff --git a/src/lib/CustomTask.h b/src/lib/CustomTask.h new file mode 100644 index 0000000..ff42cd3 --- /dev/null +++ b/src/lib/CustomTask.h @@ -0,0 +1,45 @@ +class CustomTask : public Task { +public: + CustomTask(bool enabled = false, unsigned long interval = 0) { + _enabled = enabled; + _interval = interval; + } + + bool isEnabled() { + return _enabled; + } + + void enable() { + _enabled = true; + } + + void disable() { + _enabled = false; + } + + void setInterval(unsigned long val) { + _interval = val; + } + + unsigned long getInterval() { + return _interval; + } + +protected: + bool _enabled = true; + unsigned long _lastRun = 0; + unsigned long _interval = 0; + + bool shouldRun() { + if (!_enabled || !Task::shouldRun()) { + return false; + } + + if (_interval > 0 && millis() - _lastRun < _interval) { + return false; + } + + _lastRun = millis(); + return true; + } +}; \ No newline at end of file diff --git a/src/lib/Equitherm.h b/src/lib/Equitherm.h new file mode 100644 index 0000000..4e7fc7c --- /dev/null +++ b/src/lib/Equitherm.h @@ -0,0 +1,63 @@ +#include + +#if defined(EQUITHERM_INTEGER) +// расчёты с целыми числами +typedef int datatype; +#else +// расчёты с float числами +typedef float datatype; +#endif + +class Equitherm { +public: + Equitherm() {} + + // kn, kk, kt + Equitherm(float new_kn, float new_kk, float new_kt) { + Kn = new_kn; + Kk = new_kk; + Kt = new_kt; + } + + // лимит выходной величины + void setLimits(int min_output, int max_output) { + _minOut = min_output; + _maxOut = max_output; + } + + datatype targetTemp = 0; + datatype indoorTemp = 0; + datatype outdoorTemp = 0; + float Kn = 0.0; + float Kk = 0.0; + float Kt = 0.0; + + // возвращает новое значение при вызове + datatype getResult() { + datatype output = getResultN() + getResultK() + getResultT(); + output = constrain(output, _minOut, _maxOut); // ограничиваем выход + return output; + } + + // температура контура отопления в зависимости от наружной температуры + datatype getResultN() { + float a = (-0.21 * Kn) - 0.06; // a = -0,21k — 0,06 + float b = (6.04 * Kn) + 1.98; // b = 6,04k + 1,98 + float c = (-5.06 * Kn) + 18.06; // с = -5,06k + 18,06 + float x = (-0.2 * outdoorTemp) + 5; // x = -0.2*t1 + 5 + return (a * x * x) + (b * x) + c; // Tn = ax2 + bx + c + } + + // поправка на желаемую комнатную температуру + datatype getResultK() { + return (targetTemp - 20) * Kk; + } + + // Расчет поправки (ошибки) термостата + datatype getResultT() { + return (targetTemp - indoorTemp) * Kt; + } + +private: + int _minOut = 20, _maxOut = 90; +}; \ No newline at end of file diff --git a/src/lib/MiniTask.h b/src/lib/MiniTask.h new file mode 100644 index 0000000..81b1d9a --- /dev/null +++ b/src/lib/MiniTask.h @@ -0,0 +1,64 @@ +class MiniTask { +public: + MiniTask(bool enabled = false, unsigned long interval = 0) { + _enabled = enabled; + _interval = interval; + } + + bool isEnabled() { + return _enabled; + } + + void enable() { + _enabled = true; + } + + void disable() { + _enabled = false; + } + + void setInterval(unsigned long val) { + _interval = val; + } + + unsigned long getInterval() { + return _interval; + } + + void loopWrapper() { + if (!shouldRun()) { + return; + } + + if (!_setupDone) { + setup(); + _setupDone = true; + } + + loop(); + yield(); + } + +protected: + virtual void setup() {} + virtual void loop() {} + + virtual bool shouldRun() { + if (!_enabled) { + return false; + } + + if (_interval > 0 && millis() - _lastRun < _interval) { + return false; + } + + _lastRun = millis(); + return true; + } + +private: + bool _enabled = false; + unsigned long _interval = 0; + unsigned long _lastRun = 0; + bool _setupDone = false; +}; diff --git a/src/src.ino b/src/src.ino new file mode 100644 index 0000000..d6a2bea --- /dev/null +++ b/src/src.ino @@ -0,0 +1,58 @@ +#include "defines.h" +#include +#include +#include +#include +#include "Settings.h" + +EEManager eeSettings(settings, 30000); + +#include "lib/CustomTask.h" +#include "WifiManagerTask.h" +#include "MqttTask.h" +#include "OpenThermTask.h" +#include "MainTask.h" + +// Tasks +WifiManagerTask* tWm; +MqttTask* tMqtt; +OpenThermTask* tOt; +MainTask* tMain; + +void setup() { +#ifdef USE_TELNET + TelnetStream.begin(); + delay(5000); +#else + Serial.begin(115200); + Serial.println("\n\n"); +#endif + + EEPROM.begin(eeSettings.blockSize()); + uint8_t eeSettingsResult = eeSettings.begin(0, 's'); + if (eeSettingsResult == 0) { + INFO("Settings loaded"); + + } else if (eeSettingsResult == 1) { + INFO("Settings NOT loaded, first start"); + + } else if (eeSettingsResult == 2) { + INFO("Settings NOT loaded (error)"); + } + + tWm = new WifiManagerTask(true); + Scheduler.start(tWm); + + tMqtt = new MqttTask(false); + Scheduler.start(tMqtt); + + tOt = new OpenThermTask(true); + Scheduler.start(tOt); + + tMain = new MainTask(true); + Scheduler.start(tMain); + + Scheduler.begin(); +} + +void loop() {} \ No newline at end of file