diff --git a/HeatPumpType.md b/HeatPumpType.md index 114fec8e..741f4181 100644 --- a/HeatPumpType.md +++ b/HeatPumpType.md @@ -36,7 +36,7 @@ Assuming that bytes from #129 to #138 are unique for each model of Aquarea heat |29 | E2 CF 0B 83 05 12 D0 0D 92 05 | WH-SQC12H9E8 | WH-UQ12HE8 | KIT-WQC12H9E8 | 12 | 3ph | T-CAP - Super Quiet | |30 | E2 CF 0C 78 09 12 D0 0B 06 11 | WH-SXC12H6E5 | WH-UX12HE5 | KIT-WXC12H6E5 | 12 | 1ph | T-CAP | |31 | C2 D3 0C 35 65 B2 D3 0B 96 65 | Monoblock | WH-MDC09J3E5 | Monoblock | 9 | 1ph | HP (new version?) | -|32 | 32 D4 0B 99 77 62 90 0B 01 78 | Monoblock | WH-MXC09J3E5 | Monoblock | 9 | 1ph | T-CAP +|32 | 32 D4 0B 99 77 62 90 0B 01 78 | Monoblock | WH-MXC09J3E5 | Monoblock | 9 | 1ph | T-CAP | |33 | 42 D4 0B 15 76 12 D0 0B 10 11 | WH-ADC1216H6E5C | WH-UD12HE5 | KIT-ADC12HE5C-CL | 12 | 1ph| HP - All-In-One Compact | |34 | E2 D5 0C 29 99 83 92 0C 28 98 | WH-ADC0509L3E5 | WH-WDG07LE5 | KIT-ADC07L3E5 | 7 | 1ph | HP - All-In-One L-series | |35 | E2 CF 0D 85 05 12 D0 0E 94 05 | WH-SXC09H3E8 | WH-UX09HE8 | KIT-WXC09H3E8 | 9 | 3ph | T-CAP - new version | @@ -60,6 +60,8 @@ Assuming that bytes from #129 to #138 are unique for each model of Aquarea heat |53 | 42 D4 0B 83 71 32 D2 0C 44 55 | WH-ADC0309J3E5C | WH-UD03JE5 | KIT-ADC03JE5C-S | 3.2 | 1ph | HP - All-In-One Compact | |54 | E2 CF 0C 74 09 12 D0 0E 94 05 | WH-ADC0916H9E8 | WH-UX09HE8 | KIT-AXC9HE8 | 9 | 3ph | T-CAP - All-In-One | |55 | 12 D7 0D 98 11 33 94 0C 83 10 | WH-ADC0316M9E8AN2 | WH-WXG09ME8 | Monoblock | 9 | 2ph | T-CAP - M-series DHW 185l | +|56 | E2 D5 0B 08 95 02 D6 0F 67 95 | WH-SDC0309K3E5 | WH-UDZ07KE5 | KIT-SDC07KE5 | 7 | 1ph | HP - split K-series (sold in Poland) | +|57 | E2 D5 0D 36 99 02 D6 10 66 95 | WH-ADC0309K3E5AN | WH-UDZ05KE5 | KIT-ADC05K3E5AN | 5 | 1ph | HP - All-In-One K-series - AN | All bytes are used for Heat Pump model identification in the code. diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index ad16a391..9dc216b0 100644 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -132,9 +132,19 @@ static uint8_t cmdnrel = 0; // mqtt +#ifdef TLS_SUPPORT +#include +WiFiClientSecure mqtt_tls_client; WiFiClient mqtt_wifi_client; +bool loadTlsCaFromFS(WiFiClientSecure &client); +static bool last_tls_enabled = false; +static bool new_ca_stored = false; +static std::unique_ptr persistent_ca_pem; PubSubClient mqtt_client; - +#else +WiFiClient mqtt_wifi_client; +PubSubClient mqtt_client(mqtt_wifi_client); +#endif bool firstConnectSinceBoot = true; //if this is true there is no first connection made yet @@ -307,6 +317,34 @@ void check_wifi() { } +#ifdef TLS_SUPPORT +bool loadTlsCaFromFS(WiFiClientSecure &client) { + if (!LittleFS.exists("/ca.pem")) { + log_message(_F("[TLS] /ca.pem not found")); + return false; + } + File certFile = LittleFS.open("/ca.pem", "r"); + if (!certFile) { + log_message(_F("[TLS] open(/ca.pem) failed")); + return false; + } + size_t certSize = certFile.size(); + if (certSize == 0) { + log_message(_F("[TLS] /ca.pem is empty")); + certFile.close(); + return false; + } + persistent_ca_pem.reset(new char[certSize + 1]); + size_t n = certFile.readBytes(persistent_ca_pem.get(), certSize); + persistent_ca_pem[n] = '\0'; + certFile.close(); + client.setCACert(persistent_ca_pem.get()); + log_message(_F("[TLS] CA loaded into client")); + return true; +} +#endif + + void mqtt_reconnect() { unsigned long now = millis(); @@ -319,6 +357,36 @@ void mqtt_reconnect() } char topic[256]; sprintf(topic, "%s/%s", heishamonSettings.mqtt_topic_base, mqtt_willtopic); +#ifdef TLS_SUPPORT + if (heishamonSettings.mqtt_tls_enabled != last_tls_enabled) { + mqtt_client.disconnect(); + if (last_tls_enabled) { + mqtt_tls_client.stop(); + } else { + mqtt_wifi_client.stop(); + if (!loadTlsCaFromFS(mqtt_tls_client)) { + log_message(_F("[TLS] Proceeding without valid CA (expect failure)")); + } + } + last_tls_enabled = heishamonSettings.mqtt_tls_enabled; + } + + if (new_ca_stored) { + log_message(_F("[TLS] Trying to load new CA ertificate")); + if (!loadTlsCaFromFS(mqtt_tls_client)) { + log_message(_F("[TLS] Proceeding without valid CA (expect failure)")); + } + new_ca_stored = false; + } + if (heishamonSettings.mqtt_tls_enabled) { + mqtt_client.setClient(mqtt_tls_client); + } else { + mqtt_client.setClient(mqtt_wifi_client); + } + mqtt_client.setSocketTimeout(10); + mqtt_client.setKeepAlive(30); + mqtt_client.setServer(heishamonSettings.mqtt_server, atoi(heishamonSettings.mqtt_port)); +#endif if (mqtt_client.connect(heishamonSettings.wifi_hostname, heishamonSettings.mqtt_username, heishamonSettings.mqtt_password, topic, 1, true, "Offline")) { mqttReconnects++; @@ -363,6 +431,20 @@ void mqtt_reconnect() } #endif } +//#ifdef TLS_SUPPORT // error state is useful in any case + else { + int8_t err = mqtt_client.state(); + log_message(_F("MQTT connect failed, state:")); + switch (err) { + case -1: log_message(_F(" -1 → TLS handshake or network error")); break; + case -2: log_message(_F(" -2 → Connection timeout – cannot reach broker or CA/time error")); break; + case -3: log_message(_F(" -3 → Server not found or rejected")); break; + case -4: log_message(_F(" -4 → Connection lost")); break; + case -5: log_message(_F(" -5 → Check username/password")); break; + default: log_message(_F(" → Unknown error")); break; + } + } +//#endif } } @@ -809,6 +891,18 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { LittleFS.remove("/rules.new"); client->userdata = new File(LittleFS.open("/rules.new", "a+")); } +#ifdef TLS_SUPPORT + } else if (strcmp_P((char *)dat, PSTR("/cacert")) == 0) { + client->route = 165; + if (LittleFS.begin()) { + LittleFS.remove("/ca.tmp"); + File cf = LittleFS.open("/ca.tmp", "w"); + if (cf) { + client->userdata = new File(cf); + } + new_ca_stored = true; + } +#endif } else if (strcmp_P((char *)dat, PSTR("/firmware")) == 0) { if (!Update.isRunning()) { #ifdef ESP8266 @@ -836,6 +930,10 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { client->route = 140; } else if (strcmp_P((char *)dat, PSTR("/rules")) == 0) { client->route = 160; +#ifdef TLS_SUPPORT + } else if (strcmp_P((char *)dat, PSTR("/cacert")) == 0) { + client->route = 166; +#endif } else if (strcmp_P((char *)dat, PSTR("/scandallas")) == 0) { client->route = 180; } else { @@ -940,6 +1038,15 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { f->write(args->value, args->len); } } break; +#ifdef TLS_SUPPORT + case 165: { + File *f = (File *)client->userdata; + if (f && *f && args->len > 0) { + f->write((const uint8_t*)args->value, (size_t)args->len); + } + return 0; + } break; +#endif } } break; case WEBSERVER_CLIENT_HEADER: { @@ -1065,6 +1172,20 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { case 160: { return showRules(client); } break; +#ifdef TLS_SUPPORT + case 165: { + if (client->userdata) { + File *pf = (File *)client->userdata; + pf->close(); + delete pf; + client->userdata = NULL; + } + return handleCACert(client); + } break; + case 166: { + return showCACert(client); + } break; +#endif case 170: { File *f = (File *)client->userdata; if (f) { @@ -1127,6 +1248,9 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { } } break; case 160: +#ifdef TLS_SUPPORT + case 165: +#endif case 170: { if (client->userdata != NULL) { File *f = (File *)client->userdata; @@ -1270,9 +1394,21 @@ void switchSerial() { } void setupMqtt() { - mqtt_client.setClient(mqtt_wifi_client); mqtt_client.setBufferSize(1024); +#ifdef TLS_SUPPORT + mqtt_client.setSocketTimeout(8); mqtt_client.setKeepAlive(30); //fast timeout, any slower than 10s will block the main loop too long (8s might be even safer to avoid reboots on bad wifi); short keepalive may lead to problems with TLS + if (heishamonSettings.mqtt_tls_enabled) { + if (!loadTlsCaFromFS(mqtt_tls_client)) { + log_message(_F("[TLS] Proceeding without valid CA (expect failure)")); + } + mqtt_client.setClient(mqtt_tls_client); + } else { + mqtt_client.setClient(mqtt_wifi_client); + } + last_tls_enabled = heishamonSettings.mqtt_tls_enabled; +#else mqtt_client.setSocketTimeout(10); mqtt_client.setKeepAlive(5); //fast timeout, any slower will block the main loop too long +#endif mqtt_client.setServer(heishamonSettings.mqtt_server, atoi(heishamonSettings.mqtt_port)); mqtt_client.setCallback(mqtt_callback); } @@ -1447,8 +1583,9 @@ void setup() { setupETH(); #endif - loggingSerial.println(F("Setup MQTT...")); - setupMqtt(); +// loggingSerial.println(F("Setup MQTT...")); +// setupMqtt(); +//shifted - TLS requires correct time loggingSerial.println(F("Setup HTTP...")); setupHttp(); @@ -1458,6 +1595,9 @@ void setup() { sntp_setoperatingmode(SNTP_OPMODE_POLL); sntp_init(); + loggingSerial.println(F("Setup MQTT...")); + setupMqtt(); + loggingSerial.println(F("Switch serial...")); switchSerial(); //switch serial to gpio13/gpio15 diff --git a/HeishaMon/commands.cpp b/HeishaMon/commands.cpp index 20c79151..f67fc43c 100644 --- a/HeishaMon/commands.cpp +++ b/HeishaMon/commands.cpp @@ -870,6 +870,75 @@ unsigned int set_external_error(char *msg, unsigned char *cmd, char *log_msg){ return sizeof(panasonicSendQuery); } +unsigned int set_heatingcontrol(char *msg, unsigned char *cmd, char *log_msg) { + + const byte address=30; + byte value = 0b01; + + if ( String(msg).toInt() == 1 ) { + value = 0b10; + } + + { + char tmp[256] = { 0 }; + snprintf_P(tmp, 255, PSTR("set heating control %d"), value - 1); + memcpy(log_msg, tmp, sizeof(tmp)); + } + + { + memcpy_P(cmd, panasonicSendQuery, sizeof(panasonicSendQuery)); + cmd[address] = value << 2; + } + + return sizeof(panasonicSendQuery); +} + +unsigned int set_smart_dhw(char *msg, unsigned char *cmd, char *log_msg) { + + const byte address=24; + byte value = 0b01; + + if ( String(msg).toInt() == 1 ) { + value = 0b10; + } + + { + char tmp[256] = { 0 }; + snprintf_P(tmp, 255, PSTR("set smart dhw %d"), value - 1); + memcpy(log_msg, tmp, sizeof(tmp)); + } + + { + memcpy_P(cmd, panasonicSendQuery, sizeof(panasonicSendQuery)); + cmd[address] = value << 6; + } + + return sizeof(panasonicSendQuery); +} + +unsigned int set_quiet_mode_priority(char *msg, unsigned char *cmd, char *log_msg) { + + const byte address=11; + byte value = 0b01; + + if ( String(msg).toInt() == 1 ) { + value = 0b10; + } + + { + char tmp[256] = { 0 }; + snprintf_P(tmp, 255, PSTR("set quiet mode priority %d"), value - 1); + memcpy(log_msg, tmp, sizeof(tmp)); + } + + { + memcpy_P(cmd, panasonicSendQuery, sizeof(panasonicSendQuery)); + cmd[address] = value << 4; + } + + return sizeof(panasonicSendQuery); +} + unsigned int set_external_compressor_control(char *msg, unsigned char *cmd, char *log_msg){ const byte off_state=64; const byte address=23; diff --git a/HeishaMon/commands.h b/HeishaMon/commands.h index ba748c89..0351cb93 100644 --- a/HeishaMon/commands.h +++ b/HeishaMon/commands.h @@ -66,6 +66,9 @@ unsigned int set_bivalent_ap_start_temp(char *msg, unsigned char *cmd, char *log unsigned int set_bivalent_ap_stop_temp(char *msg, unsigned char *cmd, char *log_msg); unsigned int set_external_control(char *msg, unsigned char *cmd, char *log_msg); unsigned int set_external_error(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_heatingcontrol(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_smart_dhw(char *msg, unsigned char *cmd, char *log_msg); +unsigned int set_quiet_mode_priority(char *msg, unsigned char *cmd, char *log_msg); //optional pcb commands unsigned int set_heat_cool_mode(char *msg, char *log_msg); @@ -153,6 +156,9 @@ const cmdStruct commands[] PROGMEM = { { "SetBivalentAPStartTemp", set_bivalent_ap_start_temp }, // bivalent AP stop temp - set from -15C to 35C { "SetBivalentAPStopTemp", set_bivalent_ap_stop_temp }, + { "SetHeatingControl", set_heatingcontrol }, + { "SetSmartDHW", set_smart_dhw }, + { "SetQuietModePriority", set_quiet_mode_priority }, }; struct optCmdStruct{ diff --git a/HeishaMon/decode.h b/HeishaMon/decode.h index 3a02ee5f..d2872f7b 100644 --- a/HeishaMon/decode.h +++ b/HeishaMon/decode.h @@ -42,7 +42,7 @@ static const char _unknown[] PROGMEM = "unknown"; -#define NUMBER_OF_TOPICS 139 //last topic number + 1 +#define NUMBER_OF_TOPICS 142 //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 @@ -215,6 +215,9 @@ static const char topics[][MAX_TOPIC_LEN] PROGMEM = { "Bivalent_Advanced_Start_Delay",//TOP136 "Bivalent_Advanced_Stop_Delay",//TOP137 "Bivalent_Advanced_DHW_Delay",//TOP138 + "Heating_Control", //TOP139 + "Smart_DHW", //TOP140 + "Quiet_Mode_Priority", //TOP141 }; 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 @@ -357,6 +360,9 @@ static const byte topicBytes[] PROGMEM = { //can store the index as byte (8-bit 67, //TOP136 69, //TOP137 70, //TOP138 + 30, //TOP139 + 24, //TOP140 + 11, //TOP141 }; @@ -511,6 +517,9 @@ static const topicFP topicFunctions[] PROGMEM = { getIntMinus1, //TOP136 getIntMinus1, //TOP137 getIntMinus1, //TOP138 + getBit5and6, //TOP139 + getBit1and2, //TOP140 + getBit3and4, //TOP141 }; static const char *DisabledEnabled[] PROGMEM = {"2", "Disabled", "Enabled"}; @@ -548,6 +557,9 @@ static const char *ExtPadHeaterType[] PROGMEM = {"3", "Disabled", "Type-A","Type static const char *Bivalent[] PROGMEM = {"3", "Alternative", "Parallel", "Advanced Parallel"}; static const char *Percent[] PROGMEM = {"0", "%"}; static const char *Model[] PROGMEM = {"0", "Model"}; +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 **opttopicDescription[] PROGMEM = { OffOn, //OPT0 @@ -708,4 +720,7 @@ static const char **topicDescription[] PROGMEM = { Minutes, //TOP136 Minutes, //TOP137 Minutes, //TOP138 + HeatingControl, //TOP139 + SmartDHW, //TOP140 + QuietModePriority, //TOP141 }; diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index 989a1205..c07f6f81 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -345,6 +345,37 @@ static const char settingsJS[] PROGMEM = " }" ""; +#ifdef TLS_SUPPORT +static const char caUploadJS[] PROGMEM = + ""; +#endif + /*static const char heatingCurveJS[] PROGMEM = "" "