#include #include #include "HaHelper.h" WiFiClient espClient; PubSubClient client(espClient); HaHelper haHelper(client); class MqttTask : public Task { public: MqttTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {} protected: unsigned long lastReconnectAttempt = 0; unsigned long firstFailConnect = 0; const char* getTaskName() { return "Mqtt"; } int getTaskCore() { return 0; } void setup() { Log.sinfoln("MQTT", "Started"); client.setCallback(__callback); haHelper.setDevicePrefix(settings.mqtt.prefix); haHelper.setDeviceVersion(PROJECT_VERSION); haHelper.setDeviceModel(PROJECT_NAME); haHelper.setDeviceName(PROJECT_NAME); sprintf(buffer, CONFIG_URL, WiFi.localIP().toString().c_str()); haHelper.setDeviceConfigUrl(buffer); } void loop() { if (!client.connected() && millis() - lastReconnectAttempt >= MQTT_RECONNECT_INTERVAL) { Log.sinfoln("MQTT", "Not connected, state: %i, connecting to server %s...", client.state(), settings.mqtt.server); client.setServer(settings.mqtt.server, settings.mqtt.port); if (client.connect(settings.hostname, settings.mqtt.user, settings.mqtt.password)) { Log.sinfoln("MQTT", "Connected"); client.subscribe(getTopicPath("settings/set").c_str()); client.subscribe(getTopicPath("state/set").c_str()); publishHaEntities(); publishNonStaticHaEntities(true); firstFailConnect = 0; lastReconnectAttempt = 0; } else { Log.swarningln("MQTT", "Failed to connect to server"); if (settings.emergency.enable && !vars.states.emergency) { if (firstFailConnect == 0) { firstFailConnect = millis(); } if (millis() - firstFailConnect > EMERGENCY_TIME_TRESHOLD) { vars.states.emergency = true; Log.sinfoln("MQTT", "Emergency mode enabled"); } } lastReconnectAttempt = millis(); } } if (client.connected()) { if (vars.states.emergency) { vars.states.emergency = false; Log.sinfoln("MQTT", "Emergency mode disabled"); } client.loop(); bool published = publishNonStaticHaEntities(); publish(published); } } static bool updateSettings(JsonDocument& doc) { bool flag = false; if (!doc["debug"].isNull() && doc["debug"].is()) { settings.debug = doc["debug"].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()) { if (doc["emergency"]["target"].as() > 0 && doc["emergency"]["target"].as() < 100) { 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"]["turbo"].isNull() && doc["heating"]["turbo"].is()) { settings.heating.turbo = doc["heating"]["turbo"].as(); flag = true; } if (!doc["heating"]["target"].isNull() && doc["heating"]["target"].is()) { if (doc["heating"]["target"].as() > 0 && doc["heating"]["target"].as() < 100) { settings.heating.target = round(doc["heating"]["target"].as() * 10) / 10; flag = true; } } if (!doc["heating"]["hysteresis"].isNull() && doc["heating"]["hysteresis"].is()) { if (doc["heating"]["hysteresis"].as() >= 0 && doc["heating"]["hysteresis"].as() <= 5) { settings.heating.hysteresis = round(doc["heating"]["hysteresis"].as() * 10) / 10; flag = true; } } if (!doc["heating"]["maxTemp"].isNull() && doc["heating"]["maxTemp"].is()) { if (doc["heating"]["maxTemp"].as() > 0 && doc["heating"]["maxTemp"].as() <= 100 && doc["heating"]["maxTemp"].as() > settings.heating.minTemp) { settings.heating.maxTemp = doc["heating"]["maxTemp"].as(); flag = true; } } if (!doc["heating"]["minTemp"].isNull() && doc["heating"]["minTemp"].is()) { if (doc["heating"]["minTemp"].as() >= 0 && doc["heating"]["minTemp"].as() < 100 && doc["heating"]["minTemp"].as() < settings.heating.maxTemp) { settings.heating.minTemp = doc["heating"]["minTemp"].as(); 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()) { if (doc["dhw"]["target"].as() >= 0 && doc["dhw"]["target"].as() < 100) { settings.dhw.target = doc["dhw"]["target"].as(); flag = true; } } if (!doc["dhw"]["maxTemp"].isNull() && doc["dhw"]["maxTemp"].is()) { if (doc["dhw"]["maxTemp"].as() > 0 && doc["dhw"]["maxTemp"].as() <= 100 && doc["dhw"]["maxTemp"].as() > settings.dhw.minTemp) { settings.dhw.maxTemp = doc["dhw"]["maxTemp"].as(); flag = true; } } if (!doc["dhw"]["minTemp"].isNull() && doc["dhw"]["minTemp"].is()) { if (doc["dhw"]["minTemp"].as() >= 0 && doc["dhw"]["minTemp"].as() < 100 && doc["dhw"]["minTemp"].as() < settings.dhw.maxTemp) { settings.dhw.minTemp = doc["dhw"]["minTemp"].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()) { if (doc["pid"]["p_factor"].as() > 0 && doc["pid"]["p_factor"].as() <= 10) { 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()) { if (doc["pid"]["i_factor"].as() >= 0 && doc["pid"]["i_factor"].as() <= 10) { 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()) { if (doc["pid"]["d_factor"].as() >= 0 && doc["pid"]["d_factor"].as() <= 10) { settings.pid.d_factor = round(doc["pid"]["d_factor"].as() * 1000) / 1000; flag = true; } } if (!doc["pid"]["maxTemp"].isNull() && doc["pid"]["maxTemp"].is()) { if (doc["pid"]["maxTemp"].as() > 0 && doc["pid"]["maxTemp"].as() <= 100 && doc["pid"]["maxTemp"].as() > settings.pid.minTemp) { settings.pid.maxTemp = doc["pid"]["maxTemp"].as(); flag = true; } } if (!doc["pid"]["minTemp"].isNull() && doc["pid"]["minTemp"].is()) { if (doc["pid"]["minTemp"].as() >= 0 && doc["pid"]["minTemp"].as() < 100 && doc["pid"]["minTemp"].as() < settings.pid.maxTemp) { settings.pid.minTemp = doc["pid"]["minTemp"].as(); 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()) { if (doc["equitherm"]["n_factor"].as() > 0 && doc["equitherm"]["n_factor"].as() <= 10) { 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()) { if (doc["equitherm"]["k_factor"].as() >= 0 && doc["equitherm"]["k_factor"].as() <= 10) { 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()) { if (doc["equitherm"]["t_factor"].as() >= 0 && doc["equitherm"]["t_factor"].as() <= 10) { settings.equitherm.t_factor = round(doc["equitherm"]["t_factor"].as() * 1000) / 1000; flag = true; } } // sensors if (!doc["sensors"]["outdoor"]["type"].isNull() && doc["sensors"]["outdoor"]["type"].is()) { if (doc["sensors"]["outdoor"]["type"].as() >= 0 && doc["sensors"]["outdoor"]["type"].as() <= 2) { settings.sensors.outdoor.type = doc["sensors"]["outdoor"]["type"].as(); flag = true; } } if (!doc["sensors"]["outdoor"]["offset"].isNull() && doc["sensors"]["outdoor"]["offset"].is()) { if (doc["sensors"]["outdoor"]["offset"].as() >= -10 && doc["sensors"]["outdoor"]["offset"].as() <= 10) { settings.sensors.outdoor.offset = round(doc["sensors"]["outdoor"]["offset"].as() * 1000) / 1000; flag = true; } } if (!doc["sensors"]["indoor"]["type"].isNull() && doc["sensors"]["indoor"]["type"].is()) { if (doc["sensors"]["indoor"]["type"].as() >= 1 && doc["sensors"]["indoor"]["type"].as() <= 2) { settings.sensors.indoor.type = doc["sensors"]["indoor"]["type"].as(); flag = true; } } if (!doc["sensors"]["indoor"]["offset"].isNull() && doc["sensors"]["indoor"]["offset"].is()) { if (doc["sensors"]["indoor"]["offset"].as() >= -10 && doc["sensors"]["indoor"]["offset"].as() <= 10) { settings.sensors.indoor.offset = round(doc["sensors"]["indoor"]["offset"].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()) { if (doc["tuning"]["regulator"].as() >= 0 && doc["tuning"]["regulator"].as() <= 1) { vars.tuning.regulator = doc["tuning"]["regulator"].as(); flag = true; } } if (!doc["temperatures"]["indoor"].isNull() && doc["temperatures"]["indoor"].is()) { if (settings.sensors.indoor.type == 1 && doc["temperatures"]["indoor"].as() > -100 && doc["temperatures"]["indoor"].as() < 100) { vars.temperatures.indoor = round(doc["temperatures"]["indoor"].as() * 100) / 100; flag = true; } } if (!doc["temperatures"]["outdoor"].isNull() && doc["temperatures"]["outdoor"].is()) { if (settings.sensors.outdoor.type == 1 && doc["temperatures"]["outdoor"].as() > -100 && doc["temperatures"]["outdoor"].as() < 100) { vars.temperatures.outdoor = round(doc["temperatures"]["outdoor"].as() * 100) / 100; flag = true; } } if (!doc["actions"]["restart"].isNull() && doc["actions"]["restart"].is() && doc["actions"]["restart"].as()) { vars.actions.restart = true; } if (!doc["actions"]["faultReset"].isNull() && doc["actions"]["faultReset"].is() && doc["actions"]["faultReset"].as()) { vars.actions.faultReset = true; } if (!doc["actions"]["diagnosticReset"].isNull() && doc["actions"]["diagnosticReset"].is() && doc["actions"]["diagnosticReset"].as()) { vars.actions.diagnosticReset = true; } 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"); } prevPubVars = millis(); } // publish settings if (force || millis() - prevPubSettings > settings.mqtt.interval * 10) { publishSettings(getTopicPath("settings").c_str()); prevPubSettings = millis(); } } static void publishHaEntities() { // main haHelper.publishSelectOutdoorSensorType(); haHelper.publishSelectIndoorSensorType(); haHelper.publishNumberOutdoorSensorOffset(false); haHelper.publishNumberIndoorSensorOffset(false); haHelper.publishSwitchDebug(false); // emergency haHelper.publishSwitchEmergency(); haHelper.publishNumberEmergencyTarget(); haHelper.publishSwitchEmergencyUseEquitherm(); // heating haHelper.publishSwitchHeating(false); haHelper.publishSwitchHeatingTurbo(); //haHelper.publishNumberHeatingTarget(false); haHelper.publishNumberHeatingHysteresis(); haHelper.publishSensorHeatingSetpoint(false); haHelper.publishSensorCurrentHeatingMinTemp(false); haHelper.publishSensorCurrentHeatingMaxTemp(false); haHelper.publishNumberHeatingMinTemp(false); haHelper.publishNumberHeatingMaxTemp(false); // pid haHelper.publishSwitchPID(); haHelper.publishNumberPIDFactorP(); haHelper.publishNumberPIDFactorI(); haHelper.publishNumberPIDFactorD(); haHelper.publishNumberPIDMinTemp(false); haHelper.publishNumberPIDMaxTemp(false); // equitherm haHelper.publishSwitchEquitherm(); haHelper.publishNumberEquithermFactorN(); haHelper.publishNumberEquithermFactorK(); haHelper.publishNumberEquithermFactorT(); // tuning haHelper.publishSwitchTuning(); haHelper.publishSelectTuningRegulator(); // states haHelper.publishBinSensorStatus(); haHelper.publishBinSensorOtStatus(); haHelper.publishBinSensorHeating(); haHelper.publishBinSensorFlame(); haHelper.publishBinSensorFault(); haHelper.publishBinSensorDiagnostic(); haHelper.publishSensorFaultCode(); haHelper.publishSensorRssi(false); haHelper.publishSensorUptime(false); // sensors haHelper.publishSensorModulation(false); haHelper.publishSensorPressure(false); // temperatures haHelper.publishNumberIndoorTemp(); //haHelper.publishNumberOutdoorTemp(); haHelper.publishSensorHeatingTemp(); // buttons haHelper.publishButtonRestart(false); haHelper.publishButtonFaultReset(); haHelper.publishButtonDiagnosticReset(); } static bool publishNonStaticHaEntities(bool force = false) { static byte _heatingMinTemp, _heatingMaxTemp, _dhwMinTemp, _dhwMaxTemp; static bool _editableOutdoorTemp, _editableIndoorTemp, _dhwPresent; bool published = false; bool isStupidMode = !settings.pid.enable && !settings.equitherm.enable; byte heatingMinTemp = isStupidMode ? settings.heating.minTemp : 10; byte heatingMaxTemp = isStupidMode ? settings.heating.maxTemp : 30; bool editableOutdoorTemp = settings.sensors.outdoor.type == 1; bool editableIndoorTemp = settings.sensors.indoor.type == 1; if (force || _dhwPresent != settings.opentherm.dhwPresent) { _dhwPresent = settings.opentherm.dhwPresent; if (_dhwPresent) { haHelper.publishSwitchDHW(false); haHelper.publishSensorCurrentDHWMinTemp(false); haHelper.publishSensorCurrentDHWMaxTemp(false); haHelper.publishNumberDHWMinTemp(false); haHelper.publishNumberDHWMaxTemp(false); haHelper.publishBinSensorDHW(); haHelper.publishSensorDHWTemp(); } else { haHelper.deleteSwitchDHW(); haHelper.deleteSensorCurrentDHWMinTemp(); haHelper.deleteSensorCurrentDHWMaxTemp(); haHelper.deleteNumberDHWMinTemp(); haHelper.deleteNumberDHWMaxTemp(); haHelper.deleteBinSensorDHW(); haHelper.deleteSensorDHWTemp(); haHelper.deleteNumberDHWTarget(); haHelper.deleteClimateDHW(); } published = true; } 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 (_dhwPresent && (force || _dhwMinTemp != settings.dhw.minTemp || _dhwMaxTemp != settings.dhw.maxTemp)) { _dhwMinTemp = settings.dhw.minTemp; _dhwMaxTemp = settings.dhw.maxTemp; haHelper.publishNumberDHWTarget(settings.dhw.minTemp, settings.dhw.maxTemp, false); haHelper.publishClimateDHW(settings.dhw.minTemp, settings.dhw.maxTemp); published = true; } if (force || _editableOutdoorTemp != editableOutdoorTemp) { _editableOutdoorTemp = editableOutdoorTemp; if (editableOutdoorTemp) { haHelper.deleteSensorOutdoorTemp(); haHelper.publishNumberOutdoorTemp(); } else { haHelper.deleteNumberOutdoorTemp(); haHelper.publishSensorOutdoorTemp(); } published = true; } if (force || _editableIndoorTemp != editableIndoorTemp) { _editableIndoorTemp = editableIndoorTemp; if (editableIndoorTemp) { haHelper.deleteSensorIndoorTemp(); haHelper.publishNumberIndoorTemp(); } else { haHelper.deleteNumberIndoorTemp(); haHelper.publishSensorIndoorTemp(); } published = true; } return published; } static bool publishSettings(const char* topic) { StaticJsonDocument<2048> doc; doc["debug"] = settings.debug; 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"]["turbo"] = settings.heating.turbo; doc["heating"]["target"] = settings.heating.target; doc["heating"]["hysteresis"] = settings.heating.hysteresis; doc["heating"]["minTemp"] = settings.heating.minTemp; doc["heating"]["maxTemp"] = settings.heating.maxTemp; doc["dhw"]["enable"] = settings.dhw.enable; doc["dhw"]["target"] = settings.dhw.target; doc["dhw"]["minTemp"] = settings.dhw.minTemp; doc["dhw"]["maxTemp"] = settings.dhw.maxTemp; 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["pid"]["minTemp"] = settings.pid.minTemp; doc["pid"]["maxTemp"] = settings.pid.maxTemp; 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; doc["sensors"]["outdoor"]["type"] = settings.sensors.outdoor.type; doc["sensors"]["outdoor"]["offset"] = settings.sensors.outdoor.offset; doc["sensors"]["indoor"]["type"] = settings.sensors.indoor.type; doc["sensors"]["indoor"]["offset"] = settings.sensors.indoor.offset; client.beginPublish(topic, measureJson(doc), false); 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["states"]["rssi"] = vars.states.rssi; doc["states"]["uptime"] = (unsigned long) (millis() / 1000); 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"]["heatingEnabled"] = vars.parameters.heatingEnabled; 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); 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) { String payloadStr; payloadStr.reserve(length); for (unsigned int i = 0; i < length; i++) { if ( payload[i] == 10 ) { payloadStr += "\r\n> "; } else { payloadStr += (char) payload[i]; } } Log.strace("MQTT.MSG", "Topic: %s\r\n> %s\n\r\n", topic, payloadStr.c_str()); } 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); } } };