diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index 8af40fa0..aafec55f 100644 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -443,6 +443,41 @@ bool loadTlsCaFromFS(WiFiClientSecure *client) { #endif +void publishRuleExecution(const char *name, uint8_t id, int result, uint32_t duration, bool nested) { + if (!mqtt_client.connected()) return; + JsonDocument doc; + doc["rule"] = name; + doc["rule_id"] = id; + doc["result"] = result; + doc["uptime_ms"] = millis(); + doc["nested"] = nested; + if (!nested) doc["duration_us"] = duration; + String payload; + serializeJson(doc, payload); + char topic[256]; + snprintf(topic, sizeof(topic), "%s/rules/executed", heishamonSettings.mqtt_topic_base); + mqtt_client.publish(topic, payload.c_str(), false); +} + +void publishRulesState(bool force) { + static int reported = -1; + static unsigned long lastPublish = 0; + const unsigned long now = millis(); + const int state = nrrules > 0 ? 1 : 0; + const bool periodic = (unsigned long)(now - lastPublish) >= heishamonSettings.updateAllTime * 1000UL; + if (!mqtt_client.connected()) return; + if (force || periodic || reported != state) { + char topic[256]; + snprintf(topic, sizeof(topic), "%s/rules/state", heishamonSettings.mqtt_topic_base); + if (mqtt_client.publish(topic, state ? "1" : "0", true)) { + reported = state; + lastPublish = now; + } else { + reported = -1; // Retry on the next loop. + } + } +} + void mqtt_reconnect() { unsigned long now = millis(); @@ -488,6 +523,8 @@ void mqtt_reconnect() if (mqtt_client.connect(heishamonSettings.wifi_hostname, heishamonSettings.mqtt_username, heishamonSettings.mqtt_password, topic, 1, true, "Offline")) { mqttReconnects++; + publishRulesState(true); + publishRelayStates(mqtt_client, heishamonSettings.mqtt_topic_base, heishamonSettings.updateAllTime, true); if (heishamonSettings.opentherm) { sprintf(topic, "%s/%s/#", heishamonSettings.mqtt_topic_base, mqtt_topic_opentherm_read); mqtt_client.subscribe(topic); @@ -1027,7 +1064,7 @@ void mqtt_callback(char* topic, byte* payload, unsigned int length) { mqttOTCallback(topic_otcommand, msg); } else if (strncmp(topic_command, mqtt_topic_gpio, strlen(mqtt_topic_gpio)) == 0) { char* topic_gpiocommand = topic_command + strlen(mqtt_topic_gpio) + 1; //strip the gpio subtopic from the topic - mqttGPIOCallback(topic_gpiocommand, msg); + mqttGPIOCallback(topic_gpiocommand, msg, mqtt_client, heishamonSettings.mqtt_topic_base); } else if (strncmp(topic_command, mqtt_topic_1wire, strlen(mqtt_topic_1wire)) == 0) { //this is a 1wire address topic, restore its retained value at boot char* topic_1wire_address = topic_command + strlen(mqtt_topic_1wire) + 1; //strip the "1wire/" from the topic to get the sensor address if ((strchr(topic_1wire_address, '/') == NULL) && (length > 0)) { //only handle the address topic itself (not .../alias) and skip empty (cleared) retained messages @@ -1201,6 +1238,22 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { memset(&cpy, 0, args->len + 1); snprintf((char *)&cpy, args->len + 1, "%.*s", args->len, args->value); +#ifdef ESP32 + if (strcmp((char *)args->name, "SetRelayOne") == 0 || strcmp((char *)args->name, "SetRelayTwo") == 0) { + const bool firstRelay = strcmp((char *)args->name, "SetRelayOne") == 0; + mqttGPIOCallback((char *)(firstRelay ? "relay/one" : "relay/two"), cpy, mqtt_client, heishamonSettings.mqtt_topic_base); + snprintf(log_msg, sizeof(log_msg), "%s: %d", (char *)args->name, digitalRead(firstRelay ? relayOnePin : relayTwoPin) ? 1 : 0); + if ((client->userdata = realloc(client->userdata, strlen((char *)client->userdata) + strlen(log_msg) + 2)) == NULL) { + loggingSerial.printf(PSTR("Out of memory %s:#%d\n"), __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + strcat((char *)client->userdata, log_msg); + strcat((char *)client->userdata, "\n"); + return 0; + } +#endif + for (uint8_t x = 0; x < sizeof(commands) / sizeof(commands[0]); x++) { cmdStruct tmp; memcpy_P(&tmp, &commands[x], sizeof(tmp)); @@ -2240,6 +2293,8 @@ void loop() { } timerqueue_update(); + publishRulesState(false); + publishRelayStates(mqtt_client, heishamonSettings.mqtt_topic_base, heishamonSettings.updateAllTime); #ifdef ESP32 delay(1); // to keep watchdog happy #endif diff --git a/HeishaMon/commands.cpp b/HeishaMon/commands.cpp index 0e28e023..5a05312c 100644 --- a/HeishaMon/commands.cpp +++ b/HeishaMon/commands.cpp @@ -313,6 +313,54 @@ unsigned int set_force_defrost(char *msg, unsigned char *cmd, char *log_msg) { return sizeof(panasonicSendQuery); } +// Validate the complete payload before touching the command buffer. The limits +// below describe the nonzero wire encoding; the heat pump may impose narrower +// model-specific installer-menu limits. +static unsigned int set_integer_field(char *msg, unsigned char *cmd, char *log_msg, + byte address, int minimum, int maximum, + int step, int divisor, int offset, int multiplier) { + const size_t length = msg == NULL ? 0 : strlen(msg); + bool valid = length > 0 && length <= 5; + size_t index = valid && msg[0] == '-' ? 1 : 0; + const bool negative = index == 1; + valid = valid && index < length; + int value = 0; + for (; valid && index < length; ++index) { + valid = msg[index] >= '0' && msg[index] <= '9'; + if (valid) value = value * 10 + msg[index] - '0'; + } + if (negative) value = -value; + if (!valid || value < minimum || value > maximum || value % step != 0) { + snprintf_P(log_msg, 256, PSTR("Invalid setting for byte %u: expected integer %d..%d, step %d"), + address, minimum, maximum, step); + return 0; + } + memcpy_P(cmd, panasonicSendQuery, sizeof(panasonicSendQuery)); + cmd[address] = (value / divisor + offset) * multiplier; + snprintf_P(log_msg, 256, PSTR("set byte %u to value %d"), address, value); + return sizeof(panasonicSendQuery); +} + +unsigned int set_heat_to_cool_temp(char *msg, unsigned char *cmd, char *log_msg) { + return set_integer_field(msg, cmd, log_msg, 95, -127, 127, 1, 1, 128, 1); +} + +unsigned int set_cool_to_heat_temp(char *msg, unsigned char *cmd, char *log_msg) { + return set_integer_field(msg, cmd, log_msg, 96, -127, 127, 1, 1, 128, 1); +} + +unsigned int set_room_operation_max_time(char *msg, unsigned char *cmd, char *log_msg) { + return set_integer_field(msg, cmd, log_msg, 97, 30, 7620, 30, 30, 1, 1); +} + +unsigned int set_dhw_heatup_max_time(char *msg, unsigned char *cmd, char *log_msg) { + return set_integer_field(msg, cmd, log_msg, 98, 1, 254, 1, 1, 1, 1); +} + +unsigned int set_quiet_mode_schedule(char *msg, unsigned char *cmd, char *log_msg) { + return set_integer_field(msg, cmd, log_msg, 7, 0, 1, 1, 1, 1, 64); +} + unsigned int set_sterilization_temp(char *msg, unsigned char *cmd, char *log_msg) { if (msg == NULL || strlen(msg) != 2 || msg[0] < '0' || msg[0] > '9' || msg[1] < '0' || msg[1] > '9') { snprintf_P(log_msg, 256, PSTR("Invalid sterilization temperature: expected integer 55-75 C")); diff --git a/HeishaMon/commands.h b/HeishaMon/commands.h index cfa2c74c..0cb78022 100644 --- a/HeishaMon/commands.h +++ b/HeishaMon/commands.h @@ -77,6 +77,11 @@ unsigned int set_dhw_sensor_selection(char *msg, unsigned char *cmd, char *log_m unsigned int set_dhw_heater_state(char *msg, unsigned char *cmd, char *log_msg); unsigned int set_room_heater_state(char *msg, unsigned char *cmd, char *log_msg); unsigned int set_heater_on_outdoor_temp(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_heat_to_cool_temp(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_cool_to_heat_temp(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_room_operation_max_time(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_dhw_heatup_max_time(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_quiet_mode_schedule(char *msg, unsigned char *cmd, char *log_msg); //optional pcb commands unsigned int set_heat_cool_mode(char *msg, char *log_msg); @@ -178,6 +183,11 @@ const cmdStruct commands[] PROGMEM = { { "SetDHWHeaterState", set_dhw_heater_state }, { "SetRoomHeaterState", set_room_heater_state }, { "SetHeaterOnOutdoorTemp", set_heater_on_outdoor_temp }, + { "SetHeatToCoolTemp", set_heat_to_cool_temp }, + { "SetCoolToHeatTemp", set_cool_to_heat_temp }, + { "SetRoomOperationMaxTime", set_room_operation_max_time }, + { "SetDHWHeatupMaxTime", set_dhw_heatup_max_time }, + { "SetQuietModeSchedule", set_quiet_mode_schedule }, }; struct optCmdStruct{ diff --git a/HeishaMon/decode.cpp b/HeishaMon/decode.cpp index 4d341ad4..dd65221c 100644 --- a/HeishaMon/decode.cpp +++ b/HeishaMon/decode.cpp @@ -9,6 +9,27 @@ unsigned long lastalldatatime = 0; unsigned long lastallextradatatime = 0; unsigned long lastalloptdatatime = 0; +String getRawByte(byte input) { + return String(input); +} + +String getHeaterCapacity(byte input) { + int encoded = (input >> 2) & 0b11; + return String(encoded == 0 ? -1 : encoded * 3); +} + +String getIntMinus1Times30(byte input) { + return String(((int)input - 1) * 30); +} + +String getZone2Active(byte input) { + return String((input >> 1) & 1); +} + +String getZone1Active(byte input) { + return String(input & 1); +} + String getBit1(byte input) { return String(input >> 7); } diff --git a/HeishaMon/decode.h b/HeishaMon/decode.h index b88c2024..3a0ca558 100644 --- a/HeishaMon/decode.h +++ b/HeishaMon/decode.h @@ -14,6 +14,11 @@ void decode_heatpump_data(char* data, char* actData, PubSubClient &mqtt_client, void decode_heatpump_data_extra(char* data, char* actDataExtra, PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime); void decode_optional_heatpump_data(char* data, char* actOptDat, PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime); +String getRawByte(byte input); +String getHeaterCapacity(byte input); +String getIntMinus1Times30(byte input); +String getZone2Active(byte input); +String getZone1Active(byte input); String unknown(byte input); String getBit1(byte input); String getBit1and2(byte input); @@ -42,7 +47,7 @@ static const char _unknown[] PROGMEM = "unknown"; -#define NUMBER_OF_TOPICS 144 //last topic number + 1 +#define NUMBER_OF_TOPICS 170 //last topic number + 1 #define NUMBER_OF_TOPICS_EXTRA 6 //last topic number + 1 #define NUMBER_OF_OPT_TOPICS 7 //last topic number + 1 #define MAX_TOPIC_LEN 42 // max length + 1 @@ -220,6 +225,32 @@ static const char topics[][MAX_TOPIC_LEN] PROGMEM = { "Quiet_Mode_Priority", //TOP141 "Expansion_Valve", //TOP142 "DHW_Sensor_Selection", //TOP143 + "Dry_Concrete_State", //TOP144 + "DHW_Capacity_Mode", //TOP145 + "Zone_Configuration", //TOP146 + "Internal_Heater_Capacity", //TOP147 + "DHW_Heater_Type", //TOP148 + "SG_Ready_Control", //TOP149 + "Demand_Control", //TOP150 + "DHW_Defrost", //TOP151 + "Dry_Concrete_Target_Temp", //TOP152 + "Pool_Delta", //TOP153 + "SG_Ready_1_Heat_Capacity", //TOP154 + "SG_Ready_1_DHW_Capacity", //TOP155 + "SG_Ready_2_Heat_Capacity", //TOP156 + "SG_Ready_2_DHW_Capacity", //TOP157 + "Room_Operation_Max_Time", //TOP158 + "DHW_Heatup_Max_Time", //TOP159 + "Quiet_Mode_Actual_State", //TOP160 + "Powerful_Mode_Actual_State", //TOP161 + "Heat_Cool_Switch_State", //TOP162 + "External_Switch_State", //TOP163 + "Bivalent_Boiler_Contact_State", //TOP164 + "Z2_Active", //TOP165 + "Z1_Active", //TOP166 + "Backup_Heater_Enable", //TOP167 + "Cooling_Enable", //TOP168 + "Heat_Request_Temp_With_Buffer", //TOP169 }; static const byte topicBytes[] PROGMEM = { //can store the index as byte (8-bit unsigned humber) as there aren't more then 255 bytes (actually only 203 bytes) to decode @@ -367,6 +398,32 @@ static const byte topicBytes[] PROGMEM = { //can store the index as byte (8-bit 11, //TOP141 175, //TOP142 11, //TOP143 + 5, //TOP144 + 9, //TOP145 + 21, //TOP146 + 25, //TOP147 + 25, //TOP148 + 27, //TOP149 + 27, //TOP150 + 30, //TOP151 + 46, //TOP152 + 58, //TOP153 + 71, //TOP154 + 72, //TOP155 + 73, //TOP156 + 74, //TOP157 + 97, //TOP158 + 98, //TOP159 + 110, //TOP160 + 110, //TOP161 + 110, //TOP162 + 110, //TOP163 + 112, //TOP164 + 117, //TOP165 + 117, //TOP166 + 120, //TOP167 + 120, //TOP168 + 152, //TOP169 }; @@ -526,6 +583,32 @@ static const topicFP topicFunctions[] PROGMEM = { getBit3and4, //TOP141 getIntMinus1, //TOP142 getBit7and8, //TOP143 + getBit7and8, //TOP144 + getBit3and4, //TOP145 + getRawByte, //TOP146 + getHeaterCapacity, //TOP147 + getBit7and8, //TOP148 + getBit5and6, //TOP149 + getBit7and8, //TOP150 + getBit7and8, //TOP151 + getIntMinus128, //TOP152 + getIntMinus128, //TOP153 + getIntMinus1, //TOP154 + getIntMinus1, //TOP155 + getIntMinus1, //TOP156 + getIntMinus1, //TOP157 + getIntMinus1Times30, //TOP158 + getIntMinus1, //TOP159 + getBit1and2, //TOP160 + getBit3and4, //TOP161 + getBit5and6, //TOP162 + getBit7and8, //TOP163 + getBit1and2, //TOP164 + getZone2Active, //TOP165 + getZone1Active, //TOP166 + getBit3and4, //TOP167 + getBit5and6, //TOP168 + getIntMinus128, //TOP169 }; static const char *DisabledEnabled[] PROGMEM = {"2", "Disabled", "Enabled"}; @@ -567,6 +650,12 @@ static const char *HeatingControl[] PROGMEM = {"2", "Comfort", "Efficiency"}; static const char *SmartDHW[] PROGMEM = {"2", "Variable", "Standard"}; static const char *QuietModePriority[] PROGMEM = {"2", "Sound", "Capacity"}; static const char *DHWSensorType[] PROGMEM = {"2", "Top", "Center"}; +static const char *DHWCapacityMode[] PROGMEM = {"2", "Standard", "Variable"}; +static const char *DHWHeaterType[] PROGMEM = {"2", "Internal", "External"}; +static const char *Kilowatt[] PROGMEM = {"0", "kW"}; +static const char *HeatCoolSwitch[] PROGMEM = {"2", "Heat", "Cool"}; +static const char *ClosedOpen[] PROGMEM = {"2", "Closed", "Open"}; +static const char *ZoneConfiguration[] PROGMEM = {"0", "Configuration code"}; static const char *Steps[] PROGMEM = {"0", "Steps"}; static const char **opttopicDescription[] PROGMEM = { @@ -733,4 +822,30 @@ static const char **topicDescription[] PROGMEM = { QuietModePriority, //TOP141 Steps, //TOP142 DHWSensorType, //TOP143 + OffOn, //TOP144 + DHWCapacityMode, //TOP145 + ZoneConfiguration, //TOP146 + Kilowatt, //TOP147 + DHWHeaterType, //TOP148 + DisabledEnabled, //TOP149 + DisabledEnabled, //TOP150 + DisabledEnabled, //TOP151 + Celsius, //TOP152 + Kelvin, //TOP153 + Percent, //TOP154 + Percent, //TOP155 + Percent, //TOP156 + Percent, //TOP157 + Minutes, //TOP158 + Minutes, //TOP159 + OffOn, //TOP160 + OffOn, //TOP161 + HeatCoolSwitch, //TOP162 + ClosedOpen, //TOP163 + OffOn, //TOP164 + InactiveActive, //TOP165 + InactiveActive, //TOP166 + InactiveActive, //TOP167 + InactiveActive, //TOP168 + Celsius, //TOP169 }; diff --git a/HeishaMon/gpio.cpp b/HeishaMon/gpio.cpp index 1d86491a..17bdb211 100644 --- a/HeishaMon/gpio.cpp +++ b/HeishaMon/gpio.cpp @@ -11,7 +11,34 @@ void setupGPIO(gpioSettingsStruct gpioSettings) { } } -void mqttGPIOCallback(char* topic, char* value) { +void publishRelayStates(PubSubClient &mqtt, const char* base, unsigned int interval, bool force) { +#ifdef ESP32 + static int reported[2] = {-1, -1}; + static unsigned long lastPublish = 0; + const unsigned long now = millis(); + const bool periodic = (unsigned long)(now - lastPublish) >= interval * 1000UL; + const int pins[2] = {relayOnePin, relayTwoPin}; + const char* names[2] = {"one", "two"}; + if (!mqtt.connected()) return; + bool success = true; + for (int i = 0; i < 2; i++) { + const int state = digitalRead(pins[i]) ? 1 : 0; + if (force || periodic || reported[i] != state) { + char topic[256]; + snprintf(topic, sizeof(topic), "%s/gpio/relay/%s/state", base, names[i]); + if (mqtt.publish(topic, state ? "1" : "0", true)) { + reported[i] = state; + } else { + reported[i] = -1; // Retry failed publications on the next loop. + success = false; + } + } + } + if ((force || periodic) && success) lastPublish = now; +#endif +} + +void mqttGPIOCallback(char* topic, char* value, PubSubClient &mqtt, const char* base) { char log_msg[256]; sprintf_P(log_msg, PSTR("GPIO: MQTT message received on subtopic '%s' value '%s'"), topic, value); log_message(log_msg); @@ -19,9 +46,11 @@ void mqttGPIOCallback(char* topic, char* value) { if (strcmp_P(topic, PSTR("relay/one")) == 0) { log_message(_F("GPIO: MQTT message received 'relay/one'")); digitalWrite(relayOnePin,((stricmp((char*)"true", value) == 0) || (stricmp((char*)"on", value) == 0) || (stricmp((char*)"enable", value) == 0)|| (String(value).toInt() == 1 ))); + publishRelayStates(mqtt, base, 0, true); } else if (strcmp_P(topic,PSTR("relay/two")) == 0) { log_message(_F("GPIO: MQTT message received 'relay/two'")); digitalWrite(relayTwoPin,((stricmp((char*)"true", value) == 0) || (stricmp((char*)"on", value) == 0) || (stricmp((char*)"enable", value) == 0)|| (String(value).toInt() == 1 ))); + publishRelayStates(mqtt, base, 0, true); } #endif } diff --git a/HeishaMon/gpio.h b/HeishaMon/gpio.h index aea5191d..12d1070e 100644 --- a/HeishaMon/gpio.h +++ b/HeishaMon/gpio.h @@ -11,6 +11,7 @@ #define relayTwoPin 47 #endif +#include extern const char* mqtt_topic_gpio; struct gpioSettingsStruct { @@ -24,6 +25,7 @@ struct gpioSettingsStruct { }; void setupGPIO(gpioSettingsStruct gpioSettings); -void mqttGPIOCallback(char* topic, char* value); +void mqttGPIOCallback(char* topic, char* value, PubSubClient &mqtt, const char* base); +void publishRelayStates(PubSubClient &mqtt, const char* base, unsigned int interval, bool force = false); diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index 09299d66..dae56806 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -1027,6 +1027,7 @@ document.body.onload=function(){ startWebsockets(); monitorWebSocket(); refreshTable(); + setInterval(refreshRelayStates,5000); }; var dallasAliasEdit=function(){ isEditing=false; @@ -1108,6 +1109,7 @@ async function refreshTable(){ var tb=document.getElementById('heishavalues');tb.innerHTML=''; d.heatpump.forEach(function(item){tb.appendChild(buildRow(item,'Topic'));}); } + renderRelayStates(d); if(d&&d['heatpump extra']&&Array.isArray(d['heatpump extra'])){ var tb=document.getElementById('heishavalues'); d['heatpump extra'].forEach(function(item){tb.appendChild(buildRow(item,'Topic'));}); @@ -1144,6 +1146,32 @@ async function refreshTable(){ } } catch(e){console.error(e);} } +var hasRelays=false; +function renderRelayStates(data){ + hasRelays=!!(data&&Array.isArray(data.relays)&&data.relays.length); + if(!hasRelays)return; + var tb=document.getElementById('heishavalues'); + data.relays.forEach(function(item){ + var row=document.getElementById(item.Topic+'-relay'); + if(!row){ + row=document.createElement('tr');row.id=item.Topic+'-relay'; + for(var i=0;i<4;i++)row.appendChild(document.createElement('td')); + tb.appendChild(row); + } + row.children[0].textContent=item.Topic; + row.children[1].textContent=item.Name; + row.children[2].textContent=item.Value===1?'On':'Off'; + row.children[3].textContent=item['MQTT topic']; + row.children[3].title='GPIO output state; no mechanical contact feedback'; + }); +} +async function refreshRelayStates(){ + if(!hasRelays||document.hidden)return; + try{ + var response=await fetch('/json'); + renderRelayStates(await response.json()); + }catch(e){console.error(e);} +} function buildRow(item,idKey){ var row=document.createElement('tr'); var topic=item[idKey]; diff --git a/HeishaMon/rules.cpp b/HeishaMon/rules.cpp index c452bdbf..bcff7f35 100644 --- a/HeishaMon/rules.cpp +++ b/HeishaMon/rules.cpp @@ -32,6 +32,7 @@ #define OPTDATASIZE 20 bool send_command(byte* command, int length); +void publishRuleExecution(const char *name, uint8_t id, int result, uint32_t duration, bool nested); #ifdef ESP32 extern QueueHandle_t pcbQueue; #endif @@ -364,6 +365,9 @@ if(strncmp(text, "s0#", 3) == 0 && (size == 9 || size == 13 || size == 18)) { } static void rule_done_cb(struct rules_t *obj) { + if (!parsing && obj->ctx.ret != NULL) { + publishRuleExecution(obj->name, obj->nr, 0, 0, true); + } return; } @@ -892,6 +896,7 @@ void rules_timer_cb(int nr) { int ret = rule_run(rules[nr], 0); timestamp.second = micros(); + publishRuleExecution(rules[nr]->name, rules[nr]->nr, ret, timestamp.second - timestamp.first, false); if(ret == 0) { logprintf_P(F("%s%d %s %d %s"), F("rule #"), rules[nr]->nr, F("was executed in"), timestamp.second - timestamp.first, F("microseconds")); @@ -1053,6 +1058,7 @@ void rules_event_cb(const char *prefix, const char *name) { int ret = rule_run(rules[nr], 0); timestamp.second = micros(); + publishRuleExecution(rules[nr]->name, rules[nr]->nr, ret, timestamp.second - timestamp.first, false); if(ret == 0) { logprintf_P(F("%s%d %s %d %s"), F("rule #"), rules[nr]->nr, F("was executed in"), timestamp.second - timestamp.first, F("microseconds")); @@ -1077,6 +1083,7 @@ void rules_boot(void) { int ret = rule_run(rules[nr], 0); timestamp.second = micros(); + publishRuleExecution(rules[nr]->name, rules[nr]->nr, ret, timestamp.second - timestamp.first, false); if(ret == 0) { logprintf_P(F("%s%d %s %d %s"), F("rule #"), rules[nr]->nr, F("was executed in"), timestamp.second - timestamp.first, F("microseconds")); diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 4ae8e003..b0c176e5 100644 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -1241,6 +1241,24 @@ int handleJsonOutput(struct webserver_t *client, char* actData, char* actDataExt client->content--; // The webserver also increases by 1 } else if (client->content == (NUMBER_OF_TOPICS + extraTopics + numOptTopics + 1)) { webserver_send_content_P(client, PSTR("]"), 1); +#ifdef ESP32 + // Onboard outputs are independent of the heat pump's serial data block. + JsonDocument relaysDoc; + JsonArray relays = relaysDoc["relays"].to(); + const int relayPins[2] = {relayOnePin, relayTwoPin}; + const char* relayNames[2] = {"one", "two"}; + for (int i = 0; i < 2; i++) { + JsonObject relay = relays.add(); + relay["Topic"] = String("TOP") + (NUMBER_OF_TOPICS + i); + relay["Name"] = String("Relay ") + (i + 1); + relay["Value"] = digitalRead(relayPins[i]) ? 1 : 0; + relay["MQTT topic"] = String(heishamonSettings->mqtt_topic_base) + "/gpio/relay/" + relayNames[i] + "/state"; + } + String relaysJson; + serializeJson(relays, relaysJson); + webserver_send_content_P(client, PSTR(",\"relays\":"), 10); + webserver_send_content(client, (char*)relaysJson.c_str(), relaysJson.length()); +#endif if (heishamonSettings->use_1wire) { webserver_send_content_P(client, PSTR(",\"1wire\":"), 9); dallasJsonOutput(client); diff --git a/MQTT-Topics.md b/MQTT-Topics.md index 6fe71f83..1b5138dd 100644 --- a/MQTT-Topics.md +++ b/MQTT-Topics.md @@ -1,11 +1,40 @@ # MQTT Topics for HeishaMon +`TOP...` IDs identify the normal decoded heat pump values; `XTOP...` and `OPT...` identify values from the extra data block and optional PCB. Not every MQTT topic needs such an ID: device status and event topics such as `LWT`, `log`, `stats`, `rules/state`, and `rules/executed` are documented separately without a TOP number. The ESP32 relay states are a separate extension with `TOP170` and `TOP171` IDs for the web/JSON display. + ## Availability Topic: ID | Topic | Response --- | --- | --- || LWT | Online/Offline (automatically returns to Offline if connection with the HeishaMon lost) +## Rules Engine Status: + +Topic | Response/Description +--- | --- +rules/state | Read-only, retained: `1` if at least one rule block is loaded, `0` if no rule blocks are loaded (both ESP8266 and ESP32). + +The full topic is `/rules/state`, for example `panasonic_heat_pump/rules/state`. It is published on MQTT connection/reconnection, when the state changes, and at the configured full-data resend interval (`updateAllTime`). Failed publications are retried. There is no command to set this topic; publishing to it does not enable or disable rules. + +This reports loaded rules, not whether a rule is executing at that instant. `0` can mean that no ruleset exists, parsing failed, rules were deinitialized, or loading was skipped after a crash reboot. It does not distinguish these causes. The existing `/stats` JSON field `"rules active"` provides the number of loaded rule blocks. Check `/LWT` for device availability; a retained rules state can outlive a disconnected device. + +## Rule Execution Events: + +`/rules/executed` (for example `panasonic_heat_pump/rules/executed`) publishes a read-only JSON event after a rule execution returns. It is **not retained**. + +```json +{"rule":"timer=1","rule_id":2,"result":0,"uptime_ms":123456,"nested":false,"duration_us":245} +``` + +- `rule`: block name, such as `System#Boot`, `timer=1`, `@Outside_Temp`, or a custom helper name. +- `rule_id`: the engine's numeric block ID; it may change when the ruleset is edited. +- `result`: interpreter return code (`0` = successful return, nonzero = error). Success does not imply that a command was emitted or acknowledged by the heat pump. +- `uptime_ms`: device uptime at publication, in milliseconds (wraps after approximately 49.7 days). +- `nested`: `true` for a successfully completed helper block invoked by another rule. Each completed helper produces its own event before the outer block's event. +- `duration_us`: elapsed microseconds for an outer Boot, timer, or topic/event invocation, including its helper calls. Omitted for nested helper events. Nested event publication time is included in the outer duration. + +Ruleset validation does not publish execution events. A returned error is reported for the outer invocation; a helper that fails before returning successfully has no separate completion event. A crash cannot produce a completion event. Events use MQTT QoS 0, are sent only while connected, and are neither queued nor replayed after reconnecting. Boot events may therefore be absent if MQTT is not connected yet. Every execution can produce traffic, including frequent timer/topic handlers and their helper calls. + ## Log Topic: ID | Topic | Response @@ -160,6 +189,39 @@ TOP140 | main/Smart_DHW | Smart DHW TOP141 | main/Quiet_Mode_Priority | Quiet Mode Priority (0=sound, 1=capacity) TOP142 | main/Expansion_Valve | Expansion Valve (Steps) TOP143 | main/DHW_Sensor_Selection | DHW tank sensor selection (0=Top, 1=Center) (K/L series All-In-One only) +TOP144 | main/Dry_Concrete_State | Concrete drying (0=off, 1=on) +TOP145 | main/DHW_Capacity_Mode | DHW capacity (0=Standard, 1=Variable; J series) +TOP146 | main/Zone_Configuration | Zone configuration (decimal raw code: 21=one zone, Z1 room; 25=one zone, Z1 pool; 22=two zones, Z2 room; 38=two zones, Z2 pool) +TOP147 | main/Internal_Heater_Capacity | Configured internal heater capacity (3/6/9 kW; -1=unavailable) +TOP148 | main/DHW_Heater_Type | DHW heater type (0=internal, 1=external) +TOP149 | main/SG_Ready_Control | SG Ready control (0=disabled, 1=enabled) +TOP150 | main/Demand_Control | Demand control (0=disabled, 1=enabled) +TOP151 | main/DHW_Defrost | DHW defrost option (0=no, 1=yes; K/L series) +TOP152 | main/Dry_Concrete_Target_Temp | Current stage target temperature for concrete drying (°C) +TOP153 | main/Pool_Delta | Pool temperature delta (K) +TOP154 | main/SG_Ready_1_Heat_Capacity | SG Ready setting 1 heating capacity (%) +TOP155 | main/SG_Ready_1_DHW_Capacity | SG Ready setting 1 DHW capacity (%) +TOP156 | main/SG_Ready_2_Heat_Capacity | SG Ready setting 2 heating capacity (%) +TOP157 | main/SG_Ready_2_DHW_Capacity | SG Ready setting 2 DHW capacity (%) +TOP158 | main/Room_Operation_Max_Time | Maximum room operation time during alternating room/DHW operation (minutes) +TOP159 | main/DHW_Heatup_Max_Time | Maximum DHW heat-up time (minutes) +TOP160 | main/Quiet_Mode_Actual_State | Actual quiet mode state (0=off, 1=on) +TOP161 | main/Powerful_Mode_Actual_State | Actual powerful mode state (0=off, 1=on) +TOP162 | main/Heat_Cool_Switch_State | Actual heat/cool switch state (0=heat, 1=cool) +TOP163 | main/External_Switch_State | Actual external switch state (0=closed, 1=open) +TOP164 | main/Bivalent_Boiler_Contact_State | Actual bivalent boiler contact state (0=off, 1=on) +TOP165 | main/Z2_Active | Actual zone 2 activity (0=inactive, 1=active) +TOP166 | main/Z1_Active | Actual zone 1 activity (0=inactive, 1=active) +TOP167 | main/Backup_Heater_Enable | Backup heater custom-menu setting (0=inactive, 1=active) +TOP168 | main/Cooling_Enable | Cooling custom-menu setting (0=inactive, 1=active) +TOP169 | main/Heat_Request_Temp_With_Buffer | Heating water shift/direct target including buffer delta (°C) +TOP170 | gpio/relay/one/state | Onboard relay 1 GPIO output state (ESP32 only, 0=off, 1=on, retained) +TOP171 | gpio/relay/two/state | Onboard relay 2 GPIO output state (ESP32 only, 0=off, 1=on, retained) + +TOP170 and TOP171 are displayed on the main web page as Relay 1 and Relay 2 with On/Off and their full configured MQTT topics. They are read directly from GPIO21 and GPIO47, independent of MQTT connectivity, and exposed in the `/json` response's `relays` array. The page refreshes these states every five seconds. These values report GPIO output levels, not mechanical contact feedback. + +The topics above are relative to the configured MQTT base topic. New topics TOP144–TOP169 use the documented normal data block (0x10); availability depends on the heat pump model and configuration. Two-bit status fields use -1 for an unset encoding (00). Z1_Active and Z2_Active are individual activity bits, distinct from Zones_State. Quiet/Powerful actual states are distinct from the configured level/time topics. + @@ -253,6 +315,15 @@ SET47 | SetForceHeater | Force heater mode (emergency heating), same as the heat SET48 | SetReset | Reset/confirm active heatpump fault code (e.g. H72). Equivalent to pressing "Reset" on the CZ-TAW1 remote / indoor unit panel. Writes byte 8 of the outgoing query. Clears latched errors that soft power-cycle (`SetHeatpump` 0→1) cannot clear. | 0=no action, 1=reset SET49 | SetSterilizationTemp | Set DHW sterilization target temperature; experimental, L-series write acceptance is hardware-verified | integer 55-75 (degrees C) SET50 | SetSterilizationMaxTime | Set DHW sterilization holding time after reaching target temperature; experimental, L-series write acceptance is hardware-verified | 5, 10, 15, ..., 60 minutes; other values are rejected without sending a command +SET51 | SetHeatToCoolTemp | Outdoor temperature for automatic heat-to-cool switching; readback TOP79 | integer -127..127 °C (wire range) +SET52 | SetCoolToHeatTemp | Outdoor temperature for automatic cool-to-heat switching; readback TOP80 | integer -127..127 °C (wire range) +SET53 | SetRoomOperationMaxTime | Maximum room heating operation time when alternating with DHW; readback TOP158 | 30..7620 minutes, multiples of 30 (wire range) +SET54 | SetDHWHeatupMaxTime | Maximum DHW heat-up time; readback TOP159 | integer 1..254 minutes (wire range) +SET55 | SetQuietModeSchedule | Enable/disable the existing quiet-mode schedule; readback TOP3 | 0=off, 1=on + +SET51–SET55 require complete integer payloads; empty, malformed, out-of-range or invalid-step values are rejected without sending a telegram. The ranges above describe the nonzero protocol encoding, not verified installer-menu limits. Use settings supported by your model; the heat pump may reject values outside its own menu range. These writes follow the documented telegram examples but have not been hardware-tested here. SetQuietModeSchedule toggles the existing schedule and does not configure its times or change the quiet level. + +Example: publish `21` to `panasonic_heat_pump/commands/SetHeatToCoolTemp`, `90` to `panasonic_heat_pump/commands/SetRoomOperationMaxTime`, or `1` to `panasonic_heat_pump/commands/SetQuietModeSchedule` (replace the base topic as configured). *If you operate your heatpump in water mode with direct temperature setup: topics ending xxxRequestTemperature will set the absolute target temperature.* diff --git a/Manage-Topics.md b/Manage-Topics.md index 222a5a5a..4909cd27 100644 --- a/Manage-Topics.md +++ b/Manage-Topics.md @@ -1,5 +1,7 @@ # How to add MQTT Topics for HeishaMon +The steps below apply to decoded heat pump protocol fields. Device-generated monitoring topics, such as [`rules/state` and `rules/executed`](MQTT-Topics.md#rules-engine-status), are published separately and do not have protocol byte mappings or `@` rules variables. The ESP32 relay outputs are also device-generated; see [relay control](README.md#large-board-relay-control) for MQTT commands, REST `/command` calls, and `/json` readback. + 1. Add a new topic line in file [MQTT-Topics.md](MQTT-Topics.md) and define a new line on the table *Sensors Topics:* diff --git a/ProtocolByteDecrypt.md b/ProtocolByteDecrypt.md index 7e9e47e8..6091c1cc 100644 --- a/ProtocolByteDecrypt.md +++ b/ProtocolByteDecrypt.md @@ -1,5 +1,9 @@ ## Protocol byte decrypt info: +Byte numbers are decimal; example values are hexadecimal. For the documented bit-pair fields, bits 1–2 are the most significant pair (mask 0xC0), bits 3–4 use 0x30, bits 5–6 use 0x0C, and bits 7–8 use 0x03. The individual zone-activity bits in byte 117 use masks 0x02 (Z2) and 0x01 (Z1). + +TOP144–TOP169 expose the additional documented fields below; see MQTT-Topics.md for topic names and payloads. Byte 21 is published as its decimal configuration code, without inferring undocumented bit fields. Byte 60 (external-heater time), byte 176 (possible bypass valve), byte 191 (meaning of heat-pump power), and byte 192 (possible T-CAP indicator) remain unexposed pending verification. The conflicting RC-2 offsets (documented byte 200 versus existing decoder byte 128) are unchanged. + | Topic# | Byte# | Possible Value | Value decrypt | Value Description | | :---- | ---- | ---- | ----- | ----:| | | 00 | 71 | | Header | @@ -7,11 +11,11 @@ | | 02 | 01| | Header | | | 03 | 10 | | Header | | TOP0+TOP2 | 04 | 56 | Force DHW status 56=off,96=on, 55 = heat pump off, 56= heat pump on, Service Setup: Water pump on=65, Air Purge=75, Pump Down=F0?| Force dhw, Heat pump on/off, Service setup (Water Flow, Air Purge , Pump Down) | -| TOP19+TOP13+TOP68 | 05 | 55 | Holiday mode off/on (bit3and4), weekly shedule off/on (bit 1and2) force heater off/on (bit5and6) Dry Concrete off/on (bit7and8) | Holiday mode, Sheduler status, force heater state , Dry Concrete | +| TOP19+TOP13+TOP68+TOP144 | 05 | 55 | Holiday mode off/on (bit3and4), weekly shedule off/on (bit 1and2) force heater off/on (bit5and6) Dry Concrete off/on (bit7and8) | Holiday mode, Sheduler status, force heater state , Dry Concrete | | TOP94+TOP4 | 06 | 62 | 1st Bit = Zone2
2nd Bit = Zone1
3rd & 4th bit = b01 DHW off ,b10 DHW on
5th ,6th,7th & 8th bit = b0001 - only DHW , b0010 - Heat , b0011 - Cool , b1001 - Auto(Heat) , b1010 - Auto(Cool) | Zone on/off
Mode status | | TOP18+TOP17 | 07 | 49 | Left 5 bits = quiet level (0b01001 = Off, 0b01010 = level 1, 0b01011 = level 2, 0b01100 - level 3, 0b10001 = scheduled) last 3 bits is powermode level (0b001= Off, 0b010 - power mode 30min, 0b011 -60min, 0b100-90 min) | Quiet Mode status + Powerful mode status | | TOP | 08 | 00 | | 0 byte | -| TOP58+TOP59 | 09 | 05 | 3rd & 4th bit = b01 Standard, b10 - DHW Standard/Variable (J-series only)
5th & 6th bit = b01 DHW heater off, b10 - DHW heater on
7rd & 8th bit = b01 Water heater off, b10 - Water heater on | DHW capacity (J-series only)
Heaters enable allowed status| +| TOP58+TOP59+TOP145 | 09 | 05 | 3rd & 4th bit = b01 Standard, b10 - DHW Standard/Variable (J-series only)
5th & 6th bit = b01 DHW heater off, b10 - DHW heater on
7rd & 8th bit = b01 Water heater off, b10 - Water heater on | DHW capacity (J-series only)
Heaters enable allowed status| | TOP | 10 | 00 | | 0 byte | | TOP141+TOP143 | 11 | 00 | 3rd & 4th bit = b01 - Sound , b10 - Capacity
7th & 8th bit = b01 - DHW Top sensor , b10 - DHW Center Sensor | Quiet Mode Priority (K/L series)
DHW Sensor Selection (K/L series All-In-One only) | | TOP | 12 | 00 | | 0 byte | @@ -23,16 +27,16 @@ | TOP | 18 | 00 | | 0 byte | | TOP | 19 | 00 | | 0 byte | | TOP107/108/109/110 | 20 | 19 | 1st Bit = b0 Water , b1 Glycol
3rd & 4th bit = b01 Alternative Sensor Off ,b10 Alternative Sensor On
5rd & 6th bit = b01 Antifreezing Off ,b10 Antifreezing on
7rd & 8th bit = b01 Optional PCB Off ,b10 Optional PCB on
| Circulation liquid
,Alternative outdoor temp sensor
Anti freezing
Optional PCB | -| TOP | 21 | 15 | (hex) 15 - One Zone and Z1 as room , 19 - One Zone and Z1 as pool, 16 - Two Zones and Z2 as room, 26 - Two Zones ,Z2 as pool| No. of Zones and Zone Destination | +| TOP146 | 21 | 15 | (hex) 15 - One Zone and Z1 as room , 19 - One Zone and Z1 as pool, 16 - Two Zones and Z2 as room, 26 - Two Zones ,Z2 as pool| No. of Zones and Zone Destination | | TOP111+TOP112 | 22 | 11 |First digit -Z2 ,Second digit Z1 (hex) 1 - water temperature,2 - External Thermostat, 3 - Internal Thermostat, , 4 - Thermistor | Zone & sensor settings ( system setup - Installer ) | | TOP119/120/121/122 | 23 | 55 | 1st/2nd bit = external compressor control, 3rd/4th bit = external error signal, 5th/6h bit = heat/cool-switch, 7th/8th bit = external control switch | External control, heat/cool switch, error signal and External compressor control switch (menu settings)| | TOP140+TOP99+TOP100+TOP101 | 24 | 16 | 1st & 2nd bit Smart DHW (L series) (b01=Variable, b10=Standard)
3rd & 4th bit = solar buffer (0b01=no solar, 0b10=solar buffer, 0b11=solar dhw)
5th & 6th bit = buffer installed
7th & 8th bit = DHW installed | -| TOP114 | 25 | 5e | 1st & 2nd bit = 10
3rd & 4th bit = b01 no Pad Heater, b10 - Type A, b11 Type B
5th & 6th bit = b01 - Internal Heater 3kW, b10 - 6kW, b11 - 9kW
7th & 8th bit = b01 DHW Internal Heater , b10 - DHW External Heater | External Pad Heater
Power of internal heater
DHW heater Internal/External | +| TOP114+TOP147+TOP148 | 25 | 5e | 1st & 2nd bit = 10
3rd & 4th bit = b01 no Pad Heater, b10 - Type A, b11 Type B
5th & 6th bit = b01 - Internal Heater 3kW, b10 - 6kW, b11 - 9kW
7th & 8th bit = b01 DHW Internal Heater , b10 - DHW External Heater | External Pad Heater
Power of internal heater
DHW heater Internal/External | | TOP129/130/13/132 | 26 | 55 | (hex) Bivalent control, mode and ap settings. | Bivalent settings | -| TOP | 27 | 05 | SG Ready Control on/off (bit5and6) ,Demand Control on/off (bit7and8) | SG Ready Control, Demand Control | +| TOP149+TOP150 | 27 | 05 | SG Ready Control on/off (bit5and6) ,Demand Control on/off (bit7and8) | SG Ready Control, Demand Control | | TOP76+TOP81 | 28 | 09 | (hex) 09 - Compensation curve heat and direct cool, 05 - both compensation curves , 0a - direct heat and direct cool, 06 - heat direct, cool compensation curve | Operation Setup -Installer -water temperature heating on status and cooling | | TOP106 | 29 | 00 | 3rd & 4th bit = b01 - deltaT , b10 - Max. Duty | Pump flowrate (J/K/L series) | -| TOP139 | 30 | 00 | 5th & 6th bit = Heating Control (b01=Comfort, b10=Efficiency)
7th & 8th bit = DHW Defrost (b01=No, b10=Yes) | Heating Control (K/L series)
DHW Defrost (K/L series) | +| TOP139+TOP151 | 30 | 00 | 5th & 6th bit = Heating Control (b01=Comfort, b10=Efficiency)
7th & 8th bit = DHW Defrost (b01=No, b10=Yes) | Heating Control (K/L series)
DHW Defrost (K/L series) | | TOP | 31 | 00 | | 0 byte | | TOP | 32 | 00 | | 0 byte | | TOP | 33 | 00 | | 0 byte | @@ -48,7 +52,7 @@ | TOP45 | 43 | 71 | Convert to DEC 113-128 =-15 | Heat Shift for Holiday mode [°K] | | TOP25 | 44 | 71 | Convert to DEC 113-128 =-15 | Heat Shift for DHW mode [°K] | | TOP95 | 45 | 97 | (hex) 96 = 97 | Maximum set pump speed | -| TOP | 46 | 99 | Convert to DEC 153-128 = 25 | Dry concrete target temperature for actual stage [°C] | +| TOP152 | 46 | 99 | Convert to DEC 153-128 = 25 | Dry concrete target temperature for actual stage [°C] | | TOP | 47 | 00 | | 0 byte | | TOP | 48 | 00 | | 0 byte | | TOP | 49 | 00 | | 0 byte | @@ -60,7 +64,7 @@ | TOP | 55 | 00 | | 0 byte | | TOP | 56 | 00 | | 0 byte | | TOP | 57 | 00 | | 0 byte | -| TOP | 58 | 80 | Convert to DEC-128 | Delta T for Pool [°K] | +| TOP153 | 58 | 80 | Convert to DEC-128 | Delta T for Pool [°K] | | TOP113 | 59 | 85 | Convert to DEC-128 | Delta T for buffer tank [°K]| | TOP | 60 | 15 | Convert to DEC X-1 | Time set for external heaters 20min-3h, step 5min. | | TOP102 | 61 | 8a | Convert to DEC-128 | Solar Connection Set delta T for tank ON (DHW or Buffer) | @@ -73,10 +77,10 @@ | TOP136 | 68 | 7e | Convert to DEC-128 | Control pattern in Bivalent set temperature source to stop the bivalent heat source | | TOP137 | 69 | 1f | Convert to DEC X-1 | Bivalent Delay timer to stop the bivalent heat source | | TOP138 | 70 | 1f | Convert to DEC X-1 | Bivalent Control pattern for DHW delay timer to start the bivalent source | -| TOP | 71 | 79 | Convert to DEC X-1 |SG Setting 1 Heating Capacity | -| TOP | 72 | 79 | Convert to DEC X-1 |SG Setting 1 DHW Capacity | -| TOP | 73 | 8d | Convert to DEC X-1 |SG Setting 2 Heating Capacity | -| TOP | 74 | 8d | Convert to DEC X-1 |SG Setting 2 DHW Capacity | +| TOP154 | 71 | 79 | Convert to DEC X-1 |SG Setting 1 Heating Capacity | +| TOP155 | 72 | 79 | Convert to DEC X-1 |SG Setting 1 DHW Capacity | +| TOP156 | 73 | 8d | Convert to DEC X-1 |SG Setting 2 Heating Capacity | +| TOP157 | 74 | 8d | Convert to DEC X-1 |SG Setting 2 DHW Capacity | | TOP29 | 75 | 9e | Convert to DEC 158-128 =30 | Z1 Heating Curve Outlet Water Temperature Highest Set [°C] | | TOP30 | 76 | 96 | Convert to DEC 150-128 =22 | Z1 Heating Curve Outlet Water Temperature Lowest Set [°C] | | TOP32 | 77 | 71 | Convert to DEC 113-128 =-15 | Z1 Heating Curve Outside Temperature Lowest Set [°C] | @@ -99,8 +103,8 @@ | TOP24 | 94 | 82 | Convert to DEC 130-128 =2 | Floor cooling set delta [°C] | | TOP79 | 95 | 90 | Convert to DEC 144-128=16| Outdoor temperature for (heat to cool) [°C] | | TOP80 | 96 | 8b | Convert to DEC 139-128=11| Outdoor temperature for (cool to heat) [°C] | -| TOP | 97 | 05 | Donvert to DEC (X-1) x 30 | DHW settings - Room operation max time [min] | -| TOP | 98 | 65 | Convert to DEC 101-1=100 | DHW heat up time (max) [min] | +| TOP158 | 97 | 05 | Donvert to DEC (X-1) x 30 | DHW settings - Room operation max time [min] | +| TOP159 | 98 | 65 | Convert to DEC 101-1=100 | DHW heat up time (max) [min] | | TOP22 | 99 | 78 | Convert to DEC 120-128=-8 | DHW Delta for re-heat [°K] | | TOP70 | 100 | c1 | Convert to DEC 193-128=65 | Sterilization boiling temperature [°C] | | TOP71 | 101 | 0b | Convert to DEC 11 - 1 = 10 | Sterilization max operation time [min] | @@ -112,17 +116,17 @@ | TOP | 107 | 00 | | 0 byte | | TOP | 108 | 00 | | 0 byte | | TOP | 109 | 00 | | 0 byte | -| TOP | 110 | 55 | 1st & 2nd bit - Quiet mode
3rd&4th bit - Powerful
5th & 6th bit - Heat-Cool SW b10 - Cool
7th & 8th bit - External SW b10-Open | Actual/Real states of
Quiet
Powerful
Heat-Cool SW
External SW | +| TOP160+TOP161+TOP162+TOP163 | 110 | 55 | 1st & 2nd bit - Quiet mode
3rd&4th bit - Powerful
5th & 6th bit - Heat-Cool SW b10 - Cool
7th & 8th bit - External SW b10-Open | Actual/Real states of
Quiet
Powerful
Heat-Cool SW
External SW | | TOP20+TOP26 | 111 | 56 | right 2 bits: 0b10=DHW 0b01=Room 3-Way Valve. Next 2 bits (from right) is defrosting state (0b01 = defrosting not active, 0b10 = defrosting active) | 3 way valve + Defrost status | -| TOP60+TOP61 | 112 | 55 | 1st & 2nd bit - Boiler Contact
5th & 6th bit - External Heater
7th & 8th bit - Internal Heater (room or DHW)
( b01 - OFF, b10 - ON )| Real status of Bivalent Boiler contact and Heaters relays (Line under icons) | +| TOP60+TOP61+TOP164 | 112 | 55 | 1st & 2nd bit - Boiler Contact
5th & 6th bit - External Heater
7th & 8th bit - Internal Heater (room or DHW)
( b01 - OFF, b10 - ON )| Real status of Bivalent Boiler contact and Heaters relays (Line under icons) | | TOP44 | 113 | 21 | Hex B1 - F type error, A1 - H type error. After H error reset value 21, F error reset 31 | Error code type | | TOP44 | 114 | 53 | F45 error in HEX 56, calulation 45 treat as HEX and convert to DEC 69 + 17 = 86 (Hex 56) | Error code number | | TOP | 115 | 15 | | ? | | TOP123/124/125/126 | 116 | 5a | 1st & 2nd bit Zone 2 Pump ( b01 - OFF, b10 - ON ) , 3rd & 4th bit = Zone 1 Pump ( b01 - OFF, b10 - ON ), 5th & 6th bit = 2way Valve ( b01 - Cooling, b10 - Heating) , 7th & 8th bit = 3way Valve ( b01 - Room, b10 - Tank) | Z1 & Z2 Pump 2way & 3wa Valve staus | -| TOP69 | 117 | 05 | Sterilization on/off (bit5and6) , Z2 active (bit7) ,Z1 active (bit8)| Sterilization status Zone active information (look byte #6) | +| TOP69+TOP165+TOP166 | 117 | 05 | Sterilization on/off (bit5and6) , Z2 active (bit7) ,Z1 active (bit8)| Sterilization status Zone active information (look byte #6) | | TOP | 118 | 12 | | fractional info for TOP5 and TOP6 values | | TOP | 119 | 12 | | ? | -| TOP | 120 | 19 | 3rd & 4th bit = b01 Heater Inactive ,b10 Heater Active
5th & 6th bit = b01 Cooling Inactive ,b10 Cooling Active
| Custom menu settings
Back-up Heater
Cool Mode | +| TOP167+TOP168 | 120 | 19 | 3rd & 4th bit = b01 Heater Inactive ,b10 Heater Active
5th & 6th bit = b01 Cooling Inactive ,b10 Cooling Active
| Custom menu settings
Back-up Heater
Cool Mode | | TOP | 121 | 00 | | 0 byte | | TOP | 122 | 00 | | 0 byte | | TOP | 123 | 00 | | 0 byte | @@ -154,7 +158,7 @@ | TOP46 | 149 | 32 | Convert to DEC-128 | Buffer: Water Temperature [°C] | | TOP47 | 150 | 32 | Convert to DEC-128 | Solar: Water Temperature [°C] | | TOP48 | 151 | 32 | Convert to DEC-128| Pool: Water Temperature [°C] | -| TOP | 152 | 80 | Convert to DEC 128-128 = 0 | Water shift set or direct mode value Temperature For Heat Mode [°C] including Delta T for Buffer | +| TOP169 | 152 | 80 | Convert to DEC 128-128 = 0 | Water shift set or direct mode value Temperature For Heat Mode [°C] including Delta T for Buffer | | TOP7 | 153 | b7 | Convert to DEC-128 | Outlet Water Temperature (Target) [°C] | | TOP49 | 154 | af | Convert to DEC-128 | Outlet 2 heat exchanger water temperature [°C] | | TOP50 | 155 | cd | Convert to DEC-128 | Discharge Temperature [°C] | @@ -828,6 +832,8 @@ d0 7b 78 1f 7e 1f 1f 79 79 8d 8d 00 00 00 00 00 Operation Setup --> Installer +MQTT setters for the examples below: `SetHeatToCoolTemp` writes byte 95 as temperature + 128; `SetCoolToHeatTemp` writes byte 96 with the same encoding; `SetRoomOperationMaxTime` writes byte 97 as minutes / 30 + 1; `SetDHWHeatupMaxTime` writes byte 98 as minutes + 1. `SetQuietModeSchedule` writes byte 7 as 0x40 (off) or 0x80 (on), leaving the quiet-level and powerful-mode fields unset. Byte offsets include the four header bytes. See MQTT-Topics.md for validation and supported wire ranges. + Water temperature for heating on Compensationio Curve `f1 6c 01 10 00 00 00 00 00 00 00 00 00 00 00 00 diff --git a/README.md b/README.md index df882856..f6fe6385 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,20 @@ The software is also able to measure Watt on a S0 port of two kWh meters. You on Updating the firmware is as easy as going to the firmware menu and, after authentication with username 'admin' and password 'heisha' (or other provided during setup), uploading the binary there. -A json output of all received data (heatpump and 1wire) is available at the url http://heishamon.local/json (replace heishamon.local with the ip address of your heishamon device if MDNS is not working for you). +A JSON output of heat pump data and enabled 1-wire/S0 sensors is available at http://heishamon.local/json (replace heishamon.local with the IP address of your HeishaMon device if mDNS is not working). On the ESP32 large board, it also includes the two relay output states in the `relays` array. See [relay control](#large-board-relay-control) for reading and switching those outputs. Within the 'integrations' folder you can find examples how to connect your automation platform to the HeishaMon. # Rules functionality The rules functionality allows you to control the heatpump from within the HeishaMon itself. Which makes it much more reliable then having to deal with external domotica over WiFi. When posting a new ruleset, it is immidiatly validated and when valid used. When a new ruleset is invalid it will be ignored and the old ruleset will be loaded again. You can check the console for feedback on this. If somehow a new valid ruleset crashes the HeishaMon, it will be automatically disabled the next reboot, allowing you to make changes. This prevents the HeishaMon getting into a boot loop. +Rules monitoring is available through two read-only MQTT topics on both ESP8266 and ESP32: + +- `/rules/state` is retained and reports `1` when at least one rule block is loaded, or `0` otherwise. It is sent on connection, changes, and the full-data resend interval. The existing `/stats` field `"rules active"` gives the number of loaded blocks. Use `/LWT` to check device availability. +- `/rules/executed` publishes a non-retained JSON event after an execution returns. It includes the block name and ID, interpreter result, device uptime, and whether it is a nested helper call. Outer invocations also include elapsed microseconds. Validation does not generate events; disconnected events are not replayed. + +The default base is `panasonic_heat_pump`. See [rules status](MQTT-Topics.md#rules-engine-status) and [execution events](MQTT-Topics.md#rule-execution-events) for the payload format and limitations. These are monitoring topics for MQTT clients, not `@` variables or commands in a ruleset. + The techniques used in the rule library allows you to work with very large rulesets, but best practice is to keep it below 10.000 bytes. Notice that sending commands to the heatpump is done asynced. So, commands sent to the heatpump at the beginning of your syntax will not immediatly be reflected in the values from the heatpump later on. Therefor, heatpump values should be read from the heatpump itself instead of those based on the values you keep yourself. @@ -62,7 +69,7 @@ These variables live inside a rule block. When a rule block finishes, these vari These are the same as listed in the Manage Topics documentation page and as found on the HeishaMon homepage. The ruleset also follows the R/W logic as used through the MQTT and REST API. That means that the read topics differ from the write topics. So reading the heatpump state is done through `@Heatpump_State`, changing the heatpump state through `@SetHeatpump`. - `%`: Datetime variables -These can be used for date and time based rules. Currently `%hour` (0 - 23), `%minute` (0 - 59), `%month` (1 - 12), and `%day` (1 - 7) are supported. All are plain integers. A proper NTP configuration is needed to set the correct system date and time on the HeishaMon. +These can be used for date and time based rules. Currently `%hour` (0 - 23), `%minute` (0 - 59), `%month` (1 - 12), and `%day` (1 - 7) are supported. All are plain integers. A proper NTP configuration is needed to set the correct system date and time on the HeishaMon. - `?`: Thermostat parameters These variables reflect parameters read from the connected thermostat when using the OpenTherm functionality. When OpenTherm is supported this documentation will be extended with more precise information. The can check the opentherm tab for the variables that can be used. The names are the same for reading and writing, but not all values support reading and/or writing. The opentherm tab also lists this. @@ -368,8 +375,24 @@ As until today we don't know how the commands sent to the heatpump are processed The software also supports ds18b20 1-wire temperature sensors reading. A proper 1-wire configuration (with 4.7kohm pull-up resistor) connected to GPIO4 will be read each configured secs (minimal 5) and send at the panasonic_heat_pump/1wire/"sensor-hex-address" topic. On the pre-made boards this 4.7kohm resistor is already installed. ## Large board relay control + +Relay output states are published as `0` (off) or `1` (on), with MQTT retain, to `/gpio/relay/one/state` and `/gpio/relay/two/state`. The default base is `panasonic_heat_pump`. Both states are published after an MQTT or REST relay command, on MQTT connection, and at the configured full-data resend interval (`updateAllTime`). Changes made by rules are detected and published at the end of the main loop. Failed publications are retried. These values report the GPIO output levels, not mechanical contact feedback. + The newer, large, heishamon contains two onboard relays which can be switched on and off using MQTT commands. The relays can be used for any contact switching, even 230V mains (max 5A). For example to switch the 230V contacts in the heatpump for controlling the 'external thermostat', switching a pump on or off or other lower power devices. I do not recommend to use the relay as a switch for a electric heater as they use too much power. To control the relay just send a value of 1 or 0 to the MQTT topic "panasonic_heat_pump/gpio/relay/one" for relay one or "panasonic_heat_pump/gpio/relay/two" for relay two. +The ESP32 large board also supports relay control through the existing HTTP REST `/command` endpoint (GET): + +| Action | Request | +| --- | --- | +| Relay 1 on | `http:///command?SetRelayOne=1` | +| Relay 1 off | `http:///command?SetRelayOne=0` | +| Relay 2 on | `http:///command?SetRelayTwo=1` | +| Relay 2 off | `http:///command?SetRelayTwo=0` | + +Both parameters may be combined, for example `/command?SetRelayOne=1&SetRelayTwo=0`. The plain-text response reports the resulting output level, for example `SetRelayOne: 1`. As with MQTT relay commands, `1`, `true`, `on`, and `enable` turn the relay on (text is case-insensitive); other values turn it off. REST switching works without an MQTT connection and publishes retained relay states when MQTT is connected. These commands are only available on ESP32. + +Read both output states with `GET http:///json`: the `relays` array contains `Name` (`Relay 1` / `Relay 2`) and numeric `Value` (`0` / `1`). These are GPIO output levels, not mechanical contact feedback. + ## Opentherm support If your heishamon board supports opentherm the software can also be used to bridge opentherm information from a compatible thermostat to your home automation over MQTT or JSON and as mentioned above it can also be connected directly in the rules to connect opentherm information to the heatpump and back, for example to display the outside temperature from the heatpump on your opentherm thermostat. If you enable opentherm support in settings there will be a new tab visible in the web page. On that tab you will see opentherm values. Some are of type R(ead) and some are W(rite), and some are both. Read means that the thermostat can read that information from the heishamon. You provide that information over MQTT (or using the rules) by updating this value on the mqtt 'opentherm/read' topic, for example 'panasonic_heat_pump/opentherm/read/outsideTemp'. The write values are information from the thermostat, like 'roomTemp'. These are available on mqtt topic 'opentherm/write'. You can use these values to change the heatpump behaviour in anyway you want using your home automation and mqtt set-commands to heishamon on using the internal rules. diff --git a/README_DE.md b/README_DE.md index 064d10b1..5dcd7637 100644 --- a/README_DE.md +++ b/README_DE.md @@ -40,6 +40,10 @@ Im Ordner 'integrations' findest du Beispiele, wie du deine Automatisierungsplat # Regeln-Funktionalität Die Regeln-Funktionalität ermöglicht es dir, die Wärmepumpe direkt vom HeishaMon aus zu steuern. Das macht es viel zuverlässiger als die Steuerung über externe Hausautomatisierung über WLAN. Wenn ein neues Regelwerk veröffentlicht wird, wird es sofort validiert und bei Gültigkeit übernommen. Wenn ein neues Regelwerk ungültig ist, wird es ignoriert und das alte Regelwerk wieder geladen. Du kannst die Konsole auf Rückmeldungen überprüfen. Falls ein neues gültiges Regelwerk den HeishaMon zum Absturz bringt, wird es beim nächsten Neustart automatisch deaktiviert, sodass du Änderungen vornehmen kannst. Dies verhindert eine Boot-Schleife des HeishaMon. +Zur Überwachung gibt es die schreibgeschützten MQTT-Topics `/rules/state` und `/rules/executed`. `rules/state` meldet mit Retain `1`, wenn mindestens ein Regelblock geladen ist, andernfalls `0`. `rules/executed` meldet nach einer Regelausführung ein JSON-Ereignis ohne Retain mit Regelname, Block-ID, Ergebniscode, Geräte-Uptime und der Kennzeichnung verschachtelter Hilfsblöcke. Bei äußeren Aufrufen wird zusätzlich die Laufzeit in Mikrosekunden mitgesendet. Der Standardwert für `` ist `panasonic_heat_pump`. Payload-Details und Einschränkungen stehen in der [MQTT-Dokumentation](MQTT-Topics.md#rules-engine-status). + +Nicht jedes MQTT-Topic erhält eine `TOP…`-Nummer: `TOP…` kennzeichnet die normalen decodierten Wärmepumpenwerte, `XTOP…` den zusätzlichen Datenblock und `OPT…` die optionale Platine. Geräteinformationen und Ereignisse wie `LWT`, `log`, `stats`, `rules/state` und `rules/executed` werden separat ohne TOP-Nummer dokumentiert. Die ESP32-Relaiszustände sind eine gesonderte Erweiterung mit `TOP170` und `TOP171` für die Web-/JSON-Anzeige. Die beiden Rules-Topics sind MQTT-Meldungen für externe Abonnenten und keine `@`-Variablen im Regelwerk. + Die im Regelwerk verwendeten Techniken ermöglichen die Arbeit mit sehr großen Regelwerken, aber Best Practice ist es, sie unter 10.000 Bytes zu halten. Beachte, dass das Senden von Befehlen an die Wärmepumpe asynchron erfolgt. Befehle, die zu Beginn deiner Syntax an die Wärmepumpe gesendet werden, spiegeln sich nicht sofort in den Werten der Wärmepumpe wider. Daher sollten Wärmepumpen-Werte direkt von der Wärmepumpe gelesen werden, nicht aus selbst gespeicherten Werten. @@ -364,6 +368,16 @@ Bis heute wissen wir nicht, wie die an die Wärmepumpe gesendeten Befehle in der Die Software unterstützt auch das Auslesen von DS18B20 1-Wire-Temperatursensoren. Eine korrekte 1-Wire-Konfiguration (mit 4,7kOhm Pull-up-Widerstand) an GPIO4 wird alle konfigurierten Sekunden (mindestens 5) gelesen und an das Topic panasonic_heat_pump/1wire/"sensor-hex-adresse" gesendet. Auf den vorgefertigten Boards ist dieser 4,7kOhm-Widerstand bereits installiert. ## Relaissteuerung auf der großen Platine + +Die Firmware veröffentlicht die ausgelesenen Relais-Ausgangszustände als `0` (aus) oder `1` (ein) mit MQTT-Retain: + +| Relais | Status-Topic (bei Standard-Basistopic) | +| --- | --- | +| 1 | `panasonic_heat_pump/gpio/relay/one/state` | +| 2 | `panasonic_heat_pump/gpio/relay/two/state` | + +Die Meldung erfolgt direkt nach einem MQTT-Schaltbefehl, bei jeder MQTT-Verbindung und regelmäßig im konfigurierten Intervall für die vollständige Datenversendung (`updateAllTime`). Änderungen durch die Rules Engine werden am Ende des Hauptschleifendurchlaufs erkannt und veröffentlicht. Bei einem anderen MQTT-Basistopic wird dieses auch für die Status-Topics verwendet. Der Status bestätigt den GPIO-Ausgangszustand; eine mechanische Rückmeldung des Relaiskontakts gibt es nicht. + Der neuere, große, Heishamon enthält zwei eingebaute Relais, die über MQTT-Befehle ein- und ausgeschaltet werden können. Die Relais können für jede Kontaktschaltung verwendet werden, auch 230V Netzspannung (max. 5A). Um das Relais zu steuern, sende einfach einen Wert von 1 oder 0 an das MQTT-Topic "panasonic_heat_pump/gpio/relay/one" für Relais eins oder "panasonic_heat_pump/gpio/relay/two" für Relais zwei. ## Benutzerkonfigurierbare Extra-GPIOs diff --git a/Tools/test_commands.py b/Tools/test_commands.py new file mode 100644 index 00000000..c237ac3f --- /dev/null +++ b/Tools/test_commands.py @@ -0,0 +1,140 @@ +"""Test the new setters and real command dispatch with g++ on the host. + +Run: python3 Tools/test_commands.py +""" + +import re +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SETTERS = { + "SetHeatToCoolTemp": ( + 95, + [("21", 149), ("-127", 1), ("127", 255), ("0", 128)], + ["-128", "128"], + ), + "SetCoolToHeatTemp": ( + 96, + [("12", 140), ("-127", 1), ("127", 255)], + ["-128", "128"], + ), + "SetRoomOperationMaxTime": ( + 97, + [("90", 4), ("30", 2), ("7620", 255)], + ["0", "29", "31", "7650"], + ), + "SetDHWHeatupMaxTime": ( + 98, + [("105", 106), ("1", 2), ("254", 255)], + ["0", "255", "-1"], + ), + "SetQuietModeSchedule": (7, [("0", 64), ("1", 128)], ["2", "-1"]), +} + +SHIM = r""" +#include +#include +#include +#include +using byte = unsigned char; +#define PROGMEM +#define memcpy_P memcpy +#define snprintf_P snprintf +#define PSTR(x) x +std::vector sent; +int send_count; +bool send_command(byte* data, int length) { + ++send_count; sent.assign(data, data + length); return true; +} +void log_message(char*) {} +""" + + +def main(): + header = (ROOT / "HeishaMon/commands.h").read_text() + source = (ROOT / "HeishaMon/commands.cpp").read_text() + docs = (ROOT / "MQTT-Topics.md").read_text() + additions = source[ + source.index("static unsigned int set_integer_field") : source.index( + "unsigned int set_sterilization_temp" + ) + ] + real_functions = set(re.findall(r"unsigned int (\w+)\(", additions)) + stubs = [] + for name, args in re.findall(r"unsigned int (set_\w+)\(([^;]+)\);", header): + if name not in real_functions: + stubs.append(f"unsigned int {name}({args}) {{ return 0; }}") + template = re.search(r"byte panasonicSendQuery\[\].*?\};", source, re.S)[0] + dispatcher = source[ + source.index("void send_heatpump_command(") : source.index( + "bool saveOptionalPCB(" + ) + ] + header = re.sub(r"^#include.*$", "", header, flags=re.M) + checks = [] + cases = 0 + for topic, (address, accepted, rejected) in SETTERS.items(): + assert f"| {topic} |" in docs + function = re.search(r'\{\s*"' + topic + r'",\s*(\w+)\s*\}', header)[1] + for payload, encoded in accepted: + checks.append( + f""" + {{ char topic[] = "{topic}", payload[] = "{payload}"; + sent.clear(); send_count = 0; + send_heatpump_command(topic, payload, send_command, log_message, false); + assert(send_count == 1 && sent.size() == sizeof(panasonicSendQuery)); + for (unsigned int i = 0; i < sent.size(); ++i) + assert(sent[i] == (i == {address} ? {encoded} : panasonicSendQuery[i])); + }} +""" + ) + cases += 1 + for payload in rejected + [ + "", + "abc", + "1x", + "1.0", + " 1", + "+1", + "1 ", + "-", + "999999999999999", + ]: + checks.append( + f""" + {{ char topic[] = "{topic}", payload[] = "{payload}"; + sent.clear(); send_count = 0; + send_heatpump_command(topic, payload, send_command, log_message, false); + assert(send_count == 0 && sent.empty()); + byte command[256]; memset(command, 0xa5, sizeof(command)); + char log[256]; + assert({function}(payload, command, log) == 0); + for (byte value : command) assert(value == 0xa5); + }} +""" + ) + cases += 1 + checks.append( + f""" + {{ byte command[256]; memset(command, 0xa5, sizeof(command)); char log[256]; + assert({function}(nullptr, command, log) == 0); + for (byte value : command) assert(value == 0xa5); + }} +""" + ) + cases += 1 + program = SHIM + header + template + additions + "\n".join(stubs) + dispatcher + program += "\nint main() {\n" + "".join(checks) + "\n}\n" + with tempfile.TemporaryDirectory() as directory: + cpp = Path(directory) / "test.cpp" + binary = Path(directory) / "test" + cpp.write_text(program) + subprocess.run(["g++", "-std=c++17", str(cpp), "-o", str(binary)], check=True) + subprocess.run([str(binary)], check=True) + print(f"PASS: {cases} setter/dispatch cases (bytes, boundaries, rejected payloads)") + + +if __name__ == "__main__": + main() diff --git a/Tools/test_decode.py b/Tools/test_decode.py new file mode 100644 index 00000000..c57ba5d4 --- /dev/null +++ b/Tools/test_decode.py @@ -0,0 +1,193 @@ +"""Compile the real decoder with a small host shim and test MQTT output. + +Run with python3 Tools/test_decode.py (requires g++). +""" + +import os +import re +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +SHIM = r""" +#include +#include +#include +#include +#include +#include +#include +#include +using byte = unsigned char; +class String { + std::string value; +public: + String() {} + String(const char* s): value(s) {} + template::value, int>::type = 0> + String(T n): value(std::to_string(n)) {} + String(float n, int digits) { + char buf[64]; snprintf(buf, sizeof(buf), "%.*f", digits, n); value = buf; + } + const char* c_str() const { return value.c_str(); } + int toInt() const { return atoi(c_str()); } + String operator+(const char* rhs) const { return String((value + rhs).c_str()); } + bool operator!=(const String& rhs) const { return value != rhs.value; } +}; +#define PROGMEM +#define PSTR(x) x +#define _F(x) x +#define memcpy_P memcpy +#define sprintf_P sprintf +unsigned long millis() { return 1; } +unsigned int word(char high, char low) { + return (static_cast(high) << 8) | static_cast(low); +} +struct Publication { std::string topic, value; bool retained; }; +class PubSubClient { +public: + std::vector messages; + bool publish(const char* topic, const char* value, bool retained) { + messages.push_back({topic, value, retained}); return true; + } +}; +constexpr int DATASIZE = 203, OPTDATASIZE = 20; +byte optionalPCBQuery[20]; +const char* mqtt_topic_values = "main"; +const char* mqtt_topic_xvalues = "extra"; +const char* mqtt_topic_pcbvalues = "optional"; +void websocket_write_all(char*, uint16_t) {} +void rules_event_cb(const char*, const char*) {} +void log_message(char*) {} +""" + +# Captured/documented encodings, including independent fields sharing bytes. +CASES = [ + (144, 5, 0x56, "1"), + (145, 9, 0x25, "1"), + (146, 21, 0x26, "38"), + (147, 25, 0x5E, "9"), + (148, 25, 0x5E, "1"), + (149, 27, 0x09, "1"), + (150, 27, 0x06, "1"), + (151, 30, 0x06, "1"), + (152, 46, 0x99, "25"), + (153, 58, 0x85, "5"), + (154, 71, 0x79, "120"), + (155, 72, 0x79, "120"), + (156, 73, 0x8D, "140"), + (157, 74, 0x8D, "140"), + (158, 97, 0x05, "120"), + (159, 98, 0x65, "100"), + (160, 110, 0x95, "1"), + (161, 110, 0x65, "1"), + (162, 110, 0x59, "1"), + (163, 110, 0x56, "1"), + (164, 112, 0x95, "1"), + (165, 117, 0x06, "1"), + (166, 117, 0x05, "1"), + (167, 120, 0x29, "1"), + (168, 120, 0x19, "1"), + (169, 152, 0x7B, "-5"), +] + + +def main(): + header = (ROOT / "HeishaMon/decode.h").read_text() + source = (ROOT / "HeishaMon/decode.cpp").read_text() + docs = (ROOT / "MQTT-Topics.md").read_text() + count = int(re.search(r"#define NUMBER_OF_TOPICS (\d+)", header)[1]) + tables = {} + for name in ("topics", "topicBytes", "topicFunctions", "topicDescription"): + body = re.search(r"\b" + name + r"\[\].*?=\s*\{(.*?)\};", header, re.S)[1] + tables[name] = [ + line.split("//")[0].strip().rstrip(",") + for line in body.splitlines() + if line.split("//")[0].strip() + ] + assert len(tables[name]) == count, name + assert len(set(tables["topics"])) == count + assert all(len(name.strip('"')) < 42 for name in tables["topics"]) + checks = [] + for topic, address, raw, expected in CASES: + name = tables["topics"][topic].strip('"') + assert f"TOP{topic} | main/{name} |" in docs + checks.append( + f""" + memset(data, 0x55, sizeof(data)); + data[{address}] = {raw}; + assert(std::string(getDataValue(data, {topic}).c_str()) == "{expected}"); + resetlastalldatatime(); client.messages.clear(); + decode_heatpump_data(data, previous, client, log_message, base, 60); + assert(client.messages.size() == NUMBER_OF_TOPICS); + assert(client.messages[{topic}].topic == "test/main/{name}"); + assert(client.messages[{topic}].value == "{expected}"); + assert(client.messages[{topic}].retained); + client.messages.clear(); + decode_heatpump_data(data, previous, client, log_message, base, 60); + assert(client.messages.empty()); +""" + ) + checks.append( + """ + data[152] = 0x80; + client.messages.clear(); + decode_heatpump_data(data, previous, client, log_message, base, 60); + assert(client.messages.size() == 1); + assert(client.messages[0].topic == "test/main/Heat_Request_Temp_With_Buffer"); + assert(client.messages[0].value == "0"); + assert(getHeaterCapacity(0).toInt() == -1); + assert(getHeaterCapacity(4).toInt() == 3); + assert(getHeaterCapacity(8).toInt() == 6); + assert(getIntMinus1Times30(0).toInt() == -30); + assert(getIntMinus1Times30(255).toInt() == 7620); + for (int n = 0; n < 4; ++n) { + assert(getZone1Active(n).toInt() == (n & 1)); + assert(getZone2Active(n).toInt() == ((n >> 1) & 1)); + } + assert(getBit1and2(0).toInt() == -1); + assert(getBit3and4(0).toInt() == -1); + assert(getBit5and6(0).toInt() == -1); + assert(getBit7and8(0).toInt() == -1); +""" + ) + header = re.sub(r"^#include.*$", "", header, flags=re.M) + source = re.sub(r"^#include.*$", "", source, flags=re.M) + program = ( + SHIM + + header + + source + + """ +int main() { + char data[DATASIZE], previous[DATASIZE] = {}, base[] = "test"; + PubSubClient client; +""" + + "".join(checks) + + "\n}\n" + ) + with tempfile.TemporaryDirectory() as directory: + cpp = Path(directory) / "test.cpp" + binary = Path(directory) / "test" + cpp.write_text(program) + subprocess.run( + [ + os.environ.get("CXX", "g++"), + "-std=c++17", + "-funsigned-char", + "-Wno-narrowing", + str(cpp), + "-o", + str(binary), + ], + check=True, + ) + subprocess.run([str(binary)], check=True) + print( + f"PASS: {len(CASES)} decoder/MQTT cases, tables, documentation and boundaries" + ) + + +if __name__ == "__main__": + main()