From d3450b699e8e137c650fe50eea9ade3c0dbd8139 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Tue, 3 Feb 2026 12:38:46 +1100 Subject: [PATCH 01/44] Inital implementation of ROProperty & RWProperty --- lib/SpaInterface/SpaInterface.cpp | 46 +++++++++---- lib/SpaInterface/SpaInterface.h | 102 +++++++++++++++++++++++++---- lib/SpaInterface/SpaProperties.cpp | 12 ---- lib/SpaInterface/SpaProperties.h | 20 ------ lib/SpaUtils/SpaUtils.cpp | 4 +- src/main.cpp | 15 ++++- 6 files changed, 136 insertions(+), 63 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index b99be9d..2b36606 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -179,16 +179,11 @@ bool SpaInterface::setHELE(int mode){ bool SpaInterface::setSTMP(int temp){ debugD("setSTMP - %i", temp); - if (temp==getSTMP()) { - debugD("No STMP change detected - current %i, new %i", getSTMP(), temp); + if (temp==STMP.get()) { // todo: does this ever evaluate to true? + debugD("No STMP change detected - current %i, new %i", STMP.get(), temp); return true; // No change needed } - // check the temperate is within the valid range - if (temp < 50 || temp > 410) { - debugE("STMP out of range - %i", temp); - return false; - } if (temp % 2 != 0) { temp++; } @@ -196,21 +191,44 @@ bool SpaInterface::setSTMP(int temp){ String stemp = String(temp); if (sendCommandCheckResult("W40:" + stemp, stemp)) { - update_STMP(stemp); + STMP.update(temp); return true; } return false; } +bool SpaInterface::validateSTMP(int value) { + return value >= 50 && value <= 410; +} + +bool SpaInterface::validateL_1SNZ_DAY(int value) { + switch (value) { + case 128: // Off + case 127: // Everyday + case 96: // Weekends + case 31: // Weekdays + case 16: // Monday + case 8: // Tuesday + case 4: // Wednesday + case 2: // Thursday + case 1: // Friday + case 64: // Saturday + case 32: // Sunday + return true; + default: + return false; + } +} + bool SpaInterface::setL_1SNZ_DAY(int mode){ debugD("setL_1SNZ_DAY - %i",mode); - if (mode == getL_1SNZ_DAY()) { - debugD("No L_1SNZ_DAY change detected - current %i, new %i", getL_1SNZ_DAY(), mode); + if (mode == L_1SNZ_DAY.get()) { + debugD("No L_1SNZ_DAY change detected - current %i, new %i", L_1SNZ_DAY.get(), mode); return true; } if (sendCommandCheckResult(String("W67:")+mode,String(mode))) { - update_L_1SNZ_DAY(String(mode)); + L_1SNZ_DAY.update(mode); return true; } return false; @@ -753,7 +771,7 @@ void SpaInterface::clearUpdateCallback() { void SpaInterface::updateMeasures() { #pragma region R2 - update_MainsCurrent(statusResponseRaw[R2+1]); + MainsCurrent.update(statusResponseRaw[R2+1].toInt()); update_MainsVoltage(statusResponseRaw[R2+2]); update_CaseTemperature(statusResponseRaw[R2+3]); update_PortCurrent(statusResponseRaw[R2+4]); @@ -857,12 +875,12 @@ void SpaInterface::updateMeasures() { update_LSPDValue(statusResponseRaw[R6 + 5]); update_FiltHrs(statusResponseRaw[R6 + 6]); update_FiltBlockHrs(statusResponseRaw[R6 + 7]); - update_STMP(statusResponseRaw[R6 + 8]); + STMP.update(statusResponseRaw[R6 + 8].toInt()); update_L_24HOURS(statusResponseRaw[R6 + 9]); update_PSAV_LVL(statusResponseRaw[R6 + 10]); update_PSAV_BGN(statusResponseRaw[R6 + 11]); update_PSAV_END(statusResponseRaw[R6 + 12]); - update_L_1SNZ_DAY(statusResponseRaw[R6 + 13]); + L_1SNZ_DAY.update(statusResponseRaw[R6 + 13].toInt()); update_L_2SNZ_DAY(statusResponseRaw[R6 + 14]); update_L_1SNZ_BGN(statusResponseRaw[R6 + 15]); update_L_2SNZ_BGN(statusResponseRaw[R6 + 16]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 9d035d4..2bd13c3 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -13,7 +13,6 @@ extern RemoteDebug Debug; class SpaInterface : public SpaProperties { private: - /// @brief How often to pole the spa for updates in seconds. int _updateFrequency = 60; @@ -66,7 +65,8 @@ class SpaInterface : public SpaProperties { void updateMeasures(); - + static bool validateSTMP(int value); + static bool validateL_1SNZ_DAY(int value); /// @brief Sends command to SpaNet controller. Result must be read by some other method. /// Used for the 'RF' command so that we can do a optomised read of the return array. @@ -106,7 +106,15 @@ class SpaInterface : public SpaProperties { u_long _lastWaitMessage = millis(); + /// @brief Set the desired water temperature + /// @param temp Between 5 and 40 in 0.5 increments + /// @return Returns True if succesful + bool setSTMP(int temp); + /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) + /// @param mode + /// @return Returns True if succesful + bool setL_1SNZ_DAY(int mode); public: /// @brief Init SpaInterface. @@ -114,6 +122,75 @@ class SpaInterface : public SpaProperties { ~SpaInterface(); + // Read-only value holder synced from the spa; external code can only read. + template + class ROProperty { + public: + ROProperty() = default; + + T get() const { return _value; } + operator T() const { return _value; } + + protected: + T _value{}; + bool _hasValue = false; + + // Called by SpaInterface when a fresh value is received from the spa. + void update(T newValue) { + _value = newValue; + _hasValue = true; + } + + friend class SpaInterface; + }; + + // Read/write property that sends commands before updating the cached value. + template + class RWProperty : public ROProperty { + public: + using WriteFunction = bool (SpaInterface::*)(T); + using ValidateFunction = bool (*)(T); + + RWProperty() = default; + RWProperty(SpaInterface* owner, WriteFunction writer) + : _owner(owner), _writer(writer) {} + RWProperty(SpaInterface* owner, WriteFunction writer, ValidateFunction validator) + : _owner(owner), _writer(writer), _validator(validator) {} + + // Sends the value to the spa first; caches it only on success. + bool set(T newValue) { + if (this->_hasValue && newValue == this->_value) { + return true; + } + + if (!_owner || !_writer) { + throw std::invalid_argument("RWProperty has no owner/writer"); + } + + if (_validator && !_validator(newValue)) { + throw std::out_of_range("RWProperty value out of range"); + } + + if (!(_owner->*_writer)(newValue)) { + throw std::runtime_error("RWProperty write failed"); + } + + this->update(newValue); + return true; + } + + RWProperty& operator=(T newValue) { + set(newValue); + return *this; + } + + private: + // Owner + writer are required to commit changes to the spa. + SpaInterface* _owner = nullptr; + WriteFunction _writer = nullptr; + ValidateFunction _validator = nullptr; + }; + /// @brief configure how often the spa is polled in seconds. /// @param SpaPollFrequency void setSpaPollFrequency(int updateFrequency); @@ -121,6 +198,13 @@ class SpaInterface : public SpaProperties { /// @brief Complete RF command response in a single string Property statusResponse; + /// @brief Mains current draw multiplied by 10 (77 = 7.7 actual) + ROProperty MainsCurrent; + /// @brief Water temperature set point ('C) multiplied by 10 + RWProperty STMP{this, &SpaInterface::setSTMP, &SpaInterface::validateSTMP}; + /// @brief Sleep timer 1 day bitmap (128 = off, 127 = every day, 96 = weekends, 31 = weekdays) + RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validateL_1SNZ_DAY}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -128,6 +212,10 @@ class SpaInterface : public SpaProperties { /// @return bool isInitialised(); + /// @brief Water temperature set point multiplied by 10 (380 = 38.0 actual) + /// @return + int getSTMP() { return STMP.get(); } + /// @brief Set the function to be called when properties have been updated. /// @param f void setUpdateCallback(void (*f)()); @@ -135,16 +223,6 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Set the desired water temperature - /// @param temp Between 5 and 40 in 0.5 increments - /// @return Returns True if succesful - bool setSTMP(int temp); - - /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) - /// @param mode - /// @return Returns True if succesful - bool setL_1SNZ_DAY(int mode); - /// @brief Set snooze time (provide an integer that uses this calculation HH:mm > HH*265+mm. e.g. 13:47 = 13*256+47 = 3375) /// @param mode /// @return Returns True if succesful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index c86b287..a72d2ef 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -40,10 +40,6 @@ static boolean updateTriStateProperty(Property& prop, const String& s) { return true; } -boolean SpaProperties::update_MainsCurrent(const String& s){ - return updateIntProperty(MainsCurrent, s); -} - boolean SpaProperties::update_SpaDayOfWeek(const String& s){ return updateIntProperty(SpaDayOfWeek, s); } @@ -419,10 +415,6 @@ boolean SpaProperties::update_FiltBlockHrs(const String& s){ return updateIntProperty(FiltBlockHrs, s); } -boolean SpaProperties::update_STMP(const String& s){ - return updateIntProperty(STMP, s); -} - boolean SpaProperties::update_L_24HOURS(const String& s){ return updateIntProperty(L_24HOURS, s); } @@ -439,10 +431,6 @@ boolean SpaProperties::update_PSAV_END(const String& s){ return updateIntProperty(PSAV_END, s); } -boolean SpaProperties::update_L_1SNZ_DAY(const String& s){ - return updateIntProperty(L_1SNZ_DAY, s); -} - boolean SpaProperties::update_L_2SNZ_DAY(const String& s){ return updateIntProperty(L_2SNZ_DAY, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 9f381b9..6ebf44d 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -39,8 +39,6 @@ class SpaProperties private: #pragma region R2 - /// @brief Mains current draw (A) - Property MainsCurrent; /// @brief Mains voltage (V) Property MainsVoltage; /// @brief Internal case temperature ('C) @@ -283,7 +281,6 @@ Property HV_2; /// @brief Filter block duration (hours) Property FiltBlockHrs; /// @brief Water temperature set point ('C) - Property STMP; // 1 = 12 hrs Property L_24HOURS; /// @brief Power save level @@ -298,10 +295,6 @@ Property HV_2; /// /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) Property PSAV_END; - /// @brief Sleep timer 1 - /// - /// 128 = off, 127 = every day, 96 = weekends, 31 = weekdays - Property L_1SNZ_DAY; /// @brief Sleep timer 2 /// /// 128 = off, 127 = every day, 96 = weekends, 31 = weekdays @@ -621,7 +614,6 @@ Property HV_2; protected: #pragma region R2 - boolean update_MainsCurrent(const String&); boolean update_SpaDayOfWeek(const String&); boolean update_SpaTime(const String& year, const String& month, const String& day, const String& hour, const String& minute, const String& second); boolean update_MainsVoltage(const String&); @@ -726,12 +718,10 @@ Property HV_2; boolean update_LSPDValue(const String&); boolean update_FiltHrs(const String&); boolean update_FiltBlockHrs(const String&); - boolean update_STMP(const String&); boolean update_L_24HOURS(const String&); boolean update_PSAV_LVL(const String&); boolean update_PSAV_BGN(const String&); boolean update_PSAV_END(const String&); - boolean update_L_1SNZ_DAY(const String&); boolean update_L_2SNZ_DAY(const String&); boolean update_L_1SNZ_BGN(const String&); boolean update_L_2SNZ_BGN(const String&); @@ -878,11 +868,6 @@ Property HV_2; public: - /// @brief Gets the mains current multiplied by 10 (77 = 7.7 actual) - /// @return - int getMainsCurrent() { return MainsCurrent.getValue(); } - void setMainsCurrentCallback(void (*callback)(int)) { MainsCurrent.setCallback(callback); } - int getMainsVoltage() { return MainsVoltage.getValue(); } void setMainsVoltageCallback(void (*callback)(int)) { MainsVoltage.setCallback(callback); } @@ -1208,9 +1193,6 @@ Property HV_2; // According to the docs only certain values are valid here. Using a select to ensure only valid values are used. const std::array FiltBlockHrsSelect = {"24","12","8","6","4","3","2","1"}; - int getSTMP() { return STMP.getValue(); } - void setSTMPCallback(void (*callback)(int)) { STMP.setCallback(callback); } - int getL_24HOURS() { return L_24HOURS.getValue(); } void setL_24HOURSCallback(void (*callback)(int)) { L_24HOURS.setCallback(callback); } @@ -1223,8 +1205,6 @@ Property HV_2; int getPSAV_END() { return PSAV_END.getValue(); } void setPSAV_ENDCallback(void (*callback)(int)) { PSAV_END.setCallback(callback); } - int getL_1SNZ_DAY() { return L_1SNZ_DAY.getValue(); } - void setL_1SNZ_DAYCallback(void (*callback)(int)) { L_1SNZ_DAY.setCallback(callback); } const std::array sleepSelection = {"Off", "Everyday", "Weekends", "Weekdays", "Monday", "Tuesday", "Wednesday", "Thuesday", "Friday", "Saturday", "Sunday"}; const std::array sleepBitmap = {128, 127, 96, 31, 16, 8, 4, 2, 1, 64, 32}; diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index dc5818c..370f0b5 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -129,7 +129,7 @@ int getPumpSpeedMin(String pumpInstallState) { bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String &output, bool prettyJson) { JsonDocument json; - json["temperatures"]["setPoint"] = si.getSTMP() / 10.0; + json["temperatures"]["setPoint"] = si.STMP.get() / 10.0; json["temperatures"]["water"] = si.getWTMP() / 10.0; json["temperatures"]["heater"] = si.getHeaterTemperature() / 10.0; json["temperatures"]["case"] = si.getCaseTemperature(); @@ -137,7 +137,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["temperatures"]["heatpumpCondensor"] = si.getHP_Condensor(); json["power"]["voltage"] = si.getMainsVoltage(); - json["power"]["current"]= si.getMainsCurrent() / 10.0; // convert value to A + json["power"]["current"]= si.MainsCurrent.get() / 10.0; // convert value to A json["power"]["power"] = si.getPower() / 10.0; // convert value to W json["power"]["totalenergy"]= si.getPower_kWh() / 100.0; // convert value to kWh. diff --git a/src/main.cpp b/src/main.cpp index 4bc253e..3f6ec11 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -469,7 +470,11 @@ void setSpaProperty(String property, String p) { debugI("Received update for %s to %s",property.c_str(),p.c_str()); if (property == "temperatures_setPoint") { - si.setSTMP(int(p.toFloat()*10)); + try { + si.STMP = int(p.toFloat()*10); + } catch (const std::exception& ex) { + debugE("Failed to set STMP: %s", ex.what()); + } } else if (property == "heatpump_mode") { si.setHPMP(p); // note single speed pumps should never trigger a mode or speed events @@ -534,7 +539,11 @@ void setSpaProperty(String property, String p) { for (const auto& i : si.sleepSelection) { if (i == p) { if (property == "sleepTimers_1_state") - si.setL_1SNZ_DAY(si.sleepBitmap[member]); + try { + si.L_1SNZ_DAY = si.sleepBitmap[member]; + } catch (const std::exception& ex) { + debugE("Failed to set L_1SNZ_DAY: %s", ex.what()); + } else if (property == "sleepTimers_2_state") si.setL_2SNZ_DAY(si.sleepBitmap[member]); break; @@ -830,4 +839,4 @@ void loop() { } mqttClient.loop(); -} \ No newline at end of file +} From 59d79a078312cf5b9d4d62ec62a9607bd146deca Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 7 Feb 2026 07:44:51 +1100 Subject: [PATCH 02/44] Extend Propery classes to be able to set and return via human readable strings --- lib/SpaInterface/SpaInterface.cpp | 18 ++------ lib/SpaInterface/SpaInterface.h | 70 +++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 2b36606..dd69957 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -202,22 +202,12 @@ bool SpaInterface::validateSTMP(int value) { } bool SpaInterface::validateL_1SNZ_DAY(int value) { - switch (value) { - case 128: // Off - case 127: // Everyday - case 96: // Weekends - case 31: // Weekdays - case 16: // Monday - case 8: // Tuesday - case 4: // Wednesday - case 2: // Thursday - case 1: // Friday - case 64: // Saturday - case 32: // Sunday + for (size_t i = 0; i < array_count(L_1SNZ_DAY_Map); i++) { + if (L_1SNZ_DAY_Map[i].value == value) { return true; - default: - return false; + } } + return false; } bool SpaInterface::setL_1SNZ_DAY(int mode){ diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 2bd13c3..a6095b0 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -10,6 +10,8 @@ extern RemoteDebug Debug; #define FAILEDREADFREQUENCY 1000 //(ms) Frequency to retry on a failed read of the status registers. #define V2FIRMWARE_STRING "SW V2" // String to identify V2 firmware +template +constexpr size_t array_count(const T (&)[N]) { return N; } class SpaInterface : public SpaProperties { private: @@ -126,14 +128,37 @@ class SpaInterface : public SpaProperties { template class ROProperty { public: + // Optional label/value mapping for human-readable access. + struct LabelValue { + const char* label; + T value; + }; + ROProperty() = default; + // Provide a label/value map to enable getLabel(). + ROProperty(const LabelValue* map, size_t mapSize) + : _map(map), _mapSize(mapSize) {} T get() const { return _value; } operator T() const { return _value; } + // Returns the matching label for the current value, or fallback if not found. + const char* getLabel(const char* fallback = "Unknown") const { + if (!_map || _mapSize == 0) { + return fallback; + } + for (size_t i = 0; i < _mapSize; i++) { + if (_map[i].value == _value) { + return _map[i].label; + } + } + return fallback; + } protected: T _value{}; bool _hasValue = false; + const LabelValue* _map = nullptr; + size_t _mapSize = 0; // Called by SpaInterface when a fresh value is received from the spa. void update(T newValue) { @@ -150,17 +175,27 @@ class SpaInterface : public SpaProperties { public: using WriteFunction = bool (SpaInterface::*)(T); using ValidateFunction = bool (*)(T); + using LabelValue = typename ROProperty::LabelValue; RWProperty() = default; + // Basic RW property with owner/writer only. RWProperty(SpaInterface* owner, WriteFunction writer) : _owner(owner), _writer(writer) {} + // RW property with optional validator. RWProperty(SpaInterface* owner, WriteFunction writer, ValidateFunction validator) : _owner(owner), _writer(writer), _validator(validator) {} + // RW property with label/value map for setLabel/getLabel. + RWProperty(SpaInterface* owner, WriteFunction writer, ValidateFunction validator, + const LabelValue* map, size_t mapSize) + : ROProperty(map, mapSize), + _owner(owner), + _writer(writer), + _validator(validator) {} // Sends the value to the spa first; caches it only on success. - bool set(T newValue) { + void set(T newValue) { if (this->_hasValue && newValue == this->_value) { - return true; + return; } if (!_owner || !_writer) { @@ -176,7 +211,6 @@ class SpaInterface : public SpaProperties { } this->update(newValue); - return true; } RWProperty& operator=(T newValue) { @@ -184,6 +218,20 @@ class SpaInterface : public SpaProperties { return *this; } + // Set by label, throws if map is not configured or label is unknown. + void setLabel(const char* label) { + if (!label || !this->_map || this->_mapSize == 0) { + throw std::invalid_argument("RWProperty label map not configured"); + } + for (size_t i = 0; i < this->_mapSize; i++) { + if (strcmp(this->_map[i].label, label) == 0) { + set(this->_map[i].value); + return; + } + } + throw std::out_of_range("RWProperty label not found"); + } + private: // Owner + writer are required to commit changes to the spa. SpaInterface* _owner = nullptr; @@ -203,7 +251,21 @@ class SpaInterface : public SpaProperties { /// @brief Water temperature set point ('C) multiplied by 10 RWProperty STMP{this, &SpaInterface::setSTMP, &SpaInterface::validateSTMP}; /// @brief Sleep timer 1 day bitmap (128 = off, 127 = every day, 96 = weekends, 31 = weekdays) - RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validateL_1SNZ_DAY}; + static constexpr ROProperty::LabelValue L_1SNZ_DAY_Map[] = { + {"Off", 128}, + {"Everyday", 127}, + {"Weekends", 96}, + {"Weekdays", 31}, + {"Monday", 16}, + {"Tuesday", 8}, + {"Wednesday", 4}, + {"Thuesday", 2}, + {"Friday", 1}, + {"Saturday", 64}, + {"Sunday", 32}, + }; + RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validateL_1SNZ_DAY, + L_1SNZ_DAY_Map, array_count(L_1SNZ_DAY_Map)}; /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); From 3ca7e45863dfc1424c3291bac6e5101e665d73cd Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 7 Feb 2026 07:51:28 +1100 Subject: [PATCH 03/44] Fix missed call in SpaUtils JSON response constructor --- lib/SpaUtils/SpaUtils.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 370f0b5..c91b51d 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -191,15 +191,13 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["blower"]["mode"] = si.getOutlet_Blower()==1? "Ramp" : "Variable"; json["blower"]["speed"] = si.getOutlet_Blower() ==2? "0" : String(si.getVARIValue()); + json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); + int member = 0; for (const auto& pair : si.sleepBitmap) { - if (pair == si.getL_1SNZ_DAY()) { - json["sleepTimers"]["timer1"]["state"]=si.sleepSelection[member]; - debugV("SleepTimer1: %s", si.sleepSelection[member].c_str()); - } if (pair == si.getL_2SNZ_DAY()) { json["sleepTimers"]["timer2"]["state"]=si.sleepSelection[member]; - debugV("SleepTimer2: %s", si.sleepSelection[member].c_str()); + break; } member++; } From 2e1220df6f5f8318e2892583b6558bfb63be26bd Mon Sep 17 00:00:00 2001 From: wayne-love Date: Wed, 18 Feb 2026 15:51:33 +1100 Subject: [PATCH 04/44] Refactor sleep timer properties and update related functions for consistency --- lib/HAAutoDiscovery/HAAutoDiscovery.h | 16 +++++++++++ lib/SpaInterface/SpaInterface.cpp | 14 +++++----- lib/SpaInterface/SpaInterface.h | 40 +++++++++++++++++---------- lib/SpaInterface/SpaProperties.cpp | 4 --- lib/SpaInterface/SpaProperties.h | 12 +------- lib/SpaUtils/SpaUtils.cpp | 12 ++------ src/main.cpp | 23 ++++++++------- 7 files changed, 65 insertions(+), 56 deletions(-) diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.h b/lib/HAAutoDiscovery/HAAutoDiscovery.h index ed66339..3518b62 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.h +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.h @@ -3,6 +3,7 @@ #include #include +#include "SpaInterface.h" /// @brief Configuration structure for the data elements for the Spa. @@ -52,6 +53,21 @@ void generateSelectAdJSON(String& output, const AutoDiscoveryInformationTemplate serializeJson(json, output); } +template +void generateSelectAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, const SpaInterface::ROProperty& prop) { + JsonDocument json; + generateCommonAdJSON(json, config, spa, discoveryTopic, "select"); + + json["command_topic"] = spa.commandTopic + "/" + config.propertyId; + JsonArray opts = json["options"].to(); + const size_t count = prop.getLabelCount(); + for (size_t i = 0; i < count; i++) { + opts.add(prop.getLabelAt(i)); + } + + serializeJson(json, output); +} + void generateFanAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, int min, int max, const String* modes, const size_t modesSize=0); template diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index dd69957..f12402b 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -201,9 +201,9 @@ bool SpaInterface::validateSTMP(int value) { return value >= 50 && value <= 410; } -bool SpaInterface::validateL_1SNZ_DAY(int value) { - for (size_t i = 0; i < array_count(L_1SNZ_DAY_Map); i++) { - if (L_1SNZ_DAY_Map[i].value == value) { +bool SpaInterface::validate_SNZ_DAY(int value) { + for (size_t i = 0; i < array_count(SNZ_DAY_Map); i++) { + if (SNZ_DAY_Map[i].value == value) { return true; } } @@ -254,13 +254,13 @@ bool SpaInterface::setL_1SNZ_END(int mode){ bool SpaInterface::setL_2SNZ_DAY(int mode){ debugD("setL_2SNZ_DAY - %i",mode); - if (mode == getL_2SNZ_DAY()) { - debugD("No L_2SNZ_DAY change detected - current %i, new %i", getL_2SNZ_DAY(), mode); + if (mode == L_2SNZ_DAY.get()) { + debugD("No L_2SNZ_DAY change detected - current %i, new %i", L_2SNZ_DAY.get(), mode); return true; } if (sendCommandCheckResult(String("W70:")+mode,String(mode))) { - update_L_2SNZ_DAY(String(mode)); + L_2SNZ_DAY.update(mode); return true; } return false; @@ -871,7 +871,7 @@ void SpaInterface::updateMeasures() { update_PSAV_BGN(statusResponseRaw[R6 + 11]); update_PSAV_END(statusResponseRaw[R6 + 12]); L_1SNZ_DAY.update(statusResponseRaw[R6 + 13].toInt()); - update_L_2SNZ_DAY(statusResponseRaw[R6 + 14]); + L_2SNZ_DAY.update(statusResponseRaw[R6 + 14].toInt()); update_L_1SNZ_BGN(statusResponseRaw[R6 + 15]); update_L_2SNZ_BGN(statusResponseRaw[R6 + 16]); update_L_1SNZ_END(statusResponseRaw[R6 + 17]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index a6095b0..6d02670 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -68,7 +68,7 @@ class SpaInterface : public SpaProperties { void updateMeasures(); static bool validateSTMP(int value); - static bool validateL_1SNZ_DAY(int value); + static bool validate_SNZ_DAY(int value); /// @brief Sends command to SpaNet controller. Result must be read by some other method. /// Used for the 'RF' command so that we can do a optomised read of the return array. @@ -117,6 +117,7 @@ class SpaInterface : public SpaProperties { /// @param mode /// @return Returns True if succesful bool setL_1SNZ_DAY(int mode); + bool setL_2SNZ_DAY(int mode); public: /// @brief Init SpaInterface. @@ -135,9 +136,10 @@ class SpaInterface : public SpaProperties { }; ROProperty() = default; - // Provide a label/value map to enable getLabel(). - ROProperty(const LabelValue* map, size_t mapSize) - : _map(map), _mapSize(mapSize) {} + // Provide a label/value map; array size is deduced. + template + ROProperty(const LabelValue (&map)[N]) + : _map(map), _mapSize(N) {} T get() const { return _value; } operator T() const { return _value; } @@ -153,6 +155,18 @@ class SpaInterface : public SpaProperties { } return fallback; } + // Expose label/value map for building UI dropdowns. + const LabelValue* getLabelMap(size_t& count) const { + count = _mapSize; + return _map; + } + size_t getLabelCount() const { return _mapSize; } + const char* getLabelAt(size_t index, const char* fallback = "Unknown") const { + if (!_map || index >= _mapSize) { + return fallback; + } + return _map[index].label; + } protected: T _value{}; @@ -185,9 +199,10 @@ class SpaInterface : public SpaProperties { RWProperty(SpaInterface* owner, WriteFunction writer, ValidateFunction validator) : _owner(owner), _writer(writer), _validator(validator) {} // RW property with label/value map for setLabel/getLabel. + template RWProperty(SpaInterface* owner, WriteFunction writer, ValidateFunction validator, - const LabelValue* map, size_t mapSize) - : ROProperty(map, mapSize), + const LabelValue (&map)[N]) + : ROProperty(map), _owner(owner), _writer(writer), _validator(validator) {} @@ -251,7 +266,7 @@ class SpaInterface : public SpaProperties { /// @brief Water temperature set point ('C) multiplied by 10 RWProperty STMP{this, &SpaInterface::setSTMP, &SpaInterface::validateSTMP}; /// @brief Sleep timer 1 day bitmap (128 = off, 127 = every day, 96 = weekends, 31 = weekdays) - static constexpr ROProperty::LabelValue L_1SNZ_DAY_Map[] = { + static constexpr ROProperty::LabelValue SNZ_DAY_Map[] = { {"Off", 128}, {"Everyday", 127}, {"Weekends", 96}, @@ -264,8 +279,10 @@ class SpaInterface : public SpaProperties { {"Saturday", 64}, {"Sunday", 32}, }; - RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validateL_1SNZ_DAY, - L_1SNZ_DAY_Map, array_count(L_1SNZ_DAY_Map)}; + RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validate_SNZ_DAY, + SNZ_DAY_Map}; + RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, &SpaInterface::validate_SNZ_DAY, + SNZ_DAY_Map}; /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -291,11 +308,6 @@ class SpaInterface : public SpaProperties { bool setL_1SNZ_BGN(int mode); bool setL_1SNZ_END(int mode); - /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) - /// @param mode - /// @return Returns True if succesful - bool setL_2SNZ_DAY(int mode); - /// @brief Set snooze time (provide an integer that uses this calculation HH:mm > HH*265+mm. e.g. 13:47 = 13*256+47 = 3375) /// @param mode /// @return Returns True if succesful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index a72d2ef..bcc18b8 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -431,10 +431,6 @@ boolean SpaProperties::update_PSAV_END(const String& s){ return updateIntProperty(PSAV_END, s); } -boolean SpaProperties::update_L_2SNZ_DAY(const String& s){ - return updateIntProperty(L_2SNZ_DAY, s); -} - boolean SpaProperties::update_L_1SNZ_BGN(const String& s){ return updateIntProperty(L_1SNZ_BGN, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 6ebf44d..d77863d 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -295,10 +295,7 @@ Property HV_2; /// /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) Property PSAV_END; - /// @brief Sleep timer 2 - /// - /// 128 = off, 127 = every day, 96 = weekends, 31 = weekdays - Property L_2SNZ_DAY; + /// @brief Sleep time 1 start time /// /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) @@ -722,7 +719,6 @@ Property HV_2; boolean update_PSAV_LVL(const String&); boolean update_PSAV_BGN(const String&); boolean update_PSAV_END(const String&); - boolean update_L_2SNZ_DAY(const String&); boolean update_L_1SNZ_BGN(const String&); boolean update_L_2SNZ_BGN(const String&); boolean update_L_1SNZ_END(const String&); @@ -1205,12 +1201,6 @@ Property HV_2; int getPSAV_END() { return PSAV_END.getValue(); } void setPSAV_ENDCallback(void (*callback)(int)) { PSAV_END.setCallback(callback); } - const std::array sleepSelection = {"Off", "Everyday", "Weekends", "Weekdays", "Monday", "Tuesday", "Wednesday", "Thuesday", "Friday", "Saturday", "Sunday"}; - const std::array sleepBitmap = {128, 127, 96, 31, 16, 8, 4, 2, 1, 64, 32}; - - int getL_2SNZ_DAY() { return L_2SNZ_DAY.getValue(); } - void setL_2SNZ_DAYCallback(void (*callback)(int)) { L_2SNZ_DAY.setCallback(callback); } - int getL_1SNZ_BGN() { return L_1SNZ_BGN.getValue(); } void setL_1SNZ_BGNCallback(void (*callback)(int)) { L_1SNZ_BGN.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index c91b51d..fb88675 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -191,16 +191,8 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["blower"]["mode"] = si.getOutlet_Blower()==1? "Ramp" : "Variable"; json["blower"]["speed"] = si.getOutlet_Blower() ==2? "0" : String(si.getVARIValue()); - json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); - - int member = 0; - for (const auto& pair : si.sleepBitmap) { - if (pair == si.getL_2SNZ_DAY()) { - json["sleepTimers"]["timer2"]["state"]=si.sleepSelection[member]; - break; - } - member++; - } + json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); + json["sleepTimers"]["timer2"]["state"] = si.L_2SNZ_DAY.getLabel(); json["sleepTimers"]["timer1"]["begin"]=convertToTime(si.getL_1SNZ_BGN()); json["sleepTimers"]["timer1"]["end"]=convertToTime(si.getL_1SNZ_END()); json["sleepTimers"]["timer2"]["begin"]=convertToTime(si.getL_2SNZ_BGN()); diff --git a/src/main.cpp b/src/main.cpp index 3f6ec11..59c8045 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -342,7 +342,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "sleepTimers_1_state"; ADConf.deviceClass = ""; ADConf.entityCategory = "config"; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.sleepSelection); + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.L_1SNZ_DAY); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Sleep Timer 2"; @@ -350,7 +350,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "sleepTimers_2_state"; ADConf.deviceClass = ""; ADConf.entityCategory = "config"; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.sleepSelection); + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.L_2SNZ_DAY); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); /* @@ -535,20 +535,23 @@ void setSpaProperty(String property, String p) { } else if (property == "blower_mode") { si.setOutlet_Blower(p=="Variable"?0:1); } else if (property == "sleepTimers_1_state" || property == "sleepTimers_2_state") { - int member=0; - for (const auto& i : si.sleepSelection) { - if (i == p) { - if (property == "sleepTimers_1_state") + for (const auto& entry : SpaInterface::SNZ_DAY_Map) { + if (p == entry.label) { + if (property == "sleepTimers_1_state") { try { - si.L_1SNZ_DAY = si.sleepBitmap[member]; + si.L_1SNZ_DAY = entry.value; } catch (const std::exception& ex) { debugE("Failed to set L_1SNZ_DAY: %s", ex.what()); } - else if (property == "sleepTimers_2_state") - si.setL_2SNZ_DAY(si.sleepBitmap[member]); + } else if (property == "sleepTimers_2_state") { + try { + si.L_2SNZ_DAY = entry.value; + } catch (const std::exception& ex) { + debugE("Failed to set L_2SNZ_DAY: %s", ex.what()); + } + } break; } - member++; } } else if (property == "sleepTimers_1_begin") { si.setL_1SNZ_BGN(convertToInteger(p)); From 65a38a3f3af01022c31257daa09c066cac4c4ce9 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Thu, 19 Feb 2026 18:40:58 +1100 Subject: [PATCH 05/44] Migrate snooze start and end times to RWProperty --- lib/SpaInterface/SpaInterface.cpp | 39 +++++++++++++---------- lib/SpaInterface/SpaInterface.h | 50 +++++++++++++++++++++--------- lib/SpaInterface/SpaProperties.cpp | 16 ---------- lib/SpaInterface/SpaProperties.h | 20 +++++------- lib/SpaUtils/SpaUtils.cpp | 8 ++--- src/main.cpp | 24 +++++++++++--- 6 files changed, 90 insertions(+), 67 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index f12402b..2fea305 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -210,6 +210,17 @@ bool SpaInterface::validate_SNZ_DAY(int value) { return false; } +bool SpaInterface::validate_SNZ_TIME(int value) { + if (value < 0) { + return false; + } + + int hour = value / 256; + int minute = value % 256; + + return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59; +} + bool SpaInterface::setL_1SNZ_DAY(int mode){ debugD("setL_1SNZ_DAY - %i",mode); if (mode == L_1SNZ_DAY.get()) { @@ -226,13 +237,12 @@ bool SpaInterface::setL_1SNZ_DAY(int mode){ bool SpaInterface::setL_1SNZ_BGN(int mode){ debugD("setL_1SNZ_BGN - %i",mode); - if (mode == getL_1SNZ_BGN()) { - debugD("No L_1SNZ_BGN change detected - current %i, new %i", getL_1SNZ_BGN(), mode); + if (mode == L_1SNZ_BGN.get()) { + debugD("No L_1SNZ_BGN change detected - current %i, new %i", L_1SNZ_BGN.get(), mode); return true; } if (sendCommandCheckResult(String("W68:")+mode,String(mode))) { - update_L_1SNZ_BGN(String(mode)); return true; } return false; @@ -240,13 +250,12 @@ bool SpaInterface::setL_1SNZ_BGN(int mode){ bool SpaInterface::setL_1SNZ_END(int mode){ debugD("setL_1SNZ_END - %i",mode); - if (mode == getL_1SNZ_END()) { - debugD("No L_1SNZ_END change detected - current %i, new %i", getL_1SNZ_END(), mode); + if (mode == L_1SNZ_END.get()) { + debugD("No L_1SNZ_END change detected - current %i, new %i", L_1SNZ_END.get(), mode); return true; } if (sendCommandCheckResult(String("W69:")+mode,String(mode))) { - update_L_1SNZ_END(String(mode)); return true; } return false; @@ -268,13 +277,12 @@ bool SpaInterface::setL_2SNZ_DAY(int mode){ bool SpaInterface::setL_2SNZ_BGN(int mode){ debugD("setL_2SNZ_BGN - %i",mode); - if (mode == getL_2SNZ_BGN()) { - debugD("No L_2SNZ_BGN change detected - current %i, new %i", getL_2SNZ_BGN(), mode); + if (mode == L_2SNZ_BGN.get()) { + debugD("No L_2SNZ_BGN change detected - current %i, new %i", L_2SNZ_BGN.get(), mode); return true; } if (sendCommandCheckResult(String("W71:")+mode,String(mode))) { - update_L_2SNZ_BGN(String(mode)); return true; } return false; @@ -282,13 +290,12 @@ bool SpaInterface::setL_2SNZ_BGN(int mode){ bool SpaInterface::setL_2SNZ_END(int mode){ debugD("setL_2SNZ_END - %i",mode); - if (mode == getL_2SNZ_END()) { - debugD("No L_2SNZ_END change detected - current %i, new %i", getL_2SNZ_END(), mode); + if (mode == L_2SNZ_END.get()) { + debugD("No L_2SNZ_END change detected - current %i, new %i", L_2SNZ_END.get(), mode); return true; } if (sendCommandCheckResult(String("W72:")+mode,String(mode))) { - update_L_2SNZ_END(String(mode)); return true; } return false; @@ -872,10 +879,10 @@ void SpaInterface::updateMeasures() { update_PSAV_END(statusResponseRaw[R6 + 12]); L_1SNZ_DAY.update(statusResponseRaw[R6 + 13].toInt()); L_2SNZ_DAY.update(statusResponseRaw[R6 + 14].toInt()); - update_L_1SNZ_BGN(statusResponseRaw[R6 + 15]); - update_L_2SNZ_BGN(statusResponseRaw[R6 + 16]); - update_L_1SNZ_END(statusResponseRaw[R6 + 17]); - update_L_2SNZ_END(statusResponseRaw[R6 + 18]); + L_1SNZ_BGN.update(statusResponseRaw[R6 + 15].toInt()); + L_2SNZ_BGN.update(statusResponseRaw[R6 + 16].toInt()); + L_1SNZ_END.update(statusResponseRaw[R6 + 17].toInt()); + L_2SNZ_END.update(statusResponseRaw[R6 + 18].toInt()); update_DefaultScrn(statusResponseRaw[R6 + 19]); update_TOUT(statusResponseRaw[R6 + 20]); update_VPMP(statusResponseRaw[R6 + 21]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 6d02670..db89848 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -69,6 +69,7 @@ class SpaInterface : public SpaProperties { static bool validateSTMP(int value); static bool validate_SNZ_DAY(int value); + static bool validate_SNZ_TIME(int value); /// @brief Sends command to SpaNet controller. Result must be read by some other method. /// Used for the 'RF' command so that we can do a optomised read of the return array. @@ -117,8 +118,17 @@ class SpaInterface : public SpaProperties { /// @param mode /// @return Returns True if succesful bool setL_1SNZ_DAY(int mode); + + /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) + /// @param mode + /// @return Returns True if succesful bool setL_2SNZ_DAY(int mode); + bool setL_1SNZ_BGN(int mode); + bool setL_1SNZ_END(int mode); + bool setL_2SNZ_BGN(int mode); + bool setL_2SNZ_END(int mode); + public: /// @brief Init SpaInterface. SpaInterface(); @@ -261,11 +271,16 @@ class SpaInterface : public SpaProperties { /// @brief Complete RF command response in a single string Property statusResponse; - /// @brief Mains current draw multiplied by 10 (77 = 7.7 actual) + /// @brief Mains current draw x10. + /// @details Read-only; value 77 represents 7.7A. ROProperty MainsCurrent; - /// @brief Water temperature set point ('C) multiplied by 10 + + /// @brief Water temperature set point x10. + /// @details Read/write. Valid range 50..410 (5.0C..41.0C). Uses command W40. RWProperty STMP{this, &SpaInterface::setSTMP, &SpaInterface::validateSTMP}; - /// @brief Sleep timer 1 day bitmap (128 = off, 127 = every day, 96 = weekends, 31 = weekdays) + + /// @brief Sleep timer day mode values. + /// @details Shared label/value map for both timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. static constexpr ROProperty::LabelValue SNZ_DAY_Map[] = { {"Off", 128}, {"Everyday", 127}, @@ -279,10 +294,27 @@ class SpaInterface : public SpaProperties { {"Saturday", 64}, {"Sunday", 32}, }; + /// @brief Sleep timer 1 day mode bitmap. + /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W67. RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validate_SNZ_DAY, SNZ_DAY_Map}; + /// @brief Sleep timer 2 day mode bitmap. + /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W70. RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, &SpaInterface::validate_SNZ_DAY, SNZ_DAY_Map}; + + /// @brief Sleep timer 1 start time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W68. + RWProperty L_1SNZ_BGN{this, &SpaInterface::setL_1SNZ_BGN, &SpaInterface::validate_SNZ_TIME}; + /// @brief Sleep timer 1 finish time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W69. + RWProperty L_1SNZ_END{this, &SpaInterface::setL_1SNZ_END, &SpaInterface::validate_SNZ_TIME}; + /// @brief Sleep timer 2 start time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W71. + RWProperty L_2SNZ_BGN{this, &SpaInterface::setL_2SNZ_BGN, &SpaInterface::validate_SNZ_TIME}; + /// @brief Sleep timer 2 finish time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W72. + RWProperty L_2SNZ_END{this, &SpaInterface::setL_2SNZ_END, &SpaInterface::validate_SNZ_TIME}; /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -302,18 +334,6 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Set snooze time (provide an integer that uses this calculation HH:mm > HH*265+mm. e.g. 13:47 = 13*256+47 = 3375) - /// @param mode - /// @return Returns True if succesful - bool setL_1SNZ_BGN(int mode); - bool setL_1SNZ_END(int mode); - - /// @brief Set snooze time (provide an integer that uses this calculation HH:mm > HH*265+mm. e.g. 13:47 = 13*256+47 = 3375) - /// @param mode - /// @return Returns True if succesful - bool setL_2SNZ_BGN(int mode); - bool setL_2SNZ_END(int mode); - /// @brief Set Heat pump operating mode (0 --> 3, {auto, heat, cool, off}) /// @param mode /// @return Returns True if succesful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index bcc18b8..7b4fded 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -431,22 +431,6 @@ boolean SpaProperties::update_PSAV_END(const String& s){ return updateIntProperty(PSAV_END, s); } -boolean SpaProperties::update_L_1SNZ_BGN(const String& s){ - return updateIntProperty(L_1SNZ_BGN, s); -} - -boolean SpaProperties::update_L_2SNZ_BGN(const String& s){ - return updateIntProperty(L_2SNZ_BGN, s); -} - -boolean SpaProperties::update_L_1SNZ_END(const String& s){ - return updateIntProperty(L_1SNZ_END, s); -} - -boolean SpaProperties::update_L_2SNZ_END(const String& s){ - return updateIntProperty(L_2SNZ_END, s); -} - boolean SpaProperties::update_DefaultScrn(const String& s){ return updateIntProperty(DefaultScrn, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index d77863d..9edc3a4 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -719,10 +719,6 @@ Property HV_2; boolean update_PSAV_LVL(const String&); boolean update_PSAV_BGN(const String&); boolean update_PSAV_END(const String&); - boolean update_L_1SNZ_BGN(const String&); - boolean update_L_2SNZ_BGN(const String&); - boolean update_L_1SNZ_END(const String&); - boolean update_L_2SNZ_END(const String&); boolean update_DefaultScrn(const String&); boolean update_TOUT(const String&); boolean update_VPMP(const String&); @@ -1201,17 +1197,17 @@ Property HV_2; int getPSAV_END() { return PSAV_END.getValue(); } void setPSAV_ENDCallback(void (*callback)(int)) { PSAV_END.setCallback(callback); } - int getL_1SNZ_BGN() { return L_1SNZ_BGN.getValue(); } - void setL_1SNZ_BGNCallback(void (*callback)(int)) { L_1SNZ_BGN.setCallback(callback); } + //int getL_1SNZ_BGN() { return L_1SNZ_BGN.getValue(); } + //void setL_1SNZ_BGNCallback(void (*callback)(int)) { L_1SNZ_BGN.setCallback(callback); } - int getL_2SNZ_BGN() { return L_2SNZ_BGN.getValue(); } - void setL_2SNZ_BGNCallback(void (*callback)(int)) { L_2SNZ_BGN.setCallback(callback); } + //int getL_2SNZ_BGN() { return L_2SNZ_BGN.getValue(); } + //void setL_2SNZ_BGNCallback(void (*callback)(int)) { L_2SNZ_BGN.setCallback(callback); } - int getL_1SNZ_END() { return L_1SNZ_END.getValue(); } - void setL_1SNZ_ENDCallback(void (*callback)(int)) { L_1SNZ_END.setCallback(callback); } + //int getL_1SNZ_END() { return L_1SNZ_END.getValue(); } + //void setL_1SNZ_ENDCallback(void (*callback)(int)) { L_1SNZ_END.setCallback(callback); } - int getL_2SNZ_END() { return L_2SNZ_END.getValue(); } - void setL_2SNZ_ENDCallback(void (*callback)(int)) { L_2SNZ_END.setCallback(callback); } + //int getL_2SNZ_END() { return L_2SNZ_END.getValue(); } + //void setL_2SNZ_ENDCallback(void (*callback)(int)) { L_2SNZ_END.setCallback(callback); } int getDefaultScrn() { return DefaultScrn.getValue(); } void setDefaultScrnCallback(void (*callback)(int)) { DefaultScrn.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index fb88675..565e40b 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -193,10 +193,10 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); json["sleepTimers"]["timer2"]["state"] = si.L_2SNZ_DAY.getLabel(); - json["sleepTimers"]["timer1"]["begin"]=convertToTime(si.getL_1SNZ_BGN()); - json["sleepTimers"]["timer1"]["end"]=convertToTime(si.getL_1SNZ_END()); - json["sleepTimers"]["timer2"]["begin"]=convertToTime(si.getL_2SNZ_BGN()); - json["sleepTimers"]["timer2"]["end"]=convertToTime(si.getL_2SNZ_END()); + json["sleepTimers"]["timer1"]["begin"]=convertToTime(si.L_1SNZ_BGN.get()); + json["sleepTimers"]["timer1"]["end"]=convertToTime(si.L_1SNZ_END.get()); + json["sleepTimers"]["timer2"]["begin"]=convertToTime(si.L_2SNZ_BGN.get()); + json["sleepTimers"]["timer2"]["end"]=convertToTime(si.L_2SNZ_END.get()); json["lights"]["speed"] = si.getLSPDValue(); json["lights"]["state"] = si.getRB_TP_Light()? "ON": "OFF"; diff --git a/src/main.cpp b/src/main.cpp index 59c8045..e9f8f14 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -554,13 +554,29 @@ void setSpaProperty(String property, String p) { } } } else if (property == "sleepTimers_1_begin") { - si.setL_1SNZ_BGN(convertToInteger(p)); + try { + si.L_1SNZ_BGN = convertToInteger(p); + } catch (const std::exception& ex) { + debugE("Failed to set L_1SNZ_BGN: %s", ex.what()); + } } else if (property == "sleepTimers_1_end") { - si.setL_1SNZ_END(convertToInteger(p)); + try { + si.L_1SNZ_END = convertToInteger(p); + } catch (const std::exception& ex) { + debugE("Failed to set L_1SNZ_END: %s", ex.what()); + } } else if (property == "sleepTimers_2_begin") { - si.setL_2SNZ_BGN(convertToInteger(p)); + try { + si.L_2SNZ_BGN = convertToInteger(p); + } catch (const std::exception& ex) { + debugE("Failed to set L_2SNZ_BGN: %s", ex.what()); + } } else if (property == "sleepTimers_2_end") { - si.setL_2SNZ_END(convertToInteger(p)); + try { + si.L_2SNZ_END = convertToInteger(p); + } catch (const std::exception& ex) { + debugE("Failed to set L_2SNZ_END: %s", ex.what()); + } } else if (property == "status_spaMode") { si.setMode(p); } else if (property == "filtration_blockDuration") { From d9d72a4f85fef586ca262df5d9c218950ea5928a Mon Sep 17 00:00:00 2001 From: wayne-love Date: Thu, 19 Feb 2026 18:56:05 +1100 Subject: [PATCH 06/44] Refactor sleep timer state handling to use setLabel method for improved clarity and error handling --- lib/SpaInterface/SpaInterface.h | 6 ++---- src/main.cpp | 29 +++++++++++------------------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index db89848..5666670 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -296,12 +296,10 @@ class SpaInterface : public SpaProperties { }; /// @brief Sleep timer 1 day mode bitmap. /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W67. - RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validate_SNZ_DAY, - SNZ_DAY_Map}; + RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validate_SNZ_DAY, SNZ_DAY_Map}; /// @brief Sleep timer 2 day mode bitmap. /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W70. - RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, &SpaInterface::validate_SNZ_DAY, - SNZ_DAY_Map}; + RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, &SpaInterface::validate_SNZ_DAY, SNZ_DAY_Map}; /// @brief Sleep timer 1 start time. /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W68. diff --git a/src/main.cpp b/src/main.cpp index e9f8f14..281f4de 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -534,24 +534,17 @@ void setSpaProperty(String property, String p) { else si.setVARIValue(p.toInt()); } else if (property == "blower_mode") { si.setOutlet_Blower(p=="Variable"?0:1); - } else if (property == "sleepTimers_1_state" || property == "sleepTimers_2_state") { - for (const auto& entry : SpaInterface::SNZ_DAY_Map) { - if (p == entry.label) { - if (property == "sleepTimers_1_state") { - try { - si.L_1SNZ_DAY = entry.value; - } catch (const std::exception& ex) { - debugE("Failed to set L_1SNZ_DAY: %s", ex.what()); - } - } else if (property == "sleepTimers_2_state") { - try { - si.L_2SNZ_DAY = entry.value; - } catch (const std::exception& ex) { - debugE("Failed to set L_2SNZ_DAY: %s", ex.what()); - } - } - break; - } + } else if (property == "sleepTimers_1_state") { + try { + si.L_1SNZ_DAY.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set L_1SNZ_DAY from label '%s': %s", p.c_str(), ex.what()); + } + } else if (property == "sleepTimers_2_state") { + try { + si.L_2SNZ_DAY.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set L_2SNZ_DAY from label '%s': %s", p.c_str(), ex.what()); } } else if (property == "sleepTimers_1_begin") { try { From ad36542f49b26610f30ef9758d8d0cf98687b4a6 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Fri, 20 Feb 2026 15:59:55 +1100 Subject: [PATCH 07/44] Debug github build --- .github/workflows/build_firmware.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 5bbe7f8..4ecf882 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -46,7 +46,7 @@ jobs: - name: Build file system image for ${{ inputs.platform }} run: pio run --target buildfs -e ${{ inputs.platform }} - name: Build for ${{ inputs.platform }} - run: pio run -e ${{ inputs.platform }} -t mergebin + run: pio run -e ${{ inputs.platform }} -v -t mergebin #- name: Display firmware files # run: ls -la .pio/build/${{ inputs.platform }}/firmware*.bin - name: Rename and move firmware files From a12533ca4f1b843013b6b2dc958e2110af1ba217 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Fri, 20 Feb 2026 16:14:54 +1100 Subject: [PATCH 08/44] testing 2020 standards compliance --- platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio.ini b/platformio.ini index 516c859..872da88 100644 --- a/platformio.ini +++ b/platformio.ini @@ -17,6 +17,7 @@ platform = espressif32 framework = arduino monitor_speed = 115200 lib_ldf_mode = deep +build_flags = -std=gnu++2a board_build.filesystem = spiffs lib_deps = https://github.com/ktos/RemoteDebug.git@^3.0.7 From b63ffee8c88b1ca7486bd52856117f0fb127eb44 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 21 Feb 2026 12:09:25 +1100 Subject: [PATCH 09/44] Pin platform version --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 872da88..8965dd4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,7 +13,7 @@ default_envs = esp32dev, espa-v1, espa-v2 ; data_dir = {$PROJECT_DIR}/data [env:spa-base] -platform = espressif32 +platform = espressif32@55.3.34 framework = arduino monitor_speed = 115200 lib_ldf_mode = deep From b275c590aa399cdd941703565037494faf705a52 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 21 Feb 2026 14:57:10 +1100 Subject: [PATCH 10/44] Explicitly define pioarduino --- platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8965dd4..5f752e8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,11 +13,11 @@ default_envs = esp32dev, espa-v1, espa-v2 ; data_dir = {$PROJECT_DIR}/data [env:spa-base] -platform = espressif32@55.3.34 +platform = https://github.com/pioarduino/platform-espressif32.git#55.03.34 framework = arduino monitor_speed = 115200 lib_ldf_mode = deep -build_flags = -std=gnu++2a +; build_flags = -std=gnu++2a board_build.filesystem = spiffs lib_deps = https://github.com/ktos/RemoteDebug.git@^3.0.7 From 87d5ae1d374e5551babc4a6094965a2cda6f267b Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 21 Feb 2026 15:58:08 +1100 Subject: [PATCH 11/44] Migrate HPMP and ColorMode --- lib/HAAutoDiscovery/HAAutoDiscovery.h | 41 ++++++++++++++++++++ lib/SpaInterface/SpaInterface.cpp | 56 +++++++++++++-------------- lib/SpaInterface/SpaInterface.h | 52 +++++++++++++++++++------ lib/SpaInterface/SpaProperties.cpp | 8 ---- lib/SpaInterface/SpaProperties.h | 18 --------- lib/SpaUtils/SpaUtils.cpp | 6 +-- src/main.cpp | 16 ++++++-- 7 files changed, 123 insertions(+), 74 deletions(-) diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.h b/lib/HAAutoDiscovery/HAAutoDiscovery.h index 3518b62..d847f38 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.h +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.h @@ -110,6 +110,47 @@ void generateLightAdJSON(String& output, const AutoDiscoveryInformationTemplate& serializeJson(json, output); } +template +void generateLightAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, const SpaInterface::ROProperty& prop) { + JsonDocument json; + generateCommonAdJSON(json, config, spa, discoveryTopic, "light"); + + json["brightness_state_topic"] = spa.stateTopic; + json["color_mode_state_topic"] = spa.stateTopic; + json["effect_state_topic"] = spa.stateTopic; + json["hs_state_topic"] = spa.stateTopic; + + json["command_topic"] = spa.commandTopic + "/" + config.propertyId + "_state"; + json["brightness_command_topic"] = spa.commandTopic + "/" + config.propertyId + "_brightness"; + json["effect_command_topic"] = spa.commandTopic + "/" + config.propertyId + "_effect"; + json["hs_command_topic"] = spa.commandTopic + "/" + config.propertyId + "_color"; + + int lastIndex = config.valueTemplate.length() - 1; + while (lastIndex >= 0 && (config.valueTemplate[lastIndex] == ' ' || config.valueTemplate[lastIndex] == '}')) { + lastIndex--; + } + + json["state_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".state" + config.valueTemplate.substring(lastIndex + 1); + json["brightness_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".brightness" + config.valueTemplate.substring(lastIndex + 1); + json["effect_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".effect" + config.valueTemplate.substring(lastIndex + 1); + json["hs_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".color.h" + config.valueTemplate.substring(lastIndex + 1) + "," + + config.valueTemplate.substring(0, lastIndex + 1) + ".color.s" + config.valueTemplate.substring(lastIndex + 1); + json["color_mode_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".color_mode" + config.valueTemplate.substring(lastIndex + 1); + + json["brightness"] = true; + json["brightness_scale"]=5; + json["effect"] = true; + JsonArray effect_list = json["effect_list"].to(); + const size_t count = prop.getLabelCount(); + for (size_t i = 0; i < count; i++) { + effect_list.add(prop.getLabelAt(i)); + } + JsonArray color_modes = json["supported_color_modes"].to(); + color_modes.add("hs"); + + serializeJson(json, output); +} + void generateClimateAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic); /* diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 2fea305..0fb946c 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -201,6 +201,14 @@ bool SpaInterface::validateSTMP(int value) { return value >= 50 && value <= 410; } +bool SpaInterface::validateHPMP(int value) { + return value >= 0 && value <= 3; +} + +bool SpaInterface::validateColorMode(int value) { + return value >= 0 && value <= 4; +} + bool SpaInterface::validate_SNZ_DAY(int value) { for (size_t i = 0; i < array_count(SNZ_DAY_Map); i++) { if (SNZ_DAY_Map[i].value == value) { @@ -302,56 +310,46 @@ bool SpaInterface::setL_2SNZ_END(int mode){ } bool SpaInterface::setHPMP(int mode){ + // Internal writer for HPMP RWProperty. debugD("setHPMP - %i", mode); - if (mode == getHPMP()) { - debugD("No HPMP change detected - current %i, new %i", getHPMP(), mode); + if (mode == HPMP.get()) { + debugD("No HPMP change detected - current %i, new %i", HPMP.get(), mode); return true; } + if (!validateHPMP(mode)) { + debugD("Invalid HPMP mode %i", mode); + return false; + } + String smode = String(mode); if (sendCommandCheckResult("W99:"+smode,smode)) { - update_HPMP(smode); + HPMP.update(mode); return true; } return false; } -bool SpaInterface::setHPMP(String mode){ - debugD("setHPMP - %s", mode.c_str()); - - for (uint x=0; x` to the controller and only updates + /// the cached property value when the command succeeds. + /// Valid range is enforced by `validateHPMP` (0..3). + bool setHPMP(int mode); + /// @brief Internal writer used by `ColorMode` RWProperty. + /// @details Sends `S07:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Valid range is enforced by `validateColorMode` (0..4). + bool setColorMode(int mode); /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) /// @param mode @@ -279,6 +292,33 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid range 50..410 (5.0C..41.0C). Uses command W40. RWProperty STMP{this, &SpaInterface::setSTMP, &SpaInterface::validateSTMP}; + /// @brief Heatpump operating mode values. + /// @details 0=Auto, 1=Heat, 2=Cool, 3=Off. + static constexpr ROProperty::LabelValue HPMP_Map[] = { + {"Auto", 0}, + {"Heat", 1}, + {"Cool", 2}, + {"Off", 3}, + }; + /// @brief Heatpump operating mode. + /// @details Read/write wrapper around the private `setHPMP` writer. + /// Valid range 0..3. Uses command W99. + RWProperty HPMP{this, &SpaInterface::setHPMP, &SpaInterface::validateHPMP, HPMP_Map}; + + /// @brief Light effect/mode values. + /// @details 0=White, 1=Color, 2=Fade, 3=Step, 4=Party. + static constexpr ROProperty::LabelValue ColorMode_Map[] = { + {"White", 0}, + {"Color", 1}, + {"Fade", 2}, + {"Step", 3}, + {"Party", 4}, + }; + /// @brief Light effect/mode. + /// @details Read/write wrapper around the private `setColorMode` writer. + /// Valid range 0..4. Uses command S07. + RWProperty ColorMode{this, &SpaInterface::setColorMode, &SpaInterface::validateColorMode, ColorMode_Map}; + /// @brief Sleep timer day mode values. /// @details Shared label/value map for both timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. static constexpr ROProperty::LabelValue SNZ_DAY_Map[] = { @@ -332,18 +372,6 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Set Heat pump operating mode (0 --> 3, {auto, heat, cool, off}) - /// @param mode - /// @return Returns True if succesful - bool setHPMP(int mode); - bool setHPMP(String mode); - - /// @brief Set light mode (0 = white, 1 = colour, 2 = step, 3 = fade, 4 = party) - /// @param mode - /// @return Returns True if succesful - bool setColorMode(int mode); - bool setColorMode(String mode); - /// @brief Set light brightness (min 1, max 5) /// @param mode /// @return Returns True if succesful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 7b4fded..a2dfe6e 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -399,10 +399,6 @@ boolean SpaProperties::update_CurrClr(const String& s){ return updateIntProperty(CurrClr, s); } -boolean SpaProperties::update_ColorMode(const String& s){ - return updateIntProperty(ColorMode, s); -} - boolean SpaProperties::update_LSPDValue(const String& s){ return updateIntProperty(LSPDValue, s); } @@ -552,10 +548,6 @@ boolean SpaProperties::update_HELE(const String& s) { return updateBool01Property(HELE, s); } -boolean SpaProperties::update_HPMP(const String& s){ - return updateIntProperty(HPMP, s); -} - boolean SpaProperties::update_PMIN(const String& s){ return updateIntProperty(PMIN, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 9edc3a4..a62c6e7 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -268,10 +268,6 @@ Property HV_2; /// /// min 0, max 31 Property CurrClr; - /// @brief Lights mode - /// - /// 0 = white, 1 = colour, 2 = step, 3 = fade, 4 = party - Property ColorMode; /// @brief Light effect speed /// /// min 1, max 5 @@ -399,10 +395,6 @@ Property HV_2; /// If false then when spa is in use then heat pump will not run to reduce noise levels /// See SV-Series-OEM-Install-Manual.pdf page 19. Property HELE; - /// @brief Heatpump mode - /// - /// 0 = Auto, 1 = Heat, 2 = Cool, 3 = disabled - Property HPMP; /// @brief Varible pump minimum speed setting /// /// Min 20%, Max 100% @@ -711,7 +703,6 @@ Property HV_2; boolean update_VARIValue(const String&); boolean update_LBRTValue(const String&); boolean update_CurrClr(const String&); - boolean update_ColorMode(const String&); boolean update_LSPDValue(const String&); boolean update_FiltHrs(const String&); boolean update_FiltBlockHrs(const String&); @@ -753,7 +744,6 @@ Property HV_2; boolean update_AHYS(const String&); boolean update_HUSE(const String&); boolean update_HELE(const String&); - boolean update_HPMP(const String&); boolean update_PMIN(const String&); boolean update_PFLT(const String&); boolean update_PHTR(const String&); @@ -1167,10 +1157,6 @@ Property HV_2; void setCurrClrCallback(void (*callback)(int)) { CurrClr.setCallback(callback); } const std::array colorMap = {0, 4, 4, 19, 13, 25, 25, 16, 10, 7, 2, 8, 5, 3, 6, 6, 21, 21, 21, 18, 18, 9, 9, 1, 1}; - int getColorMode() { return ColorMode.getValue(); } - void setColorModeCallback(void (*callback)(int)) { ColorMode.setCallback(callback); } - const std::array colorModeStrings = {"White","Color","Fade","Step","Party"}; - int getLSPDValue() { return LSPDValue.getValue(); } void setLSPDValueCallback(void (*callback)(int)) { LSPDValue.setCallback(callback); } const std::array lightSpeedMap = {"1","2","3","4","5"}; @@ -1302,10 +1288,6 @@ Property HV_2; bool getHELE() { return HELE.getValue(); } void setHELECallback(void (*callback)(bool)) { HELE.setCallback(callback); } - int getHPMP() { return HPMP.getValue(); } - void setHPMPCallback(void (*callback)(int)) { HPMP.setCallback(callback); } - const std::array HPMPStrings = {"Auto","Heat","Cool","Off"}; - int getPMIN() { return PMIN.getValue(); } void setPMINCallback(void (*callback)(int)) { PMIN.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 565e40b..bcbd342 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -156,7 +156,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["eSpa"]["model"] = xstr(PIOENV); json["eSpa"]["update"]["installed_version"] = xstr(BUILD_INFO); - json["heatpump"]["mode"] = si.HPMPStrings[si.getHPMP()]; + json["heatpump"]["mode"] = si.HPMP.getLabel(); json["heatpump"]["auxheat"] = si.getHELE()==0? "OFF" : "ON"; json["filtration"]["blockDuration"] = si.getFiltBlockHrs(); @@ -200,11 +200,11 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["lights"]["speed"] = si.getLSPDValue(); json["lights"]["state"] = si.getRB_TP_Light()? "ON": "OFF"; - json["lights"]["effect"] = si.colorModeStrings[si.getColorMode()]; + json["lights"]["effect"] = si.ColorMode.getLabel(); json["lights"]["brightness"] = si.getLBRTValue(); // 0 = white, if white, then set the hue and saturation to white so the light displays correctly in HA. - if (si.getColorMode() == 0) { + if (si.ColorMode.get() == 0) { json["lights"]["color"]["h"] = 0; json["lights"]["color"]["s"] = 0; } else { diff --git a/src/main.cpp b/src/main.cpp index 281f4de..f5a027d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -309,7 +309,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "heatpump_mode"; ADConf.deviceClass = ""; ADConf.entityCategory = ""; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.HPMPStrings); + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.HPMP); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Aux Heat Element"; @@ -326,7 +326,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "lights"; ADConf.deviceClass = ""; ADConf.entityCategory = ""; - generateLightAdJSON(output, ADConf, spa, discoveryTopic, si.colorModeStrings); + generateLightAdJSON(output, ADConf, spa, discoveryTopic, si.ColorMode); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Lights Speed"; @@ -476,7 +476,11 @@ void setSpaProperty(String property, String p) { debugE("Failed to set STMP: %s", ex.what()); } } else if (property == "heatpump_mode") { - si.setHPMP(p); + try { + si.HPMP.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set HPMP label: %s", ex.what()); + } // note single speed pumps should never trigger a mode or speed events } else if (property.startsWith("pump") && property.endsWith("_speed")) { int pumpNum = property.charAt(4) - '0'; @@ -516,7 +520,11 @@ void setSpaProperty(String property, String p) { } else if (property == "lights_state") { si.setRB_TP_Light(p=="ON"?1:0); } else if (property == "lights_effect") { - si.setColorMode(p); + try { + si.ColorMode.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set ColorMode label: %s", ex.what()); + } } else if (property == "lights_brightness") { si.setLBRTValue(p.toInt()); } else if (property == "lights_color") { From 42c1f4753313506c2c13ef7748c7a039ce01efa0 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 22 Feb 2026 11:56:13 +1100 Subject: [PATCH 12/44] Remove explicit validators --- lib/SpaInterface/SpaInterface.cpp | 106 +++++++++++++++++++----------- lib/SpaInterface/SpaInterface.h | 43 ++++-------- 2 files changed, 81 insertions(+), 68 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 0fb946c..8e3f078 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -179,6 +179,10 @@ bool SpaInterface::setHELE(int mode){ bool SpaInterface::setSTMP(int temp){ debugD("setSTMP - %i", temp); + if (temp < 50 || temp > 410) { + throw std::out_of_range("STMP value out of range (50..410)"); + } + if (temp==STMP.get()) { // todo: does this ever evaluate to true? debugD("No STMP change detected - current %i, new %i", STMP.get(), temp); return true; // No change needed @@ -197,40 +201,19 @@ bool SpaInterface::setSTMP(int temp){ return false; } -bool SpaInterface::validateSTMP(int value) { - return value >= 50 && value <= 410; -} - -bool SpaInterface::validateHPMP(int value) { - return value >= 0 && value <= 3; -} - -bool SpaInterface::validateColorMode(int value) { - return value >= 0 && value <= 4; -} - -bool SpaInterface::validate_SNZ_DAY(int value) { +bool SpaInterface::setL_1SNZ_DAY(int mode){ + debugD("setL_1SNZ_DAY - %i",mode); + bool validMode = false; for (size_t i = 0; i < array_count(SNZ_DAY_Map); i++) { - if (SNZ_DAY_Map[i].value == value) { - return true; + if (SNZ_DAY_Map[i].value == mode) { + validMode = true; + break; } } - return false; -} - -bool SpaInterface::validate_SNZ_TIME(int value) { - if (value < 0) { - return false; + if (!validMode) { + throw std::out_of_range("L_1SNZ_DAY value out of range"); } - int hour = value / 256; - int minute = value % 256; - - return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59; -} - -bool SpaInterface::setL_1SNZ_DAY(int mode){ - debugD("setL_1SNZ_DAY - %i",mode); if (mode == L_1SNZ_DAY.get()) { debugD("No L_1SNZ_DAY change detected - current %i, new %i", L_1SNZ_DAY.get(), mode); return true; @@ -245,6 +228,15 @@ bool SpaInterface::setL_1SNZ_DAY(int mode){ bool SpaInterface::setL_1SNZ_BGN(int mode){ debugD("setL_1SNZ_BGN - %i",mode); + if (mode < 0) { + throw std::out_of_range("L_1SNZ_BGN value out of range"); + } + int hour = mode / 256; + int minute = mode % 256; + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { + throw std::out_of_range("L_1SNZ_BGN value out of range"); + } + if (mode == L_1SNZ_BGN.get()) { debugD("No L_1SNZ_BGN change detected - current %i, new %i", L_1SNZ_BGN.get(), mode); return true; @@ -258,6 +250,15 @@ bool SpaInterface::setL_1SNZ_BGN(int mode){ bool SpaInterface::setL_1SNZ_END(int mode){ debugD("setL_1SNZ_END - %i",mode); + if (mode < 0) { + throw std::out_of_range("L_1SNZ_END value out of range"); + } + int hour = mode / 256; + int minute = mode % 256; + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { + throw std::out_of_range("L_1SNZ_END value out of range"); + } + if (mode == L_1SNZ_END.get()) { debugD("No L_1SNZ_END change detected - current %i, new %i", L_1SNZ_END.get(), mode); return true; @@ -271,6 +272,17 @@ bool SpaInterface::setL_1SNZ_END(int mode){ bool SpaInterface::setL_2SNZ_DAY(int mode){ debugD("setL_2SNZ_DAY - %i",mode); + bool validMode = false; + for (size_t i = 0; i < array_count(SNZ_DAY_Map); i++) { + if (SNZ_DAY_Map[i].value == mode) { + validMode = true; + break; + } + } + if (!validMode) { + throw std::out_of_range("L_2SNZ_DAY value out of range"); + } + if (mode == L_2SNZ_DAY.get()) { debugD("No L_2SNZ_DAY change detected - current %i, new %i", L_2SNZ_DAY.get(), mode); return true; @@ -285,6 +297,15 @@ bool SpaInterface::setL_2SNZ_DAY(int mode){ bool SpaInterface::setL_2SNZ_BGN(int mode){ debugD("setL_2SNZ_BGN - %i",mode); + if (mode < 0) { + throw std::out_of_range("L_2SNZ_BGN value out of range"); + } + int hour = mode / 256; + int minute = mode % 256; + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { + throw std::out_of_range("L_2SNZ_BGN value out of range"); + } + if (mode == L_2SNZ_BGN.get()) { debugD("No L_2SNZ_BGN change detected - current %i, new %i", L_2SNZ_BGN.get(), mode); return true; @@ -298,6 +319,15 @@ bool SpaInterface::setL_2SNZ_BGN(int mode){ bool SpaInterface::setL_2SNZ_END(int mode){ debugD("setL_2SNZ_END - %i",mode); + if (mode < 0) { + throw std::out_of_range("L_2SNZ_END value out of range"); + } + int hour = mode / 256; + int minute = mode % 256; + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { + throw std::out_of_range("L_2SNZ_END value out of range"); + } + if (mode == L_2SNZ_END.get()) { debugD("No L_2SNZ_END change detected - current %i, new %i", L_2SNZ_END.get(), mode); return true; @@ -312,16 +342,15 @@ bool SpaInterface::setL_2SNZ_END(int mode){ bool SpaInterface::setHPMP(int mode){ // Internal writer for HPMP RWProperty. debugD("setHPMP - %i", mode); + if (mode < 0 || mode > 3) { + throw std::out_of_range("HPMP value out of range (0..3)"); + } + if (mode == HPMP.get()) { debugD("No HPMP change detected - current %i, new %i", HPMP.get(), mode); return true; } - if (!validateHPMP(mode)) { - debugD("Invalid HPMP mode %i", mode); - return false; - } - String smode = String(mode); if (sendCommandCheckResult("W99:"+smode,smode)) { HPMP.update(mode); @@ -332,16 +361,15 @@ bool SpaInterface::setHPMP(int mode){ bool SpaInterface::setColorMode(int mode){ debugD("setColorMode - %i", mode); + if (mode < 0 || mode > 4) { + throw std::out_of_range("ColorMode value out of range (0..4)"); + } + if (mode == ColorMode.get()) { debugD("No ColorMode change detected - current %i, new %i", ColorMode.get(), mode); return true; } - if (!validateColorMode(mode)) { - debugD("Invalid ColorMode %i", mode); - return false; - } - String smode = String(mode); if (sendCommandCheckResult("S07:"+smode,smode)) { ColorMode.update(mode); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index d827bf1..47714cb 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -67,12 +67,6 @@ class SpaInterface : public SpaProperties { void updateMeasures(); - static bool validateSTMP(int value); - static bool validateHPMP(int value); - static bool validateColorMode(int value); - static bool validate_SNZ_DAY(int value); - static bool validate_SNZ_TIME(int value); - /// @brief Sends command to SpaNet controller. Result must be read by some other method. /// Used for the 'RF' command so that we can do a optomised read of the return array. /// @param cmd - cmd to be executed. @@ -119,12 +113,12 @@ class SpaInterface : public SpaProperties { /// @brief Internal writer used by `HPMP` RWProperty. /// @details Sends `W99:` to the controller and only updates /// the cached property value when the command succeeds. - /// Valid range is enforced by `validateHPMP` (0..3). + /// Throws std::out_of_range for invalid values (0..3). bool setHPMP(int mode); /// @brief Internal writer used by `ColorMode` RWProperty. /// @details Sends `S07:` to the controller and only updates /// the cached property value when the command succeeds. - /// Valid range is enforced by `validateColorMode` (0..4). + /// Throws std::out_of_range for invalid values (0..4). bool setColorMode(int mode); /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) @@ -211,26 +205,22 @@ class SpaInterface : public SpaProperties { class RWProperty : public ROProperty { public: using WriteFunction = bool (SpaInterface::*)(T); - using ValidateFunction = bool (*)(T); using LabelValue = typename ROProperty::LabelValue; RWProperty() = default; // Basic RW property with owner/writer only. RWProperty(SpaInterface* owner, WriteFunction writer) : _owner(owner), _writer(writer) {} - // RW property with optional validator. - RWProperty(SpaInterface* owner, WriteFunction writer, ValidateFunction validator) - : _owner(owner), _writer(writer), _validator(validator) {} // RW property with label/value map for setLabel/getLabel. template - RWProperty(SpaInterface* owner, WriteFunction writer, ValidateFunction validator, + RWProperty(SpaInterface* owner, WriteFunction writer, const LabelValue (&map)[N]) : ROProperty(map), _owner(owner), - _writer(writer), - _validator(validator) {} + _writer(writer) {} // Sends the value to the spa first; caches it only on success. + // Validation is performed by the writer and any exception is propagated. void set(T newValue) { if (this->_hasValue && newValue == this->_value) { return; @@ -240,10 +230,6 @@ class SpaInterface : public SpaProperties { throw std::invalid_argument("RWProperty has no owner/writer"); } - if (_validator && !_validator(newValue)) { - throw std::out_of_range("RWProperty value out of range"); - } - if (!(_owner->*_writer)(newValue)) { throw std::runtime_error("RWProperty write failed"); } @@ -274,7 +260,6 @@ class SpaInterface : public SpaProperties { // Owner + writer are required to commit changes to the spa. SpaInterface* _owner = nullptr; WriteFunction _writer = nullptr; - ValidateFunction _validator = nullptr; }; /// @brief configure how often the spa is polled in seconds. @@ -290,7 +275,7 @@ class SpaInterface : public SpaProperties { /// @brief Water temperature set point x10. /// @details Read/write. Valid range 50..410 (5.0C..41.0C). Uses command W40. - RWProperty STMP{this, &SpaInterface::setSTMP, &SpaInterface::validateSTMP}; + RWProperty STMP{this, &SpaInterface::setSTMP}; /// @brief Heatpump operating mode values. /// @details 0=Auto, 1=Heat, 2=Cool, 3=Off. @@ -303,7 +288,7 @@ class SpaInterface : public SpaProperties { /// @brief Heatpump operating mode. /// @details Read/write wrapper around the private `setHPMP` writer. /// Valid range 0..3. Uses command W99. - RWProperty HPMP{this, &SpaInterface::setHPMP, &SpaInterface::validateHPMP, HPMP_Map}; + RWProperty HPMP{this, &SpaInterface::setHPMP, HPMP_Map}; /// @brief Light effect/mode values. /// @details 0=White, 1=Color, 2=Fade, 3=Step, 4=Party. @@ -317,7 +302,7 @@ class SpaInterface : public SpaProperties { /// @brief Light effect/mode. /// @details Read/write wrapper around the private `setColorMode` writer. /// Valid range 0..4. Uses command S07. - RWProperty ColorMode{this, &SpaInterface::setColorMode, &SpaInterface::validateColorMode, ColorMode_Map}; + RWProperty ColorMode{this, &SpaInterface::setColorMode, ColorMode_Map}; /// @brief Sleep timer day mode values. /// @details Shared label/value map for both timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. @@ -336,23 +321,23 @@ class SpaInterface : public SpaProperties { }; /// @brief Sleep timer 1 day mode bitmap. /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W67. - RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, &SpaInterface::validate_SNZ_DAY, SNZ_DAY_Map}; + RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, SNZ_DAY_Map}; /// @brief Sleep timer 2 day mode bitmap. /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W70. - RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, &SpaInterface::validate_SNZ_DAY, SNZ_DAY_Map}; + RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, SNZ_DAY_Map}; /// @brief Sleep timer 1 start time. /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W68. - RWProperty L_1SNZ_BGN{this, &SpaInterface::setL_1SNZ_BGN, &SpaInterface::validate_SNZ_TIME}; + RWProperty L_1SNZ_BGN{this, &SpaInterface::setL_1SNZ_BGN}; /// @brief Sleep timer 1 finish time. /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W69. - RWProperty L_1SNZ_END{this, &SpaInterface::setL_1SNZ_END, &SpaInterface::validate_SNZ_TIME}; + RWProperty L_1SNZ_END{this, &SpaInterface::setL_1SNZ_END}; /// @brief Sleep timer 2 start time. /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W71. - RWProperty L_2SNZ_BGN{this, &SpaInterface::setL_2SNZ_BGN, &SpaInterface::validate_SNZ_TIME}; + RWProperty L_2SNZ_BGN{this, &SpaInterface::setL_2SNZ_BGN}; /// @brief Sleep timer 2 finish time. /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W72. - RWProperty L_2SNZ_END{this, &SpaInterface::setL_2SNZ_END, &SpaInterface::validate_SNZ_TIME}; + RWProperty L_2SNZ_END{this, &SpaInterface::setL_2SNZ_END}; /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); From 884fbdc3d87d03e2978894d97fe6ba658845d368 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 22 Feb 2026 12:02:03 +1100 Subject: [PATCH 13/44] Migrate LBRTValue --- lib/SpaInterface/SpaInterface.cpp | 12 +++++++---- lib/SpaInterface/SpaInterface.h | 32 +++++++++++++++++------------- lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 8 -------- lib/SpaUtils/SpaUtils.cpp | 20 +++++++++---------- src/main.cpp | 6 +++++- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 8e3f078..95edd99 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -380,14 +380,18 @@ bool SpaInterface::setColorMode(int mode){ bool SpaInterface::setLBRTValue(int mode){ debugD("setLBRTValue - %i", mode); - if (mode == getLBRTValue()) { - debugD("No LBRTValue change detected - current %i, new %i", getLBRTValue(), mode); + if (mode < 1 || mode > 5) { + throw std::out_of_range("LBRTValue value out of range (1..5)"); + } + + if (mode == LBRTValue.get()) { + debugD("No LBRTValue change detected - current %i, new %i", LBRTValue.get(), mode); return true; } String smode = String(mode); if (sendCommandCheckResult("S08:"+smode,smode)) { - update_LBRTValue(smode); + LBRTValue.update(mode); return true; } return false; @@ -892,7 +896,7 @@ void SpaInterface::updateMeasures() { #pragma endregion #pragma region R6 update_VARIValue(statusResponseRaw[R6 + 1]); - update_LBRTValue(statusResponseRaw[R6 + 2]); + LBRTValue.update(statusResponseRaw[R6 + 2].toInt()); update_CurrClr(statusResponseRaw[R6 + 3]); ColorMode.update(statusResponseRaw[R6 + 4].toInt()); update_LSPDValue(statusResponseRaw[R6 + 5]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 47714cb..e195b2c 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -120,6 +120,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (0..4). bool setColorMode(int mode); + /// @brief Internal writer used by `LBRTValue` RWProperty. + /// @details Sends `S08:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (1..5). + bool setLBRTValue(int mode); /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) /// @param mode @@ -274,7 +279,7 @@ class SpaInterface : public SpaProperties { ROProperty MainsCurrent; /// @brief Water temperature set point x10. - /// @details Read/write. Valid range 50..410 (5.0C..41.0C). Uses command W40. + /// @details Read/write. Valid range 50..410 (5.0C..41.0C). RWProperty STMP{this, &SpaInterface::setSTMP}; /// @brief Heatpump operating mode values. @@ -287,7 +292,7 @@ class SpaInterface : public SpaProperties { }; /// @brief Heatpump operating mode. /// @details Read/write wrapper around the private `setHPMP` writer. - /// Valid range 0..3. Uses command W99. + /// Valid range 0..3. RWProperty HPMP{this, &SpaInterface::setHPMP, HPMP_Map}; /// @brief Light effect/mode values. @@ -301,9 +306,13 @@ class SpaInterface : public SpaProperties { }; /// @brief Light effect/mode. /// @details Read/write wrapper around the private `setColorMode` writer. - /// Valid range 0..4. Uses command S07. + /// Valid range 0..4. RWProperty ColorMode{this, &SpaInterface::setColorMode, ColorMode_Map}; + /// @brief Light brightness. + /// @details Read/write. Valid range 1..5. + RWProperty LBRTValue{this, &SpaInterface::setLBRTValue}; + /// @brief Sleep timer day mode values. /// @details Shared label/value map for both timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. static constexpr ROProperty::LabelValue SNZ_DAY_Map[] = { @@ -320,23 +329,23 @@ class SpaInterface : public SpaProperties { {"Sunday", 32}, }; /// @brief Sleep timer 1 day mode bitmap. - /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W67. + /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, SNZ_DAY_Map}; /// @brief Sleep timer 2 day mode bitmap. - /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. Uses command W70. + /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, SNZ_DAY_Map}; /// @brief Sleep timer 1 start time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W68. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). RWProperty L_1SNZ_BGN{this, &SpaInterface::setL_1SNZ_BGN}; /// @brief Sleep timer 1 finish time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W69. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). RWProperty L_1SNZ_END{this, &SpaInterface::setL_1SNZ_END}; /// @brief Sleep timer 2 start time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W71. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). RWProperty L_2SNZ_BGN{this, &SpaInterface::setL_2SNZ_BGN}; /// @brief Sleep timer 2 finish time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). Uses command W72. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). RWProperty L_2SNZ_END{this, &SpaInterface::setL_2SNZ_END}; /// @brief To be called by loop function of main sketch. Does regular updates, etc. @@ -357,11 +366,6 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Set light brightness (min 1, max 5) - /// @param mode - /// @return Returns True if succesful - bool setLBRTValue(int mode); - /// @brief Set light effect speed (min 1, max 5) /// @param mode /// @return Returns True if succesful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index a2dfe6e..f994780 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -391,10 +391,6 @@ boolean SpaProperties::update_VARIValue(const String& s){ return updateIntProperty(VARIValue, s); } -boolean SpaProperties::update_LBRTValue(const String& s){ - return updateIntProperty(LBRTValue, s); -} - boolean SpaProperties::update_CurrClr(const String& s){ return updateIntProperty(CurrClr, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index a62c6e7..b05d6c9 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -260,10 +260,6 @@ Property HV_2; /// /// min 1, max 5 Property VARIValue; - /// @brief Lights brightness - /// - /// min 1, max 5 - Property LBRTValue; /// @brief Light colour /// /// min 0, max 31 @@ -701,7 +697,6 @@ Property HV_2; #pragma endregion #pragma region R6 boolean update_VARIValue(const String&); - boolean update_LBRTValue(const String&); boolean update_CurrClr(const String&); boolean update_LSPDValue(const String&); boolean update_FiltHrs(const String&); @@ -1150,9 +1145,6 @@ Property HV_2; int getVARIValue() { return VARIValue.getValue(); } void setVARIValueCallback(void (*callback)(int)) { VARIValue.setCallback(callback); } - int getLBRTValue() { return LBRTValue.getValue(); } - void setLBRTValueCallback(void (*callback)(int)) { LBRTValue.setCallback(callback); } - int getCurrClr() { return CurrClr.getValue(); } void setCurrClrCallback(void (*callback)(int)) { CurrClr.setCallback(callback); } const std::array colorMap = {0, 4, 4, 19, 13, 25, 25, 16, 10, 7, 2, 8, 5, 3, 6, 6, 21, 21, 21, 18, 18, 9, 9, 1, 1}; diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index bcbd342..e394cd5 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -156,7 +156,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["eSpa"]["model"] = xstr(PIOENV); json["eSpa"]["update"]["installed_version"] = xstr(BUILD_INFO); - json["heatpump"]["mode"] = si.HPMP.getLabel(); + json["heatpump"]["mode"] = si.HPMP.getLabel(); json["heatpump"]["auxheat"] = si.getHELE()==0? "OFF" : "ON"; json["filtration"]["blockDuration"] = si.getFiltBlockHrs(); @@ -191,20 +191,20 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["blower"]["mode"] = si.getOutlet_Blower()==1? "Ramp" : "Variable"; json["blower"]["speed"] = si.getOutlet_Blower() ==2? "0" : String(si.getVARIValue()); - json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); - json["sleepTimers"]["timer2"]["state"] = si.L_2SNZ_DAY.getLabel(); - json["sleepTimers"]["timer1"]["begin"]=convertToTime(si.L_1SNZ_BGN.get()); - json["sleepTimers"]["timer1"]["end"]=convertToTime(si.L_1SNZ_END.get()); - json["sleepTimers"]["timer2"]["begin"]=convertToTime(si.L_2SNZ_BGN.get()); - json["sleepTimers"]["timer2"]["end"]=convertToTime(si.L_2SNZ_END.get()); + json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); + json["sleepTimers"]["timer2"]["state"] = si.L_2SNZ_DAY.getLabel(); + json["sleepTimers"]["timer1"]["begin"]=convertToTime(si.L_1SNZ_BGN.get()); + json["sleepTimers"]["timer1"]["end"]=convertToTime(si.L_1SNZ_END.get()); + json["sleepTimers"]["timer2"]["begin"]=convertToTime(si.L_2SNZ_BGN.get()); + json["sleepTimers"]["timer2"]["end"]=convertToTime(si.L_2SNZ_END.get()); json["lights"]["speed"] = si.getLSPDValue(); json["lights"]["state"] = si.getRB_TP_Light()? "ON": "OFF"; - json["lights"]["effect"] = si.ColorMode.getLabel(); - json["lights"]["brightness"] = si.getLBRTValue(); + json["lights"]["effect"] = si.ColorMode.getLabel(); + json["lights"]["brightness"] = si.LBRTValue; // 0 = white, if white, then set the hue and saturation to white so the light displays correctly in HA. - if (si.ColorMode.get() == 0) { + if (si.ColorMode.get() == 0) { json["lights"]["color"]["h"] = 0; json["lights"]["color"]["s"] = 0; } else { diff --git a/src/main.cpp b/src/main.cpp index f5a027d..d0f4c79 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -526,7 +526,11 @@ void setSpaProperty(String property, String p) { debugE("Failed to set ColorMode label: %s", ex.what()); } } else if (property == "lights_brightness") { - si.setLBRTValue(p.toInt()); + try { + si.LBRTValue = p.toInt(); + } catch (const std::exception& ex) { + debugE("Failed to set LBRTValue: %s", ex.what()); + } } else if (property == "lights_color") { int pos = p.indexOf(','); if ( pos > 0) { From 0d0b2b28656c3979924855e044de078e90613118 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 22 Feb 2026 12:15:54 +1100 Subject: [PATCH 14/44] Migreate LSPD + fix dumb error with assignement of LBRT --- lib/SpaInterface/SpaInterface.cpp | 21 ++++++++------------- lib/SpaInterface/SpaInterface.h | 24 ++++++++++++++++++------ lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 9 --------- lib/SpaUtils/SpaUtils.cpp | 4 ++-- src/main.cpp | 8 ++++++-- 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 95edd99..21db848 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -399,28 +399,23 @@ bool SpaInterface::setLBRTValue(int mode){ bool SpaInterface::setLSPDValue(int mode){ debugD("setLSPDValue - %i", mode); - if (mode == getLSPDValue()) { - debugD("No LSPDValue change detected - current %i, new %i", getLSPDValue(), mode); + if (mode < 1 || mode > 5) { + throw std::out_of_range("LSPDValue value out of range (1..5)"); + } + + if (mode == LSPDValue.get()) { + debugD("No LSPDValue change detected - current %i, new %i", LSPDValue.get(), mode); return true; } String smode = String(mode); if (sendCommandCheckResult("S09:"+smode,smode)) { - update_LSPDValue(smode); + LSPDValue.update(mode); return true; } return false; } -bool SpaInterface::setLSPDValue(String mode){ - debugD("setLSPDValue - %s", mode.c_str()); - int x = atoi(mode.c_str()); - if (x > 0 && x < 6) { - return setLSPDValue(x); - } - return false; -} - bool SpaInterface::setCurrClr(int mode){ debugD("setCurrClr - %i", mode); if (mode == getCurrClr()) { @@ -899,7 +894,7 @@ void SpaInterface::updateMeasures() { LBRTValue.update(statusResponseRaw[R6 + 2].toInt()); update_CurrClr(statusResponseRaw[R6 + 3]); ColorMode.update(statusResponseRaw[R6 + 4].toInt()); - update_LSPDValue(statusResponseRaw[R6 + 5]); + LSPDValue.update(statusResponseRaw[R6 + 5].toInt()); update_FiltHrs(statusResponseRaw[R6 + 6]); update_FiltBlockHrs(statusResponseRaw[R6 + 7]); STMP.update(statusResponseRaw[R6 + 8].toInt()); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index e195b2c..c6f9841 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -125,6 +125,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (1..5). bool setLBRTValue(int mode); + /// @brief Internal writer used by `LSPDValue` RWProperty. + /// @details Sends `S09:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (1..5). + bool setLSPDValue(int mode); /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) /// @param mode @@ -313,6 +318,19 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid range 1..5. RWProperty LBRTValue{this, &SpaInterface::setLBRTValue}; + /// @brief Light effect speed values. This seems dumb but it is used to build the UI dropdowns as we + /// present light effect speed as a select. + static constexpr ROProperty::LabelValue LSPDValue_Map[] = { + {"1", 1}, + {"2", 2}, + {"3", 3}, + {"4", 4}, + {"5", 5}, + }; + /// @brief Light effect speed. + /// @details Read/write. Valid range 1..5. + RWProperty LSPDValue{this, &SpaInterface::setLSPDValue, LSPDValue_Map}; + /// @brief Sleep timer day mode values. /// @details Shared label/value map for both timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. static constexpr ROProperty::LabelValue SNZ_DAY_Map[] = { @@ -366,12 +384,6 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Set light effect speed (min 1, max 5) - /// @param mode - /// @return Returns True if succesful - bool setLSPDValue(int mode); - bool setLSPDValue(String mode); - /// @brief Set light colour (min 0, max 31) /// @param mode /// @return Returns True if succesful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index f994780..66c5ddb 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -395,10 +395,6 @@ boolean SpaProperties::update_CurrClr(const String& s){ return updateIntProperty(CurrClr, s); } -boolean SpaProperties::update_LSPDValue(const String& s){ - return updateIntProperty(LSPDValue, s); -} - boolean SpaProperties::update_FiltHrs(const String& s){ return updateIntProperty(FiltSetHrs, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index b05d6c9..7666081 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -264,10 +264,6 @@ Property HV_2; /// /// min 0, max 31 Property CurrClr; - /// @brief Light effect speed - /// - /// min 1, max 5 - Property LSPDValue; /// @brief Filter run time (in hours) per block Property FiltSetHrs; /// @brief Filter block duration (hours) @@ -698,7 +694,6 @@ Property HV_2; #pragma region R6 boolean update_VARIValue(const String&); boolean update_CurrClr(const String&); - boolean update_LSPDValue(const String&); boolean update_FiltHrs(const String&); boolean update_FiltBlockHrs(const String&); boolean update_L_24HOURS(const String&); @@ -1149,10 +1144,6 @@ Property HV_2; void setCurrClrCallback(void (*callback)(int)) { CurrClr.setCallback(callback); } const std::array colorMap = {0, 4, 4, 19, 13, 25, 25, 16, 10, 7, 2, 8, 5, 3, 6, 6, 21, 21, 21, 18, 18, 9, 9, 1, 1}; - int getLSPDValue() { return LSPDValue.getValue(); } - void setLSPDValueCallback(void (*callback)(int)) { LSPDValue.setCallback(callback); } - const std::array lightSpeedMap = {"1","2","3","4","5"}; - int getFiltHrs() { return FiltSetHrs.getValue(); } void setFiltHrsCallback(void (*callback)(int)) { FiltSetHrs.setCallback(callback); } // This is used to generated the select diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index e394cd5..e709027 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -198,10 +198,10 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["sleepTimers"]["timer2"]["begin"]=convertToTime(si.L_2SNZ_BGN.get()); json["sleepTimers"]["timer2"]["end"]=convertToTime(si.L_2SNZ_END.get()); - json["lights"]["speed"] = si.getLSPDValue(); + json["lights"]["speed"] = si.LSPDValue.get(); json["lights"]["state"] = si.getRB_TP_Light()? "ON": "OFF"; json["lights"]["effect"] = si.ColorMode.getLabel(); - json["lights"]["brightness"] = si.LBRTValue; + json["lights"]["brightness"] = si.LBRTValue.get(); // 0 = white, if white, then set the hue and saturation to white so the light displays correctly in HA. if (si.ColorMode.get() == 0) { diff --git a/src/main.cpp b/src/main.cpp index d0f4c79..15d7f24 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -334,7 +334,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "lights_speed"; ADConf.deviceClass = ""; ADConf.entityCategory = ""; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.lightSpeedMap ); + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.LSPDValue); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Sleep Timer 1"; @@ -538,7 +538,11 @@ void setSpaProperty(String property, String p) { si.setCurrClr(si.colorMap[value/15]); } } else if (property == "lights_speed") { - si.setLSPDValue(p); + try { + si.LSPDValue = p.toInt(); + } catch (const std::exception& ex) { + debugE("Failed to set LSPDValue: %s", ex.what()); + } } else if (property == "blower_state") { si.setOutlet_Blower(p=="OFF"?2:0); } else if (property == "blower_speed") { From 7c8921d5146e425c08bd05e75c7fccdf4fe118f1 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Mon, 23 Feb 2026 17:34:19 +1100 Subject: [PATCH 15/44] Migrate CurrClr --- lib/SpaInterface/SpaInterface.cpp | 12 ++++++--- lib/SpaInterface/SpaInterface.h | 43 ++++++++++++++++++++++++++---- lib/SpaInterface/SpaProperties.cpp | 4 --- lib/SpaInterface/SpaProperties.h | 9 ------- lib/SpaUtils/SpaUtils.cpp | 21 ++++++--------- src/main.cpp | 10 ++++++- 6 files changed, 63 insertions(+), 36 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 21db848..8e5ca2e 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -418,14 +418,18 @@ bool SpaInterface::setLSPDValue(int mode){ bool SpaInterface::setCurrClr(int mode){ debugD("setCurrClr - %i", mode); - if (mode == getCurrClr()) { - debugD("No CurrClr change detected - current %i, new %i", getCurrClr(), mode); + if (mode < 0 || mode > 31) { + throw std::out_of_range("CurrClr value out of range (0..31)"); + } + + if (mode == CurrClr.get()) { + debugD("No CurrClr change detected - current %i, new %i", CurrClr.get(), mode); return true; } String smode = String(mode); if (sendCommandCheckResult("S10:"+smode,smode)) { - update_CurrClr(smode); + CurrClr.update(mode); return true; } return false; @@ -892,7 +896,7 @@ void SpaInterface::updateMeasures() { #pragma region R6 update_VARIValue(statusResponseRaw[R6 + 1]); LBRTValue.update(statusResponseRaw[R6 + 2].toInt()); - update_CurrClr(statusResponseRaw[R6 + 3]); + CurrClr.update(statusResponseRaw[R6 + 3].toInt()); ColorMode.update(statusResponseRaw[R6 + 4].toInt()); LSPDValue.update(statusResponseRaw[R6 + 5].toInt()); update_FiltHrs(statusResponseRaw[R6 + 6]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index c6f9841..2a71898 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -130,6 +130,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (1..5). bool setLSPDValue(int mode); + /// @brief Internal writer used by `CurrClr` RWProperty. + /// @details Sends `S10:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..31). + bool setCurrClr(int mode); /// @brief Set snooze day ({128,127,96,31} -> {"Off","Everyday","Weekends","Weekdays"};) /// @param mode @@ -331,6 +336,39 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid range 1..5. RWProperty LSPDValue{this, &SpaInterface::setLSPDValue, LSPDValue_Map}; + /// @brief Maps hue values in 15-degree buckets to spa color indices. + /// @details Reverse lookup is ambiguous and returns the first matching hue label. + static constexpr ROProperty::LabelValue CurrClr_Map[] = { + {"0", 0}, + {"15", 4}, + {"30", 4}, + {"45", 19}, + {"60", 13}, + {"75", 25}, + {"90", 25}, + {"105", 16}, + {"120", 10}, + {"135", 7}, + {"150", 2}, + {"165", 8}, + {"180", 5}, + {"195", 3}, + {"210", 6}, + {"225", 6}, + {"240", 21}, + {"255", 21}, + {"270", 21}, + {"285", 18}, + {"300", 18}, + {"315", 9}, + {"330", 9}, + {"345", 1}, + {"360", 1}, + }; + /// @brief Light color index. + /// @details Read/write. Valid range 0..31. + RWProperty CurrClr{this, &SpaInterface::setCurrClr, CurrClr_Map}; + /// @brief Sleep timer day mode values. /// @details Shared label/value map for both timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. static constexpr ROProperty::LabelValue SNZ_DAY_Map[] = { @@ -384,11 +422,6 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Set light colour (min 0, max 31) - /// @param mode - /// @return Returns True if succesful - bool setCurrClr(int mode); - /// @brief Set the operating mode for pump 1 /// @param mode 0 = off, 1 = on, 4 = auto (if supported) /// @return True if successful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 66c5ddb..baa654b 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -391,10 +391,6 @@ boolean SpaProperties::update_VARIValue(const String& s){ return updateIntProperty(VARIValue, s); } -boolean SpaProperties::update_CurrClr(const String& s){ - return updateIntProperty(CurrClr, s); -} - boolean SpaProperties::update_FiltHrs(const String& s){ return updateIntProperty(FiltSetHrs, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 7666081..41a44ab 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -260,10 +260,6 @@ Property HV_2; /// /// min 1, max 5 Property VARIValue; - /// @brief Light colour - /// - /// min 0, max 31 - Property CurrClr; /// @brief Filter run time (in hours) per block Property FiltSetHrs; /// @brief Filter block duration (hours) @@ -693,7 +689,6 @@ Property HV_2; #pragma endregion #pragma region R6 boolean update_VARIValue(const String&); - boolean update_CurrClr(const String&); boolean update_FiltHrs(const String&); boolean update_FiltBlockHrs(const String&); boolean update_L_24HOURS(const String&); @@ -1140,10 +1135,6 @@ Property HV_2; int getVARIValue() { return VARIValue.getValue(); } void setVARIValueCallback(void (*callback)(int)) { VARIValue.setCallback(callback); } - int getCurrClr() { return CurrClr.getValue(); } - void setCurrClrCallback(void (*callback)(int)) { CurrClr.setCallback(callback); } - const std::array colorMap = {0, 4, 4, 19, 13, 25, 25, 16, 10, 7, 2, 8, 5, 3, 6, 6, 21, 21, 21, 18, 18, 9, 9, 1, 1}; - int getFiltHrs() { return FiltSetHrs.getValue(); } void setFiltHrsCallback(void (*callback)(int)) { FiltSetHrs.setCallback(callback); } // This is used to generated the select diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index e709027..53bd49b 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -204,19 +204,14 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["lights"]["brightness"] = si.LBRTValue.get(); // 0 = white, if white, then set the hue and saturation to white so the light displays correctly in HA. - if (si.ColorMode.get() == 0) { - json["lights"]["color"]["h"] = 0; - json["lights"]["color"]["s"] = 0; - } else { - int hue = 4; - for (uint count = 0; count < sizeof(si.colorMap); count++){ - if (si.colorMap[count] == si.getCurrClr()) { - hue = count * 15; - } - } - json["lights"]["color"]["h"] = hue; - json["lights"]["color"]["s"] = 100; - } + if (si.ColorMode.get() == 0) { + json["lights"]["color"]["h"] = 0; + json["lights"]["color"]["s"] = 0; + } else { + int hue = atoi(si.CurrClr.getLabel("0")); + json["lights"]["color"]["h"] = hue; + json["lights"]["color"]["s"] = 100; + } json["lights"]["color_mode"] = "hs"; int jsonSize; diff --git a/src/main.cpp b/src/main.cpp index 15d7f24..aa09949 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -535,7 +535,15 @@ void setSpaProperty(String property, String p) { int pos = p.indexOf(','); if ( pos > 0) { int value = p.substring(0, pos).toInt(); - si.setCurrClr(si.colorMap[value/15]); + int hue = (value / 15) * 15; + if (hue < 0) hue = 0; + if (hue > 360) hue = 360; + String hueLabel = String(hue); + try { + si.CurrClr.setLabel(hueLabel.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set CurrClr: %s", ex.what()); + } } } else if (property == "lights_speed") { try { From d028ed9a91b197a22b135ec4fb097f942eab46ff Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 10:43:06 +1100 Subject: [PATCH 16/44] Migrate PumpStatuses + update gitignore --- .gitignore | 1 + lib/SpaInterface/SpaInterface.cpp | 72 ++++++++++++++-------- lib/SpaInterface/SpaInterface.h | 96 +++++++++++++++--------------- lib/SpaInterface/SpaProperties.cpp | 20 ------- lib/SpaInterface/SpaProperties.h | 39 ------------ lib/SpaUtils/SpaUtils.cpp | 19 +++--- src/main.cpp | 27 +++++++-- 7 files changed, 128 insertions(+), 146 deletions(-) diff --git a/.gitignore b/.gitignore index 8293886..ac60914 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .vscode/ipch .vscode/extensions.json .DS_Store +.vscode/extensions.json diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 8e5ca2e..11980d1 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -11,6 +11,13 @@ SpaInterface::SpaInterface() : port(SPA_SERIAL) { SpaInterface::~SpaInterface() {} +SpaInterface::PumpStatus SpaInterface::pumpStatuses[] = { + &SpaInterface::RB_TP_Pump1, + &SpaInterface::RB_TP_Pump2, + &SpaInterface::RB_TP_Pump3, + &SpaInterface::RB_TP_Pump4, + &SpaInterface::RB_TP_Pump5, +}; void SpaInterface::setSpaPollFrequency(int updateFrequency) { _updateFrequency = updateFrequency; @@ -75,70 +82,85 @@ bool SpaInterface::sendCommandCheckResult(String cmd, String expected){ } bool SpaInterface::setRB_TP_Pump1(int mode){ - debugD("setRB_TP_Pump1 - %i",mode); - if (mode == getRB_TP_Pump1()) { - debugD("No Pump1 change detected - current %i, new %i", getRB_TP_Pump1(), mode); + debugD("setRB_TP_Pump1 - %i", mode); + if (mode < 0 || mode > 4) { + throw std::out_of_range("RB_TP_Pump1 value out of range (0..4)"); + } + if (mode == RB_TP_Pump1.get()) { + debugD("No Pump1 change detected - current %i, new %i", RB_TP_Pump1.get(), mode); return true; } if (sendCommandCheckResult("S22:"+String(mode),"S22-OK")) { - update_RB_TP_Pump1(String(mode)); + RB_TP_Pump1.update(mode); return true; } return false; } bool SpaInterface::setRB_TP_Pump2(int mode){ - debugD("setRB_TP_Pump2 - %i",mode); - if (mode == getRB_TP_Pump2()) { - debugD("No Pump2 change detected - current %i, new %i", getRB_TP_Pump2(), mode); + debugD("setRB_TP_Pump2 - %i", mode); + if (mode < 0 || mode > 4) { + throw std::out_of_range("RB_TP_Pump2 value out of range (0..4)"); + } + if (mode == RB_TP_Pump2.get()) { + debugD("No Pump2 change detected - current %i, new %i", RB_TP_Pump2.get(), mode); return true; } if (sendCommandCheckResult("S23:"+String(mode),"S23-OK")) { - update_RB_TP_Pump2(String(mode)); + RB_TP_Pump2.update(mode); return true; } return false; } bool SpaInterface::setRB_TP_Pump3(int mode){ - debugD("setRB_TP_Pump3 - %i",mode); - if (mode == getRB_TP_Pump3()) { - debugD("No Pump3 change detected - current %i, new %i", getRB_TP_Pump3(), mode); + debugD("setRB_TP_Pump3 - %i", mode); + if (mode < 0 || mode > 4) { + throw std::out_of_range("RB_TP_Pump3 value out of range (0..4)"); + } + if (mode == RB_TP_Pump3.get()) { + debugD("No Pump3 change detected - current %i, new %i", RB_TP_Pump3.get(), mode); return true; } if (sendCommandCheckResult("S24:"+String(mode),"S24-OK")) { - update_RB_TP_Pump3(String(mode)); + RB_TP_Pump3.update(mode); return true; } return false; } bool SpaInterface::setRB_TP_Pump4(int mode){ - debugD("setRB_TP_Pump4 - %i",mode); - if (mode == getRB_TP_Pump4()) { - debugD("No Pump4 change detected - current %i, new %i", getRB_TP_Pump4(), mode); + debugD("setRB_TP_Pump4 - %i", mode); + if (mode < 0 || mode > 4) { + throw std::out_of_range("RB_TP_Pump4 value out of range (0..4)"); + } + if (mode == RB_TP_Pump4.get()) { + debugD("No Pump4 change detected - current %i, new %i", RB_TP_Pump4.get(), mode); return true; } if (sendCommandCheckResult("S25:"+String(mode),"S25-OK")) { - update_RB_TP_Pump4(String(mode)); + RB_TP_Pump4.update(mode); return true; } return false; } bool SpaInterface::setRB_TP_Pump5(int mode){ - debugD("setRB_TP_Pump5 - %i",mode); - if (mode == getRB_TP_Pump5()) { - debugD("No Pump5 change detected - current %i, new %i", getRB_TP_Pump5(), mode); + debugD("setRB_TP_Pump5 - %i", mode); + if (mode < 0 || mode > 4) { + throw std::out_of_range("RB_TP_Pump5 value out of range (0..4)"); + } + if (mode == RB_TP_Pump5.get()) { + debugD("No Pump5 change detected - current %i, new %i", RB_TP_Pump5.get(), mode); return true; } if (sendCommandCheckResult("S26:"+String(mode),"S26-OK")) { - update_RB_TP_Pump5(String(mode)); + RB_TP_Pump5.update(mode); return true; } return false; @@ -887,11 +909,11 @@ void SpaInterface::updateMeasures() { update_RB_TP_Light(statusResponseRaw[R5 + 14]); update_WTMP(statusResponseRaw[R5 + 15]); update_CleanCycle(statusResponseRaw[R5 + 16]); - update_RB_TP_Pump1(statusResponseRaw[R5 + 18]); - update_RB_TP_Pump2(statusResponseRaw[R5 + 19]); - update_RB_TP_Pump3(statusResponseRaw[R5 + 20]); - update_RB_TP_Pump4(statusResponseRaw[R5 + 21]); - update_RB_TP_Pump5(statusResponseRaw[R5 + 22]); + RB_TP_Pump1.update(statusResponseRaw[R5 + 18].toInt()); + RB_TP_Pump2.update(statusResponseRaw[R5 + 19].toInt()); + RB_TP_Pump3.update(statusResponseRaw[R5 + 20].toInt()); + RB_TP_Pump4.update(statusResponseRaw[R5 + 21].toInt()); + RB_TP_Pump5.update(statusResponseRaw[R5 + 22].toInt()); #pragma endregion #pragma region R6 update_VARIValue(statusResponseRaw[R6 + 1]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 2a71898..3f003ac 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -151,6 +151,32 @@ class SpaInterface : public SpaProperties { bool setL_2SNZ_BGN(int mode); bool setL_2SNZ_END(int mode); + /// @brief Internal writer used by `RB_TP_Pump1` RWProperty. + /// @details Sends `S22:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..4). + bool setRB_TP_Pump1(int mode); + /// @brief Internal writer used by `RB_TP_Pump2` RWProperty. + /// @details Sends `S23:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..4). + bool setRB_TP_Pump2(int mode); + /// @brief Internal writer used by `RB_TP_Pump3` RWProperty. + /// @details Sends `S24:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..4). + bool setRB_TP_Pump3(int mode); + /// @brief Internal writer used by `RB_TP_Pump4` RWProperty. + /// @details Sends `S25:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..4). + bool setRB_TP_Pump4(int mode); + /// @brief Internal writer used by `RB_TP_Pump5` RWProperty. + /// @details Sends `S26:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..4). + bool setRB_TP_Pump5(int mode); + public: /// @brief Init SpaInterface. SpaInterface(); @@ -404,6 +430,22 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). RWProperty L_2SNZ_END{this, &SpaInterface::setL_2SNZ_END}; + /// @brief Pump 1 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump1{this, &SpaInterface::setRB_TP_Pump1}; + /// @brief Pump 2 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump2{this, &SpaInterface::setRB_TP_Pump2}; + /// @brief Pump 3 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump3{this, &SpaInterface::setRB_TP_Pump3}; + /// @brief Pump 4 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump4{this, &SpaInterface::setRB_TP_Pump4}; + /// @brief Pump 5 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump5{this, &SpaInterface::setRB_TP_Pump5}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -422,31 +464,6 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Set the operating mode for pump 1 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful - bool setRB_TP_Pump1(int mode); - - /// @brief Set the operating mode for pump 2 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful - bool setRB_TP_Pump2(int mode); - - /// @brief Set the operating mode for pump 3 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful - bool setRB_TP_Pump3(int mode); - - /// @brief Set the operating mode for pump 4 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful - bool setRB_TP_Pump4(int mode); - - /// @brief Set the operating mode for pump 5 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful - bool setRB_TP_Pump5(int mode); - bool setRB_TP_Light(int mode); /// @brief Set aux element operating mode @@ -490,6 +507,12 @@ class SpaInterface : public SpaProperties { bool setFiltHrs(String duration); bool setLockMode(int mode); + + /// @brief Unified array of RWProperty pointers for each migrated pump, used for + /// both reading state and sending commands. Grows as pumps are migrated. + using PumpStatus = RWProperty SpaInterface::*; + static PumpStatus pumpStatuses[]; + static const int pumpStatusesCount = 5; }; @@ -505,28 +528,5 @@ static GetPumpStateInstallFunction pumpInstallStateFunctions[] = { &SpaInterface::getPump5InstallState }; -// Define the function pointer type for getPumpState functions -typedef int (SpaInterface::*GetPumpStateFunction)(); - -// Declare the array of function pointers for each pump's state as static -static GetPumpStateFunction pumpStateFunctions[] = { - &SpaInterface::getRB_TP_Pump1, - &SpaInterface::getRB_TP_Pump2, - &SpaInterface::getRB_TP_Pump3, - &SpaInterface::getRB_TP_Pump4, - &SpaInterface::getRB_TP_Pump5 -}; - -// Define the function pointer type for getPumpState functions -typedef bool (SpaInterface::*SetPumpFunction)(int); - -// Declare the array of function pointers for each pump's state as static -static SetPumpFunction setPumpFunctions[] = { - &SpaInterface::setRB_TP_Pump1, - &SpaInterface::setRB_TP_Pump2, - &SpaInterface::setRB_TP_Pump3, - &SpaInterface::setRB_TP_Pump4, - &SpaInterface::setRB_TP_Pump5 -}; #endif diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index baa654b..45722da 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -335,26 +335,6 @@ boolean SpaProperties::update_Vari_Mode(const String& s){ return updateIntProperty(Vari_Mode, s); } -boolean SpaProperties::update_RB_TP_Pump1(const String& s){ - return updateIntProperty(RB_TP_Pump1, s); -} - -boolean SpaProperties::update_RB_TP_Pump2(const String& s){ - return updateIntProperty(RB_TP_Pump2, s); -} - -boolean SpaProperties::update_RB_TP_Pump3(const String& s){ - return updateIntProperty(RB_TP_Pump3, s); -} - -boolean SpaProperties::update_RB_TP_Pump4(const String& s){ - return updateIntProperty(RB_TP_Pump4, s); -} - -boolean SpaProperties::update_RB_TP_Pump5(const String& s){ - return updateIntProperty(RB_TP_Pump5, s); -} - boolean SpaProperties::update_RB_TP_Blower(const String& s){ return updateIntProperty(RB_TP_Blower, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 41a44ab..1b04652 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -209,26 +209,6 @@ Property HV_2; // R5 // Unknown encoding - Attribute TouchPad2; // Unknown encoding - Attribute TouchPad1; - /// @brief Pump 1 state - /// - /// 0 = off, 1 = running, 4 = auto - Property RB_TP_Pump1; - /// @brief Pump 2 state - /// - /// 0 = off, 1 = running, 4 = auto - Property RB_TP_Pump2; - /// @brief Pump 3 state - /// - /// 0 = off, 1 = running, 4 = auto - Property RB_TP_Pump3; - /// @brief Pump 4 state - /// - /// 0 = off, 1 = running, 4 = auto - Property RB_TP_Pump4; - /// @brief Pump 5 state - /// - /// 0 = off, 1 = running, 4 = auto - Property RB_TP_Pump5; Property RB_TP_Blower; Property RB_TP_Light; /// @brief Auto enabled @@ -673,11 +653,6 @@ Property HV_2; // R5 // Unknown encoding - TouchPad2.update_Value(); // Unknown encoding - TouchPad1.update_Value(); - boolean update_RB_TP_Pump1(const String&); - boolean update_RB_TP_Pump2(const String&); - boolean update_RB_TP_Pump3(const String&); - boolean update_RB_TP_Pump4(const String&); - boolean update_RB_TP_Pump5(const String&); boolean update_RB_TP_Blower(const String&); boolean update_RB_TP_Light(const String&); boolean update_RB_TP_Auto(const String&); @@ -1089,20 +1064,6 @@ Property HV_2; int getVari_Mode() { return Vari_Mode.getValue(); } void setVari_ModeCallback(void (*callback)(int)) { Vari_Mode.setCallback(callback); } - int getRB_TP_Pump1() { return RB_TP_Pump1.getValue(); } - void setRB_TP_Pump1Callback(void (*callback)(int)) { RB_TP_Pump1.setCallback(callback); } - - int getRB_TP_Pump2() { return RB_TP_Pump2.getValue(); } - void setRB_TP_Pump2Callback(void (*callback)(int)) { RB_TP_Pump2.setCallback(callback); } - - int getRB_TP_Pump3() { return RB_TP_Pump3.getValue(); } - void setRB_TP_Pump3Callback(void (*callback)(int)) { RB_TP_Pump3.setCallback(callback); } - - int getRB_TP_Pump4() { return RB_TP_Pump4.getValue(); } - void setRB_TP_Pump4Callback(void (*callback)(int)) { RB_TP_Pump4.setCallback(callback); } - - int getRB_TP_Pump5() { return RB_TP_Pump5.getValue(); } - void setRB_TP_Pump5Callback(void (*callback)(int)) { RB_TP_Pump5.setCallback(callback); } const std::array autoPumpOptions = {"Manual", "Auto"}; int getRB_TP_Blower() { return RB_TP_Blower.getValue(); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 53bd49b..4be4a0e 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -78,7 +78,8 @@ bool getPumpModesJson(SpaInterface &si, int pumpNumber, JsonObject pumps) { } } - int pumpState = (si.*(pumpStateFunctions[pumpNumber - 1]))(); + int pumpState = (pumpNumber - 1 < SpaInterface::pumpStatusesCount) + ? (si.*(SpaInterface::pumpStatuses[pumpNumber - 1])).get() : 0; if (pumpInstallState.endsWith("4") && possibleStates.length() > 1) { if (pumpState == 4) pumps[pumpKey]["mode"] = "Auto"; else pumps[pumpKey]["mode"] = "Manual"; @@ -204,14 +205,14 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["lights"]["brightness"] = si.LBRTValue.get(); // 0 = white, if white, then set the hue and saturation to white so the light displays correctly in HA. - if (si.ColorMode.get() == 0) { - json["lights"]["color"]["h"] = 0; - json["lights"]["color"]["s"] = 0; - } else { - int hue = atoi(si.CurrClr.getLabel("0")); - json["lights"]["color"]["h"] = hue; - json["lights"]["color"]["s"] = 100; - } + if (si.ColorMode.get() == 0) { + json["lights"]["color"]["h"] = 0; + json["lights"]["color"]["s"] = 0; + } else { + int hue = atoi(si.CurrClr.getLabel("0")); + json["lights"]["color"]["h"] = hue; + json["lights"]["color"]["s"] = 100; + } json["lights"]["color_mode"] = "hs"; int jsonSize; diff --git a/src/main.cpp b/src/main.cpp index aa09949..2e73680 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -489,16 +489,33 @@ void setSpaProperty(String property, String p) { if (p == "1") p = "0"; else if (p == "2") p = "3"; else if (p == "3") p = "2"; - (si.*(setPumpFunctions[pumpNum-1]))(p.toInt()); + if (pumpNum - 1 < SpaInterface::pumpStatusesCount) + try { + (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(p.toInt()); + } catch (const std::exception& ex) { + debugE("Failed to set pump%d speed: %s", pumpNum, ex.what()); + } } else if (property.startsWith("pump") && property.endsWith("_mode")) { int pumpNum = property.charAt(4) - '0'; - if (p == "Auto") (si.*(setPumpFunctions[pumpNum-1]))(4); - else (si.*(setPumpFunctions[pumpNum-1]))(3); // When we change mode to manual set speed to low, as this matches the auto display speed + if (pumpNum - 1 < SpaInterface::pumpStatusesCount) { + try { + if (p == "Auto") (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(4); + else (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(3); // When we change mode to manual set speed to low, as this matches the auto display speed + } catch (const std::exception& ex) { + debugE("Failed to set pump%d mode: %s", pumpNum, ex.what()); + } + } } else if (property.startsWith("pump") && property.endsWith("_state")) { int pumpNum = property.charAt(4) - '0'; String pumpState = (si.*(pumpInstallStateFunctions[pumpNum-1]))(); - if (getPumpSpeedType(pumpState) == "2") (si.*(setPumpFunctions[pumpNum-1]))(p=="OFF"?0:2); // When we turn on the pump use speed high - else (si.*(setPumpFunctions[pumpNum-1]))(p=="OFF"?0:1); + if (pumpNum - 1 < SpaInterface::pumpStatusesCount) { + try { + if (getPumpSpeedType(pumpState) == "2") (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(p=="OFF"?0:2); // When we turn on the pump use speed high + else (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(p=="OFF"?0:1); + } catch (const std::exception& ex) { + debugE("Failed to set pump%d state: %s", pumpNum, ex.what()); + } + } } else if (property == "heatpump_auxheat") { si.setHELE(p=="OFF"?0:1); } else if (property == "status_datetime") { From 1d34cc90791f2815f5feb6f7a2e7cd89df9e4d99 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 10:54:33 +1100 Subject: [PATCH 17/44] Migrate HELE --- lib/SpaInterface/SpaInterface.cpp | 13 +++++++------ lib/SpaInterface/SpaInterface.h | 11 +++++++---- lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 8 -------- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 6 +++++- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 11980d1..43e9f7f 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -180,15 +180,16 @@ bool SpaInterface::setRB_TP_Light(int mode){ return false; } -bool SpaInterface::setHELE(int mode){ +bool SpaInterface::setHELE(bool mode){ debugD("setHELE - %i", mode); - if (mode == getHELE()) { - debugD("No HELE change detected - current %i, new %i", getHELE(), mode); + if (mode == HELE.get()) { + debugD("No HELE change detected - current %i, new %i", HELE.get(), mode); return true; } - if (sendCommandCheckResult("W98:"+String(mode),String(mode))) { - update_HELE(String(mode)); + int v = mode ? 1 : 0; + if (sendCommandCheckResult("W98:"+String(v),String(v))) { + HELE.update(mode); return true; } return false; @@ -974,7 +975,7 @@ void SpaInterface::updateMeasures() { update_VMAX(statusResponseRaw[R7 + 22]); update_AHYS(statusResponseRaw[R7 + 23]); update_HUSE(statusResponseRaw[R7 + 24]); - update_HELE(statusResponseRaw[R7 + 25]); + HELE.update(statusResponseRaw[R7 + 25] == "1"); HPMP.update(statusResponseRaw[R7 + 26].toInt()); update_PMIN(statusResponseRaw[R7 + 27]); update_PFLT(statusResponseRaw[R7 + 28]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 3f003ac..9bdf905 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -176,6 +176,10 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (0..4). bool setRB_TP_Pump5(int mode); + /// @brief Internal writer used by `HELE` RWProperty. + /// @details Sends `W98:` to the controller and only updates + /// the cached property value when the command succeeds. + bool setHELE(bool mode); public: /// @brief Init SpaInterface. @@ -445,6 +449,9 @@ class SpaInterface : public SpaProperties { /// @brief Pump 5 operating state. /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). RWProperty RB_TP_Pump5{this, &SpaInterface::setRB_TP_Pump5}; + /// @brief Aux element (booster) state. + /// @details Read/write. false=Off, true=On. + RWProperty HELE{this, &SpaInterface::setHELE}; /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -466,10 +473,6 @@ class SpaInterface : public SpaProperties { bool setRB_TP_Light(int mode); - /// @brief Set aux element operating mode - /// @param mode 0 = off, 1 = on - /// @return True if successful - bool setHELE(int mode); /// @brief Sets the day of week on the spa /// @param d Day of week (0 = Monday - 6 = Sunday) diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 45722da..b5fc6cb 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -512,10 +512,6 @@ boolean SpaProperties::update_HUSE(const String& s){ return updateBool01Property(HUSE, s); } -boolean SpaProperties::update_HELE(const String& s) { - return updateBool01Property(HELE, s); -} - boolean SpaProperties::update_PMIN(const String& s){ return updateIntProperty(PMIN, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 1b04652..f6162c5 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -358,11 +358,6 @@ Property HV_2; /// /// 1 = Off Property HUSE; - /// @brief Heat pump active whilst spa is in use - /// - /// If false then when spa is in use then heat pump will not run to reduce noise levels - /// See SV-Series-OEM-Install-Manual.pdf page 19. - Property HELE; /// @brief Varible pump minimum speed setting /// /// Min 20%, Max 100% @@ -703,7 +698,6 @@ Property HV_2; boolean update_VMAX(const String&); boolean update_AHYS(const String&); boolean update_HUSE(const String&); - boolean update_HELE(const String&); boolean update_PMIN(const String&); boolean update_PFLT(const String&); boolean update_PHTR(const String&); @@ -1220,8 +1214,6 @@ Property HV_2; bool getHUSE() { return HUSE.getValue(); } void setHUSECallback(void (*callback)(bool)) { HUSE.setCallback(callback); } - bool getHELE() { return HELE.getValue(); } - void setHELECallback(void (*callback)(bool)) { HELE.setCallback(callback); } int getPMIN() { return PMIN.getValue(); } void setPMINCallback(void (*callback)(int)) { PMIN.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 4be4a0e..83af2cd 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -158,7 +158,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["eSpa"]["update"]["installed_version"] = xstr(BUILD_INFO); json["heatpump"]["mode"] = si.HPMP.getLabel(); - json["heatpump"]["auxheat"] = si.getHELE()==0? "OFF" : "ON"; + json["heatpump"]["auxheat"] = si.HELE ? "ON" : "OFF"; json["filtration"]["blockDuration"] = si.getFiltBlockHrs(); json["filtration"]["hours"] = si.getFiltHrs(); diff --git a/src/main.cpp b/src/main.cpp index 2e73680..863a727 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -517,7 +517,11 @@ void setSpaProperty(String property, String p) { } } } else if (property == "heatpump_auxheat") { - si.setHELE(p=="OFF"?0:1); + try { + si.HELE.set(p != "OFF"); + } catch (const std::exception& ex) { + debugE("Failed to set HELE: %s", ex.what()); + } } else if (property == "status_datetime") { tmElements_t tm; tm.Year=CalendarYrToTm(p.substring(0,4).toInt()); From bdd994d5d1db8d433b07bfefca5bffb9de6d25bd Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 11:01:49 +1100 Subject: [PATCH 18/44] Migrate SpaDayOfWeek --- lib/SpaInterface/SpaInterface.cpp | 11 +++++++---- lib/SpaInterface/SpaInterface.h | 24 +++++++++++++++++++----- lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 10 ---------- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 9 ++++----- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 43e9f7f..b788289 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -460,14 +460,17 @@ bool SpaInterface::setCurrClr(int mode){ bool SpaInterface::setSpaDayOfWeek(int d){ debugD("setSpaDayOfWeek - %i", d); - if (d == getSpaDayOfWeek()) { - debugD("No SpaDayOfWeek change detected - current %i, new %i", getSpaDayOfWeek(), d); + if (d < 0 || d > 6) { + throw std::out_of_range("SpaDayOfWeek value out of range (0..6)"); + } + if (d == SpaDayOfWeek.get()) { + debugD("No SpaDayOfWeek change detected - current %i, new %i", SpaDayOfWeek.get(), d); return true; } String sd = String(d); if (sendCommandCheckResult("S06:"+sd,sd)) { - update_SpaDayOfWeek(sd); + SpaDayOfWeek.update(d); return true; } return false; @@ -824,7 +827,7 @@ void SpaInterface::updateMeasures() { update_MainsVoltage(statusResponseRaw[R2+2]); update_CaseTemperature(statusResponseRaw[R2+3]); update_PortCurrent(statusResponseRaw[R2+4]); - update_SpaDayOfWeek(statusResponseRaw[R2+5]); + SpaDayOfWeek.update(statusResponseRaw[R2+5].toInt()); update_SpaTime(statusResponseRaw[R2+11], statusResponseRaw[R2+10], statusResponseRaw[R2+9], statusResponseRaw[R2+6], statusResponseRaw[R2+7], statusResponseRaw[R2+8]); update_HeaterTemperature(statusResponseRaw[R2+12]); update_PoolTemperature(statusResponseRaw[R2+13]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 9bdf905..37d744f 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -180,6 +180,11 @@ class SpaInterface : public SpaProperties { /// @details Sends `W98:` to the controller and only updates /// the cached property value when the command succeeds. bool setHELE(bool mode); + /// @brief Internal writer used by `SpaDayOfWeek` RWProperty. + /// @details Sends `S06:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..6). + bool setSpaDayOfWeek(int d); public: /// @brief Init SpaInterface. @@ -453,6 +458,20 @@ class SpaInterface : public SpaProperties { /// @details Read/write. false=Off, true=On. RWProperty HELE{this, &SpaInterface::setHELE}; + /// @brief Day of week label map. 0=Monday .. 6=Sunday. + static constexpr ROProperty::LabelValue SpaDayOfWeek_Map[] = { + {"Monday", 0}, + {"Tuesday", 1}, + {"Wednesday", 2}, + {"Thursday", 3}, + {"Friday", 4}, + {"Saturday", 5}, + {"Sunday", 6}, + }; + /// @brief Current day of week on Spa RTC. + /// @details Read/write. 0=Monday .. 6=Sunday. + RWProperty SpaDayOfWeek{this, &SpaInterface::setSpaDayOfWeek, SpaDayOfWeek_Map}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -474,11 +493,6 @@ class SpaInterface : public SpaProperties { bool setRB_TP_Light(int mode); - /// @brief Sets the day of week on the spa - /// @param d Day of week (0 = Monday - 6 = Sunday) - /// @return True if successful - bool setSpaDayOfWeek(int d); - /// @brief Sets the clock on the spa /// @param t Time /// @return True if successful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index b5fc6cb..39a446b 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -40,10 +40,6 @@ static boolean updateTriStateProperty(Property& prop, const String& s) { return true; } -boolean SpaProperties::update_SpaDayOfWeek(const String& s){ - return updateIntProperty(SpaDayOfWeek, s); -} - boolean SpaProperties::update_SpaTime(const String& year, const String& month, const String& day, const String& hour, const String& minute, const String& second){ tmElements_t tm; diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index f6162c5..8315f81 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -46,9 +46,6 @@ class SpaProperties /// @brief 12v port current (mA) Property PortCurrent; - /// @brief Current day of week on Spa RTC - /// 0 = Monday, 1 = Tuesday, ..., 6 = Sunday - Property SpaDayOfWeek; /// @brief Current time on Spa RTC Property SpaTime; /// @brief Heater temperature ('C) @@ -566,7 +563,6 @@ Property HV_2; protected: #pragma region R2 - boolean update_SpaDayOfWeek(const String&); boolean update_SpaTime(const String& year, const String& month, const String& day, const String& hour, const String& minute, const String& second); boolean update_MainsVoltage(const String&); boolean update_CaseTemperature(const String&); @@ -815,12 +811,6 @@ Property HV_2; int getPortCurrent() { return PortCurrent.getValue(); } void setPortCurrentCallback(void (*callback)(int)) { PortCurrent.setCallback(callback); } - /// @brief Gets the day of week from the spa - /// @return - int getSpaDayOfWeek() { return SpaDayOfWeek.getValue(); } - void SpaDayOfWeekCallback(void (*callback)(int)) { SpaDayOfWeek.setCallback(callback); } - const std::array spaDayOfWeekStrings = {"Monday","Tuesday", "Wednesday","Thursday","Friday","Saturday","Sunday"}; - /// @brief Gets the current time from the spa clock /// @return time_t getSpaTime() { return SpaTime.getValue(); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 83af2cd..a721f02 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -186,7 +186,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String if (second(si.getSpaTime())<10) s = "0"+s; json["status"]["datetime"]=y+"-"+m+"-"+d+" "+h+":"+min+":"+s; - json["status"]["dayOfWeek"]=si.spaDayOfWeekStrings[si.getSpaDayOfWeek()]; + json["status"]["dayOfWeek"]=si.SpaDayOfWeek.getLabel(); json["blower"]["state"] = si.getOutlet_Blower()==2? "OFF" : "ON"; json["blower"]["mode"] = si.getOutlet_Blower()==1? "Ramp" : "Variable"; diff --git a/src/main.cpp b/src/main.cpp index 863a727..c6af4a4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -532,11 +532,10 @@ void setSpaProperty(String property, String p) { tm.Second=p.substring(17).toInt(); si.setSpaTime(makeTime(tm)); } else if (property == "status_dayOfWeek") { - for (int i = 0; i < si.spaDayOfWeekStrings.size(); i++) { - if (si.spaDayOfWeekStrings[i] == p) { - si.setSpaDayOfWeek(i); - break; - } + try { + si.SpaDayOfWeek.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set SpaDayOfWeek '%s': %s", p.c_str(), ex.what()); } } else if (property == "lights_state") { si.setRB_TP_Light(p=="ON"?1:0); From c687dd8c16e0edd123aa844c32cb21c9dafc812d Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 11:19:20 +1100 Subject: [PATCH 19/44] migrate Blower --- lib/SpaInterface/SpaInterface.cpp | 13 ++++++++----- lib/SpaInterface/SpaInterface.h | 20 +++++++++++++++----- lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 8 -------- lib/SpaUtils/SpaUtils.cpp | 6 +++--- src/main.cpp | 28 +++++++++++++++++++++++----- 6 files changed, 49 insertions(+), 30 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index b788289..cde5928 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -513,15 +513,18 @@ bool SpaInterface::setSpaTime(time_t t){ } bool SpaInterface::setOutlet_Blower(int mode){ - debugD("setOuput-Blower - %i", mode); - if (mode == getOutlet_Blower()) { - debugD("No Outlet_Blower change detected - current %i, new %i", getOutlet_Blower(), mode); + debugD("setOutlet_Blower - %i", mode); + if (mode < 0 || mode > 2) { + throw std::out_of_range("Outlet_Blower value out of range (0..2)"); + } + if (mode == Outlet_Blower.get()) { + debugD("No Outlet_Blower change detected - current %i, new %i", Outlet_Blower.get(), mode); return true; } String smode = String(mode); if (sendCommandCheckResult("S28:"+smode,"S28-OK")) { - update_Outlet_Blower(smode); + Outlet_Blower.update(mode); return true; } return false; @@ -1032,7 +1035,7 @@ void SpaInterface::updateMeasures() { //Outlet_Pump2.updateValue(statusResponseRaw[]); //Outlet_Pump4.updateValue(statusResponseRaw[]); //Outlet_Pump5.updateValue(statusResponseRaw[]); - update_Outlet_Blower(statusResponseRaw[RC + 10]); + Outlet_Blower.update(statusResponseRaw[RC + 10].toInt()); #pragma endregion #pragma region RE update_HP_Present(statusResponseRaw[RE + 1]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 37d744f..82a2ab1 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -185,6 +185,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (0..6). bool setSpaDayOfWeek(int d); + /// @brief Internal writer used by `Outlet_Blower` RWProperty. + /// @details Sends `S28:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..2). + bool setOutlet_Blower(int mode); public: /// @brief Init SpaInterface. @@ -472,6 +477,16 @@ class SpaInterface : public SpaProperties { /// @details Read/write. 0=Monday .. 6=Sunday. RWProperty SpaDayOfWeek{this, &SpaInterface::setSpaDayOfWeek, SpaDayOfWeek_Map}; + /// @brief Blower mode label map. + static constexpr ROProperty::LabelValue Outlet_Blower_Map[] = { + {"Variable", 0}, + {"Ramp", 1}, + {"Off", 2}, + }; + /// @brief Air blower operating mode. + /// @details Read/write. 0=Variable, 1=Ramp, 2=Off. + RWProperty Outlet_Blower{this, &SpaInterface::setOutlet_Blower, Outlet_Blower_Map}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -498,11 +513,6 @@ class SpaInterface : public SpaProperties { /// @return True if successful bool setSpaTime(time_t t); - /// @brief Controls the air blower - /// @param mode 0 = Varible, 1 = Ramp, 2 = Off - /// @return True if successful - bool setOutlet_Blower(int mode); - /// @brief Set the speed of the air blower /// @param mode 1 = low, 5 = high /// @return True if successful diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 39a446b..772b0f7 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -656,10 +656,6 @@ boolean SpaProperties::update_F3_VE(const String& s) { return updateBool01Property(F3_VE, s); } -boolean SpaProperties::update_Outlet_Blower(const String& s){ - return updateIntProperty(Outlet_Blower, s); -} - boolean SpaProperties::update_HP_Present(const String& s){ return updateIntProperty(HP_Present, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 8315f81..7b68d9f 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -458,10 +458,6 @@ Property HV_2; // Attribute Outlet_Pump2; // Attribute Outlet_Pump4; // Attribute Outlet_Pump5; - /// @brief Blower status - /// - /// 0 = variable mode, 1 = ramp mode, 2 = off - Property Outlet_Blower; #pragma endregion #pragma region RE // RE @@ -746,7 +742,6 @@ Property HV_2; // Outlet_Pump2.update_Value(String); // Outlet_Pump4.update_Value(String); // Outlet_Pump5.update_Value(String); - boolean update_Outlet_Blower(const String&); #pragma endregion #pragma region RE boolean update_HP_Present(const String&); @@ -1052,7 +1047,6 @@ Property HV_2; int getRB_TP_Blower() { return RB_TP_Blower.getValue(); } void setRB_TP_BlowerCallback(void (*callback)(int)) { RB_TP_Blower.setCallback(callback); } - const std::array blowerStrings = {"Variable", "Ramp"}; int getRB_TP_Light() { return RB_TP_Light.getValue(); } void setRB_TP_LightCallback(void (*callback)(int)) { RB_TP_Light.setCallback(callback); } @@ -1352,8 +1346,6 @@ Property HV_2; int getF3_ST() { return F3_ST.getValue(); } void setF3_STCallback(void (*callback)(int)) { F3_ST.setCallback(callback); } - int getOutlet_Blower() { return Outlet_Blower.getValue(); } - void setOutlet_BlowerCallback(void (*callback)(int)) { Outlet_Blower.setCallback(callback); } int getHP_Present() { return HP_Present.getValue(); } void setHP_PresentCallback(void (*callback)(int)) { HP_Present.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index a721f02..edcaf51 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -188,9 +188,9 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["status"]["datetime"]=y+"-"+m+"-"+d+" "+h+":"+min+":"+s; json["status"]["dayOfWeek"]=si.SpaDayOfWeek.getLabel(); - json["blower"]["state"] = si.getOutlet_Blower()==2? "OFF" : "ON"; - json["blower"]["mode"] = si.getOutlet_Blower()==1? "Ramp" : "Variable"; - json["blower"]["speed"] = si.getOutlet_Blower() ==2? "0" : String(si.getVARIValue()); + json["blower"]["state"] = si.Outlet_Blower==2? "OFF" : "ON"; + json["blower"]["mode"] = si.Outlet_Blower.getLabel(); + json["blower"]["speed"] = si.Outlet_Blower==2? "0" : String(si.getVARIValue()); json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); json["sleepTimers"]["timer2"]["state"] = si.L_2SNZ_DAY.getLabel(); diff --git a/src/main.cpp b/src/main.cpp index c6af4a4..433668c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -408,7 +408,13 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "blower"; ADConf.deviceClass = ""; ADConf.entityCategory = ""; - generateFanAdJSON(output, ADConf, spa, discoveryTopic, 1, 5, si.blowerStrings.data(), si.blowerStrings.size()); + // blowerModes bridges the type mismatch between Outlet_Blower_Map (ROProperty::LabelValue[], + // which includes "Off") and generateFanAdJSON which expects const String* of preset mode names only. + // "Off" is a state rather than a preset mode so is intentionally excluded here. + // TODO: future improvement — update generateFanAdJSON to accept the label map directly, skipping + // entries that represent the off state, to eliminate this bridging array. + static const String blowerModes[] = {"Variable", "Ramp"}; + generateFanAdJSON(output, ADConf, spa, discoveryTopic, 1, 5, blowerModes, 2); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Spa Mode"; @@ -572,12 +578,24 @@ void setSpaProperty(String property, String p) { debugE("Failed to set LSPDValue: %s", ex.what()); } } else if (property == "blower_state") { - si.setOutlet_Blower(p=="OFF"?2:0); + try { + si.Outlet_Blower.set(p=="OFF"?2:0); + } catch (const std::exception& ex) { + debugE("Failed to set blower state: %s", ex.what()); + } } else if (property == "blower_speed") { - if (p=="0") si.setOutlet_Blower(2); - else si.setVARIValue(p.toInt()); + try { + if (p=="0") si.Outlet_Blower.set(2); + else si.setVARIValue(p.toInt()); + } catch (const std::exception& ex) { + debugE("Failed to set blower speed: %s", ex.what()); + } } else if (property == "blower_mode") { - si.setOutlet_Blower(p=="Variable"?0:1); + try { + si.Outlet_Blower.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set blower mode from label '%s': %s", p.c_str(), ex.what()); + } } else if (property == "sleepTimers_1_state") { try { si.L_1SNZ_DAY.setLabel(p.c_str()); From 6527e713babf15f495bff64fd79b744b52b57c38 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 11:30:29 +1100 Subject: [PATCH 20/44] Migrate Mode --- lib/SpaInterface/SpaInterface.cpp | 23 +++++++------------ lib/SpaInterface/SpaInterface.h | 36 +++++++++++++++++++++++++----- lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 16 ------------- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 6 ++++- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index cde5928..79becf5 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -549,29 +549,22 @@ bool SpaInterface::setVARIValue(int mode){ bool SpaInterface::setMode(int mode){ debugD("setMode - %i", mode); - if (mode == getModeIndex(getMode())) { - debugD("No Mode change detected - current %i, new %i", getModeIndex(getMode()), mode); + if (mode < 0 || mode > 3) { + throw std::out_of_range("Mode value out of range (0..3)"); + } + if (mode == Mode.get()) { + debugD("No Mode change detected - current %i, new %i", Mode.get(), mode); return true; } - + String smode = String(mode); if (sendCommandCheckResult("W66:"+smode,smode)) { - update_Mode(spaModeStrings[mode]); + Mode.update(mode); return true; } return false; } -bool SpaInterface::setMode(String mode){ - debugD("setMode - %s", mode.c_str()); - for (uint x=0; x` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..3). + bool setMode(int mode); public: /// @brief Init SpaInterface. @@ -251,6 +256,20 @@ class SpaInterface : public SpaProperties { _value = newValue; _hasValue = true; } + // Reverse-lookup by label string; throws std::invalid_argument if the label + // is not found in the map or if no map is configured. + void updateFromLabel(const char* label) { + if (!label || !this->_map || this->_mapSize == 0) { + throw std::invalid_argument("updateFromLabel: no label map configured"); + } + for (size_t i = 0; i < this->_mapSize; i++) { + if (strcmp(this->_map[i].label, label) == 0) { + update(this->_map[i].value); + return; + } + } + throw std::invalid_argument(String("updateFromLabel: unknown label '") + label + "'"); + } friend class SpaInterface; }; @@ -487,6 +506,17 @@ class SpaInterface : public SpaProperties { /// @details Read/write. 0=Variable, 1=Ramp, 2=Off. RWProperty Outlet_Blower{this, &SpaInterface::setOutlet_Blower, Outlet_Blower_Map}; + /// @brief Spa operating mode label map. + static constexpr ROProperty::LabelValue Mode_Map[] = { + {"NORM", 0}, + {"ECON", 1}, + {"AWAY", 2}, + {"WEEK", 3}, + }; + /// @brief Spa operating mode. + /// @details Read/write. 0=NORM, 1=ECON, 2=AWAY, 3=WEEK. + RWProperty Mode{this, &SpaInterface::setMode, Mode_Map}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -518,12 +548,6 @@ class SpaInterface : public SpaProperties { /// @return True if successful bool setVARIValue(int mode); - /// @brief Set Spa mode (0 --> 4, {"NORM","ECON","AWAY","WEEK"};) - /// @param mode - /// @return Returns True if succesful - bool setMode(int mode); - bool setMode(String mode); - /// @brief Set filtration block duration (1,2,3,4,6,8,12,24 hours) /// @param duration /// @return diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 772b0f7..a6bc1fc 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -231,10 +231,6 @@ boolean SpaProperties::update_HCON(const String& s){ return updateIntProperty(HCON, s); } -boolean SpaProperties::update_Mode(const String& s){ - return updateStringProperty(Mode, s); -} - boolean SpaProperties::update_Ser1_Timer(const String& s){ return updateIntProperty(Ser1_Timer, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 7b68d9f..fd7317d 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -152,10 +152,6 @@ Property HV_2; #pragma endregion #pragma region R4 // R4 - /// @brief Operation mode - /// - /// One of NORM, ECON, AWAY, WEEK - Property Mode; /// @brief Service Timer 1 (wks) 0 = off Property Ser1_Timer; /// @brief Service Timer 2 (wks) 0 = off @@ -610,7 +606,6 @@ Property HV_2; // boolean update_HV_2(const String&); #pragma endregion #pragma region R4 - boolean update_Mode(const String&); boolean update_Ser1_Timer(const String&); boolean update_Ser2_Timer(const String&); boolean update_Ser3_Timer(const String&); @@ -941,17 +936,6 @@ Property HV_2; int getHCON() { return HCON.getValue(); } void setHCONCallback(void (*callback)(int)) { HCON.setCallback(callback); } - String getMode() { return Mode.getValue(); } - int getModeIndex(String mode) { - for (size_t i = 0; i < spaModeStrings.size(); i++) - { - if (spaModeStrings[i] == mode) - return i; - } - return -1; - } - void setModeCallback(void (*callback)(String)) { Mode.setCallback(callback); } - const std::array spaModeStrings = {"NORM","ECON", "AWAY","WEEK"}; int getSer1_Timer() { return Ser1_Timer.getValue(); } void setSer1_TimerCallback(void (*callback)(int)) { Ser1_Timer.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index edcaf51..10740f7 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -145,7 +145,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["status"]["heatingActive"] = si.getRB_TP_Heater()? "ON": "OFF"; json["status"]["ozoneActive"] = si.getRB_TP_Ozone()? "ON": "OFF"; json["status"]["state"] = si.getStatus(); - json["status"]["spaMode"] = si.getMode(); + json["status"]["spaMode"] = si.Mode.getLabel(); json["status"]["controller"] = si.getModel(); String firmware = si.getSVER().substring(3); firmware.replace(' ', '.'); diff --git a/src/main.cpp b/src/main.cpp index 433668c..971fba9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -633,7 +633,11 @@ void setSpaProperty(String property, String p) { debugE("Failed to set L_2SNZ_END: %s", ex.what()); } } else if (property == "status_spaMode") { - si.setMode(p); + try { + si.Mode.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set Mode from label '%s': %s", p.c_str(), ex.what()); + } } else if (property == "filtration_blockDuration") { si.setFiltBlockHrs(p); } else if (property == "filtration_hours") { From eb96f77b3f4b550e1d48669c0dbf7e77e7524807 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 11:36:04 +1100 Subject: [PATCH 21/44] Migrate VARIValue --- lib/SpaInterface/SpaInterface.cpp | 19 ++++++++++--------- lib/SpaInterface/SpaInterface.h | 14 +++++++++----- lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 10 +--------- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 2 +- 6 files changed, 22 insertions(+), 29 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 79becf5..f03a423 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -532,17 +532,18 @@ bool SpaInterface::setOutlet_Blower(int mode){ bool SpaInterface::setVARIValue(int mode){ debugD("setVARIValue - %i", mode); - if (mode == getVARIValue()) { - debugD("No VARIValue change detected - current %i, new %i", getVARIValue(), mode); + if (mode < 1 || mode > 5) { + throw std::out_of_range("VARIValue out of range (1..5)"); + } + if (mode == VARIValue.get()) { + debugD("No VARIValue change detected - current %i, new %i", VARIValue.get(), mode); return true; } - if (mode > 0 && mode < 6) { - String smode = String(mode); - if (sendCommandCheckResult("S13:"+smode,smode+" S13")) { - update_VARIValue(smode); - return true; - } + String smode = String(mode); + if (sendCommandCheckResult("S13:"+smode,smode+" S13")) { + VARIValue.update(mode); + return true; } return false; } @@ -916,7 +917,7 @@ void SpaInterface::updateMeasures() { RB_TP_Pump5.update(statusResponseRaw[R5 + 22].toInt()); #pragma endregion #pragma region R6 - update_VARIValue(statusResponseRaw[R6 + 1]); + VARIValue.update(statusResponseRaw[R6 + 1].toInt()); LBRTValue.update(statusResponseRaw[R6 + 2].toInt()); CurrClr.update(statusResponseRaw[R6 + 3].toInt()); ColorMode.update(statusResponseRaw[R6 + 4].toInt()); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 7f6315a..76eec98 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -195,6 +195,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (0..3). bool setMode(int mode); + /// @brief Internal writer used by `VARIValue` RWProperty. + /// @details Sends `S13:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (1..5). + bool setVARIValue(int mode); public: /// @brief Init SpaInterface. @@ -517,6 +522,10 @@ class SpaInterface : public SpaProperties { /// @details Read/write. 0=NORM, 1=ECON, 2=AWAY, 3=WEEK. RWProperty Mode{this, &SpaInterface::setMode, Mode_Map}; + /// @brief Variable pump/blower speed. + /// @details Read/write. Valid range 1..5. + RWProperty VARIValue{this, &SpaInterface::setVARIValue}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -543,11 +552,6 @@ class SpaInterface : public SpaProperties { /// @return True if successful bool setSpaTime(time_t t); - /// @brief Set the speed of the air blower - /// @param mode 1 = low, 5 = high - /// @return True if successful - bool setVARIValue(int mode); - /// @brief Set filtration block duration (1,2,3,4,6,8,12,24 hours) /// @param duration /// @return diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index a6bc1fc..e6e326e 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -359,10 +359,6 @@ boolean SpaProperties::update_CleanCycle(const String& s) { return updateBool01Property(CleanCycle, s); } -boolean SpaProperties::update_VARIValue(const String& s){ - return updateIntProperty(VARIValue, s); -} - boolean SpaProperties::update_FiltHrs(const String& s){ return updateIntProperty(FiltSetHrs, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index fd7317d..ed67a6a 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -229,11 +229,7 @@ Property HV_2; #pragma endregion #pragma region R6 // R6 - /// @brief Blower variable speed - /// - /// min 1, max 5 - Property VARIValue; - /// @brief Filter run time (in hours) per block +/// @brief Filter run time (in hours) per block Property FiltSetHrs; /// @brief Filter block duration (hours) Property FiltBlockHrs; @@ -645,7 +641,6 @@ Property HV_2; boolean update_CleanCycle(const String&); #pragma endregion #pragma region R6 - boolean update_VARIValue(const String&); boolean update_FiltHrs(const String&); boolean update_FiltBlockHrs(const String&); boolean update_L_24HOURS(const String&); @@ -1055,9 +1050,6 @@ Property HV_2; bool getCleanCycle() { return CleanCycle.getValue(); } void setCleanCycleCallback(void (*callback)(bool)) { CleanCycle.setCallback(callback); } - int getVARIValue() { return VARIValue.getValue(); } - void setVARIValueCallback(void (*callback)(int)) { VARIValue.setCallback(callback); } - int getFiltHrs() { return FiltSetHrs.getValue(); } void setFiltHrsCallback(void (*callback)(int)) { FiltSetHrs.setCallback(callback); } // This is used to generated the select diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 10740f7..c96e9b4 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -190,7 +190,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["blower"]["state"] = si.Outlet_Blower==2? "OFF" : "ON"; json["blower"]["mode"] = si.Outlet_Blower.getLabel(); - json["blower"]["speed"] = si.Outlet_Blower==2? "0" : String(si.getVARIValue()); + json["blower"]["speed"] = si.Outlet_Blower==2? "0" : String(si.VARIValue.get()); json["sleepTimers"]["timer1"]["state"] = si.L_1SNZ_DAY.getLabel(); json["sleepTimers"]["timer2"]["state"] = si.L_2SNZ_DAY.getLabel(); diff --git a/src/main.cpp b/src/main.cpp index 971fba9..f6f3567 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -586,7 +586,7 @@ void setSpaProperty(String property, String p) { } else if (property == "blower_speed") { try { if (p=="0") si.Outlet_Blower.set(2); - else si.setVARIValue(p.toInt()); + else si.VARIValue.set(p.toInt()); } catch (const std::exception& ex) { debugE("Failed to set blower speed: %s", ex.what()); } From ddad3d69e9955f4abc5f4e83c8d5beae86cfc98f Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 11:41:22 +1100 Subject: [PATCH 22/44] Migrate Filter Block Hours --- lib/SpaInterface/SpaInterface.cpp | 29 ++++++++++++++++++----------- lib/SpaInterface/SpaInterface.h | 28 ++++++++++++++++++++++------ lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 12 ++---------- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 12 ++++++++++-- 6 files changed, 53 insertions(+), 34 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index f03a423..c9ebd8e 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -566,16 +566,23 @@ bool SpaInterface::setMode(int mode){ return false; } -bool SpaInterface::setFiltBlockHrs(String duration){ - debugD("setFiltBlockHrs - %s", duration); - for (int i = 0; i < FiltBlockHrsSelect.size(); i++) { - if (FiltBlockHrsSelect[i] == duration) { - if (sendCommandCheckResult("W90:"+FiltBlockHrsSelect[i],FiltBlockHrsSelect[i])) { - update_FiltBlockHrs(FiltBlockHrsSelect[i]); - return true; - } - return false; - } +bool SpaInterface::setFiltBlockHrs(int mode){ + debugD("setFiltBlockHrs - %i", mode); + static const int valid[] = {1, 2, 3, 4, 6, 8, 12, 24}; + bool isValid = false; + for (int v : valid) { + if (mode == v) { isValid = true; break; } + } + if (!isValid) { + throw std::out_of_range("FiltBlockHrs value not in valid set (1,2,3,4,6,8,12,24)"); + } + if (mode == FiltBlockHrs.get()) { + debugD("No FiltBlockHrs change detected - current %i, new %i", FiltBlockHrs.get(), mode); + return true; + } + if (sendCommandCheckResult("W90:"+String(mode), String(mode))) { + FiltBlockHrs.update(mode); + return true; } return false; } @@ -923,7 +930,7 @@ void SpaInterface::updateMeasures() { ColorMode.update(statusResponseRaw[R6 + 4].toInt()); LSPDValue.update(statusResponseRaw[R6 + 5].toInt()); update_FiltHrs(statusResponseRaw[R6 + 6]); - update_FiltBlockHrs(statusResponseRaw[R6 + 7]); + FiltBlockHrs.update(statusResponseRaw[R6 + 7].toInt()); STMP.update(statusResponseRaw[R6 + 8].toInt()); update_L_24HOURS(statusResponseRaw[R6 + 9]); update_PSAV_LVL(statusResponseRaw[R6 + 10]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 76eec98..ea684da 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -200,6 +200,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (1..5). bool setVARIValue(int mode); + /// @brief Internal writer used by `FiltBlockHrs` RWProperty. + /// @details Sends `W90:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for values not in the valid set (1,2,3,4,6,8,12,24). + bool setFiltBlockHrs(int mode); public: /// @brief Init SpaInterface. @@ -526,6 +531,22 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid range 1..5. RWProperty VARIValue{this, &SpaInterface::setVARIValue}; + /// @brief Filtration block duration label map. + /// @details Valid durations in hours: 1, 2, 3, 4, 6, 8, 12, 24. + static constexpr ROProperty::LabelValue FiltBlockHrs_Map[] = { + {"1", 1}, + {"2", 2}, + {"3", 3}, + {"4", 4}, + {"6", 6}, + {"8", 8}, + {"12", 12}, + {"24", 24}, + }; + /// @brief Filtration block duration (hours). + /// @details Read/write. Valid values: 1, 2, 3, 4, 6, 8, 12, 24. + RWProperty FiltBlockHrs{this, &SpaInterface::setFiltBlockHrs, FiltBlockHrs_Map}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -552,12 +573,7 @@ class SpaInterface : public SpaProperties { /// @return True if successful bool setSpaTime(time_t t); - /// @brief Set filtration block duration (1,2,3,4,6,8,12,24 hours) - /// @param duration - /// @return - bool setFiltBlockHrs(String duration); - - /// @brief Set filtration hours (1 to 24 hours) +/// @brief Set filtration hours (1 to 24 hours) /// @param duration bool setFiltHrs(String duration); diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index e6e326e..75231e5 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -363,10 +363,6 @@ boolean SpaProperties::update_FiltHrs(const String& s){ return updateIntProperty(FiltSetHrs, s); } -boolean SpaProperties::update_FiltBlockHrs(const String& s){ - return updateIntProperty(FiltBlockHrs, s); -} - boolean SpaProperties::update_L_24HOURS(const String& s){ return updateIntProperty(L_24HOURS, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index ed67a6a..b78d1c8 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -231,9 +231,7 @@ Property HV_2; // R6 /// @brief Filter run time (in hours) per block Property FiltSetHrs; - /// @brief Filter block duration (hours) - Property FiltBlockHrs; - /// @brief Water temperature set point ('C) +/// @brief Water temperature set point ('C) // 1 = 12 hrs Property L_24HOURS; /// @brief Power save level @@ -642,7 +640,6 @@ Property HV_2; #pragma endregion #pragma region R6 boolean update_FiltHrs(const String&); - boolean update_FiltBlockHrs(const String&); boolean update_L_24HOURS(const String&); boolean update_PSAV_LVL(const String&); boolean update_PSAV_BGN(const String&); @@ -1055,12 +1052,7 @@ Property HV_2; // This is used to generated the select - int getFiltBlockHrs() { return FiltBlockHrs.getValue(); } - void setFiltBlockHrsCallback(void (*callback)(int)) { FiltBlockHrs.setCallback(callback); } - // According to the docs only certain values are valid here. Using a select to ensure only valid values are used. - const std::array FiltBlockHrsSelect = {"24","12","8","6","4","3","2","1"}; - - int getL_24HOURS() { return L_24HOURS.getValue(); } +int getL_24HOURS() { return L_24HOURS.getValue(); } void setL_24HOURSCallback(void (*callback)(int)) { L_24HOURS.setCallback(callback); } int getPSAV_LVL() { return PSAV_LVL.getValue(); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index c96e9b4..6b56557 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -160,7 +160,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["heatpump"]["mode"] = si.HPMP.getLabel(); json["heatpump"]["auxheat"] = si.HELE ? "ON" : "OFF"; - json["filtration"]["blockDuration"] = si.getFiltBlockHrs(); + json["filtration"]["blockDuration"] = si.FiltBlockHrs.get(); json["filtration"]["hours"] = si.getFiltHrs(); json["lockmode"] = si.lockModeMap[si.getLockMode()]; diff --git a/src/main.cpp b/src/main.cpp index f6f3567..34c0d71 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -430,7 +430,11 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "filtration_blockDuration"; ADConf.deviceClass = ""; ADConf.entityCategory = "config"; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.FiltBlockHrsSelect); + // TODO (future improvement): generateSelectAdJSON takes const std::array, but + // FiltBlockHrs_Map is LabelValue[]. A local array bridges the type mismatch until + // generateSelectAdJSON is updated to accept the label map directly. + static const std::array FiltBlockHrsSelect = {"24","12","8","6","4","3","2","1"}; + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, FiltBlockHrsSelect); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); // Simply used to populate the select options for filtration hours 1 to 24 @@ -639,7 +643,11 @@ void setSpaProperty(String property, String p) { debugE("Failed to set Mode from label '%s': %s", p.c_str(), ex.what()); } } else if (property == "filtration_blockDuration") { - si.setFiltBlockHrs(p); + try { + si.FiltBlockHrs.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set FiltBlockHrs from label '%s': %s", p.c_str(), ex.what()); + } } else if (property == "filtration_hours") { si.setFiltHrs(p); } else if (property == "lock_mode") { From 8980b4806f47c3b86189b8dcd1fd4cb28a675470 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 11:44:13 +1100 Subject: [PATCH 23/44] Migrate FiltHrs --- lib/SpaInterface/SpaInterface.cpp | 18 ++++++++++-------- lib/SpaInterface/SpaInterface.h | 13 +++++++++---- lib/SpaInterface/SpaProperties.cpp | 4 ---- lib/SpaInterface/SpaProperties.h | 8 -------- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 6 +++++- 6 files changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index c9ebd8e..289335d 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -587,15 +587,17 @@ bool SpaInterface::setFiltBlockHrs(int mode){ return false; } -bool SpaInterface::setFiltHrs(String duration){ - debugD("setFiltHrs - %s", duration); - int hrs = duration.toInt(); - if (hrs<1 or hrs>24) { - debugE("FiltHrs out of range - %s", duration.c_str()); - return false; +bool SpaInterface::setFiltHrs(int hrs){ + debugD("setFiltHrs - %i", hrs); + if (hrs < 1 || hrs > 24) { + throw std::out_of_range("FiltHrs value out of range (1..24)"); + } + if (hrs == FiltHrs.get()) { + debugD("No FiltHrs change detected - current %i, new %i", FiltHrs.get(), hrs); + return true; } if (sendCommandCheckResult("W60:" + String(hrs), String(hrs))) { - update_FiltHrs(duration); + FiltHrs.update(hrs); return true; } return false; @@ -929,7 +931,7 @@ void SpaInterface::updateMeasures() { CurrClr.update(statusResponseRaw[R6 + 3].toInt()); ColorMode.update(statusResponseRaw[R6 + 4].toInt()); LSPDValue.update(statusResponseRaw[R6 + 5].toInt()); - update_FiltHrs(statusResponseRaw[R6 + 6]); + FiltHrs.update(statusResponseRaw[R6 + 6].toInt()); FiltBlockHrs.update(statusResponseRaw[R6 + 7].toInt()); STMP.update(statusResponseRaw[R6 + 8].toInt()); update_L_24HOURS(statusResponseRaw[R6 + 9]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index ea684da..bdb8779 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -205,6 +205,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for values not in the valid set (1,2,3,4,6,8,12,24). bool setFiltBlockHrs(int mode); + /// @brief Internal writer used by `FiltHrs` RWProperty. + /// @details Sends `W60:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (1..24). + bool setFiltHrs(int hrs); public: /// @brief Init SpaInterface. @@ -547,6 +552,10 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid values: 1, 2, 3, 4, 6, 8, 12, 24. RWProperty FiltBlockHrs{this, &SpaInterface::setFiltBlockHrs, FiltBlockHrs_Map}; + /// @brief Filtration run time per block (hours). + /// @details Read/write. Valid range 1..24. + RWProperty FiltHrs{this, &SpaInterface::setFiltHrs}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -573,10 +582,6 @@ class SpaInterface : public SpaProperties { /// @return True if successful bool setSpaTime(time_t t); -/// @brief Set filtration hours (1 to 24 hours) - /// @param duration - bool setFiltHrs(String duration); - bool setLockMode(int mode); /// @brief Unified array of RWProperty pointers for each migrated pump, used for diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 75231e5..331b446 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -359,10 +359,6 @@ boolean SpaProperties::update_CleanCycle(const String& s) { return updateBool01Property(CleanCycle, s); } -boolean SpaProperties::update_FiltHrs(const String& s){ - return updateIntProperty(FiltSetHrs, s); -} - boolean SpaProperties::update_L_24HOURS(const String& s){ return updateIntProperty(L_24HOURS, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index b78d1c8..199acc1 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -229,8 +229,6 @@ Property HV_2; #pragma endregion #pragma region R6 // R6 -/// @brief Filter run time (in hours) per block - Property FiltSetHrs; /// @brief Water temperature set point ('C) // 1 = 12 hrs Property L_24HOURS; @@ -639,7 +637,6 @@ Property HV_2; boolean update_CleanCycle(const String&); #pragma endregion #pragma region R6 - boolean update_FiltHrs(const String&); boolean update_L_24HOURS(const String&); boolean update_PSAV_LVL(const String&); boolean update_PSAV_BGN(const String&); @@ -1047,11 +1044,6 @@ Property HV_2; bool getCleanCycle() { return CleanCycle.getValue(); } void setCleanCycleCallback(void (*callback)(bool)) { CleanCycle.setCallback(callback); } - int getFiltHrs() { return FiltSetHrs.getValue(); } - void setFiltHrsCallback(void (*callback)(int)) { FiltSetHrs.setCallback(callback); } - // This is used to generated the select - - int getL_24HOURS() { return L_24HOURS.getValue(); } void setL_24HOURSCallback(void (*callback)(int)) { L_24HOURS.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 6b56557..c5fcbf7 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -161,7 +161,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["heatpump"]["auxheat"] = si.HELE ? "ON" : "OFF"; json["filtration"]["blockDuration"] = si.FiltBlockHrs.get(); - json["filtration"]["hours"] = si.getFiltHrs(); + json["filtration"]["hours"] = si.FiltHrs.get(); json["lockmode"] = si.lockModeMap[si.getLockMode()]; diff --git a/src/main.cpp b/src/main.cpp index 34c0d71..c112620 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -649,7 +649,11 @@ void setSpaProperty(String property, String p) { debugE("Failed to set FiltBlockHrs from label '%s': %s", p.c_str(), ex.what()); } } else if (property == "filtration_hours") { - si.setFiltHrs(p); + try { + si.FiltHrs.set(p.toInt()); + } catch (const std::exception& ex) { + debugE("Failed to set FiltHrs: %s", ex.what()); + } } else if (property == "lock_mode") { for (int i = 0; i < si.lockModeMap.size(); i++) { if (si.lockModeMap[i] == p) { From 68b70e10ac66302d1b02600fe3dc6a2a4058b7b5 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 14:05:49 +1100 Subject: [PATCH 24/44] Migrate LockMode and fix visabilit of map decs. --- lib/SpaInterface/SpaInterface.cpp | 19 ++-- lib/SpaInterface/SpaInterface.h | 170 ++++++++++++++--------------- lib/SpaInterface/SpaProperties.cpp | 4 - lib/SpaInterface/SpaProperties.h | 8 -- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 19 ++-- 6 files changed, 101 insertions(+), 121 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 289335d..2b92188 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -605,18 +605,15 @@ bool SpaInterface::setFiltHrs(int hrs){ bool SpaInterface::setLockMode(int mode){ debugD("setLockMode - %i", mode); - if (mode == getLockMode()) { - debugD("No LockMode change detected - current %i, new %i", getLockMode(), mode); - return true; - } - if (mode < 0 || mode > 2) { - debugE("LockMode out of range - %i", mode); - return false; + throw std::out_of_range("LockMode value out of range (0..2)"); } - - if (sendCommandCheckResult("S21:"+String(mode),String(mode))) { - update_LockMode(String(mode)); + if (mode == LockMode.get()) { + debugD("No LockMode change detected - current %i, new %i", LockMode.get(), mode); + return true; + } + if (sendCommandCheckResult("S21:"+String(mode), String(mode))) { + LockMode.update(mode); return true; } return false; @@ -1091,7 +1088,7 @@ void SpaInterface::updateMeasures() { update_Pump3OkToRun(statusResponseRaw[RG + 3]); update_Pump4OkToRun(statusResponseRaw[RG + 4]); update_Pump5OkToRun(statusResponseRaw[RG + 5]); - update_LockMode(statusResponseRaw[RG + 12]); + LockMode.update(statusResponseRaw[RG + 12].toInt()); #pragma endregion }; diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index bdb8779..465a474 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -210,6 +210,11 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (1..24). bool setFiltHrs(int hrs); + /// @brief Internal writer used by `LockMode` RWProperty. + /// @details Sends `S21:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..2). + bool setLockMode(int mode); public: /// @brief Init SpaInterface. @@ -351,36 +356,14 @@ class SpaInterface : public SpaProperties { WriteFunction _writer = nullptr; }; - /// @brief configure how often the spa is polled in seconds. - /// @param SpaPollFrequency - void setSpaPollFrequency(int updateFrequency); - - /// @brief Complete RF command response in a single string - Property statusResponse; - - /// @brief Mains current draw x10. - /// @details Read-only; value 77 represents 7.7A. - ROProperty MainsCurrent; - - /// @brief Water temperature set point x10. - /// @details Read/write. Valid range 50..410 (5.0C..41.0C). - RWProperty STMP{this, &SpaInterface::setSTMP}; - - /// @brief Heatpump operating mode values. - /// @details 0=Auto, 1=Heat, 2=Cool, 3=Off. + private: + // Label maps are private — use getLabelMap() on the property for external access. static constexpr ROProperty::LabelValue HPMP_Map[] = { {"Auto", 0}, {"Heat", 1}, {"Cool", 2}, {"Off", 3}, }; - /// @brief Heatpump operating mode. - /// @details Read/write wrapper around the private `setHPMP` writer. - /// Valid range 0..3. - RWProperty HPMP{this, &SpaInterface::setHPMP, HPMP_Map}; - - /// @brief Light effect/mode values. - /// @details 0=White, 1=Color, 2=Fade, 3=Step, 4=Party. static constexpr ROProperty::LabelValue ColorMode_Map[] = { {"White", 0}, {"Color", 1}, @@ -388,17 +371,6 @@ class SpaInterface : public SpaProperties { {"Step", 3}, {"Party", 4}, }; - /// @brief Light effect/mode. - /// @details Read/write wrapper around the private `setColorMode` writer. - /// Valid range 0..4. - RWProperty ColorMode{this, &SpaInterface::setColorMode, ColorMode_Map}; - - /// @brief Light brightness. - /// @details Read/write. Valid range 1..5. - RWProperty LBRTValue{this, &SpaInterface::setLBRTValue}; - - /// @brief Light effect speed values. This seems dumb but it is used to build the UI dropdowns as we - /// present light effect speed as a select. static constexpr ROProperty::LabelValue LSPDValue_Map[] = { {"1", 1}, {"2", 2}, @@ -406,12 +378,6 @@ class SpaInterface : public SpaProperties { {"4", 4}, {"5", 5}, }; - /// @brief Light effect speed. - /// @details Read/write. Valid range 1..5. - RWProperty LSPDValue{this, &SpaInterface::setLSPDValue, LSPDValue_Map}; - - /// @brief Maps hue values in 15-degree buckets to spa color indices. - /// @details Reverse lookup is ambiguous and returns the first matching hue label. static constexpr ROProperty::LabelValue CurrClr_Map[] = { {"0", 0}, {"15", 4}, @@ -439,12 +405,7 @@ class SpaInterface : public SpaProperties { {"345", 1}, {"360", 1}, }; - /// @brief Light color index. - /// @details Read/write. Valid range 0..31. - RWProperty CurrClr{this, &SpaInterface::setCurrClr, CurrClr_Map}; - - /// @brief Sleep timer day mode values. - /// @details Shared label/value map for both timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. + /// @details Shared label/value map for both sleep timers: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. static constexpr ROProperty::LabelValue SNZ_DAY_Map[] = { {"Off", 128}, {"Everyday", 127}, @@ -458,13 +419,85 @@ class SpaInterface : public SpaProperties { {"Saturday", 64}, {"Sunday", 32}, }; + static constexpr ROProperty::LabelValue SpaDayOfWeek_Map[] = { + {"Monday", 0}, + {"Tuesday", 1}, + {"Wednesday", 2}, + {"Thursday", 3}, + {"Friday", 4}, + {"Saturday", 5}, + {"Sunday", 6}, + }; + static constexpr ROProperty::LabelValue Outlet_Blower_Map[] = { + {"Variable", 0}, + {"Ramp", 1}, + {"Off", 2}, + }; + static constexpr ROProperty::LabelValue Mode_Map[] = { + {"NORM", 0}, + {"ECON", 1}, + {"AWAY", 2}, + {"WEEK", 3}, + }; + static constexpr ROProperty::LabelValue FiltBlockHrs_Map[] = { + {"1", 1}, + {"2", 2}, + {"3", 3}, + {"4", 4}, + {"6", 6}, + {"8", 8}, + {"12", 12}, + {"24", 24}, + }; + static constexpr ROProperty::LabelValue LockMode_Map[] = { + {"Unlocked", 0}, + {"Partially Locked", 1}, + {"Locked", 2}, + }; + + public: + /// @brief configure how often the spa is polled in seconds. + /// @param SpaPollFrequency + void setSpaPollFrequency(int updateFrequency); + + /// @brief Complete RF command response in a single string + Property statusResponse; + + /// @brief Mains current draw x10. + /// @details Read-only; value 77 represents 7.7A. + ROProperty MainsCurrent; + + /// @brief Water temperature set point x10. + /// @details Read/write. Valid range 50..410 (5.0C..41.0C). + RWProperty STMP{this, &SpaInterface::setSTMP}; + + /// @brief Heatpump operating mode. + /// @details Read/write. 0=Auto, 1=Heat, 2=Cool, 3=Off. + RWProperty HPMP{this, &SpaInterface::setHPMP, HPMP_Map}; + + /// @brief Light effect/mode. + /// @details Read/write. 0=White, 1=Color, 2=Fade, 3=Step, 4=Party. + RWProperty ColorMode{this, &SpaInterface::setColorMode, ColorMode_Map}; + + /// @brief Light brightness. + /// @details Read/write. Valid range 1..5. + RWProperty LBRTValue{this, &SpaInterface::setLBRTValue}; + + /// @brief Light effect speed. + /// @details Read/write. Valid range 1..5. + RWProperty LSPDValue{this, &SpaInterface::setLSPDValue, LSPDValue_Map}; + + /// @brief Light color index. + /// @details Read/write. Valid range 0..31. + RWProperty CurrClr{this, &SpaInterface::setCurrClr, CurrClr_Map}; + /// @brief Sleep timer 1 day mode bitmap. /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, SNZ_DAY_Map}; /// @brief Sleep timer 2 day mode bitmap. /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, SNZ_DAY_Map}; - + /// @brief Sleep timer 1 start time. /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). RWProperty L_1SNZ_BGN{this, &SpaInterface::setL_1SNZ_BGN}; @@ -497,37 +530,14 @@ class SpaInterface : public SpaProperties { /// @details Read/write. false=Off, true=On. RWProperty HELE{this, &SpaInterface::setHELE}; - /// @brief Day of week label map. 0=Monday .. 6=Sunday. - static constexpr ROProperty::LabelValue SpaDayOfWeek_Map[] = { - {"Monday", 0}, - {"Tuesday", 1}, - {"Wednesday", 2}, - {"Thursday", 3}, - {"Friday", 4}, - {"Saturday", 5}, - {"Sunday", 6}, - }; /// @brief Current day of week on Spa RTC. /// @details Read/write. 0=Monday .. 6=Sunday. RWProperty SpaDayOfWeek{this, &SpaInterface::setSpaDayOfWeek, SpaDayOfWeek_Map}; - /// @brief Blower mode label map. - static constexpr ROProperty::LabelValue Outlet_Blower_Map[] = { - {"Variable", 0}, - {"Ramp", 1}, - {"Off", 2}, - }; /// @brief Air blower operating mode. /// @details Read/write. 0=Variable, 1=Ramp, 2=Off. RWProperty Outlet_Blower{this, &SpaInterface::setOutlet_Blower, Outlet_Blower_Map}; - /// @brief Spa operating mode label map. - static constexpr ROProperty::LabelValue Mode_Map[] = { - {"NORM", 0}, - {"ECON", 1}, - {"AWAY", 2}, - {"WEEK", 3}, - }; /// @brief Spa operating mode. /// @details Read/write. 0=NORM, 1=ECON, 2=AWAY, 3=WEEK. RWProperty Mode{this, &SpaInterface::setMode, Mode_Map}; @@ -536,18 +546,6 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid range 1..5. RWProperty VARIValue{this, &SpaInterface::setVARIValue}; - /// @brief Filtration block duration label map. - /// @details Valid durations in hours: 1, 2, 3, 4, 6, 8, 12, 24. - static constexpr ROProperty::LabelValue FiltBlockHrs_Map[] = { - {"1", 1}, - {"2", 2}, - {"3", 3}, - {"4", 4}, - {"6", 6}, - {"8", 8}, - {"12", 12}, - {"24", 24}, - }; /// @brief Filtration block duration (hours). /// @details Read/write. Valid values: 1, 2, 3, 4, 6, 8, 12, 24. RWProperty FiltBlockHrs{this, &SpaInterface::setFiltBlockHrs, FiltBlockHrs_Map}; @@ -556,6 +554,10 @@ class SpaInterface : public SpaProperties { /// @details Read/write. Valid range 1..24. RWProperty FiltHrs{this, &SpaInterface::setFiltHrs}; + /// @brief Keypad lock mode. + /// @details Read/write. 0=Unlocked, 1=Partially Locked, 2=Locked. + RWProperty LockMode{this, &SpaInterface::setLockMode, LockMode_Map}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -582,8 +584,6 @@ class SpaInterface : public SpaProperties { /// @return True if successful bool setSpaTime(time_t t); - bool setLockMode(int mode); - /// @brief Unified array of RWProperty pointers for each migrated pump, used for /// both reading state and sending commands. Grows as pumps are migrated. using PumpStatus = RWProperty SpaInterface::*; diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 331b446..8df3bcd 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -770,8 +770,4 @@ boolean SpaProperties::update_Pump5OkToRun(const String& s) { return updateBool01Property(Pump5OkToRun, s); } -boolean SpaProperties::update_LockMode(const String& s) { - // LockMode is tri-state: 0 = unlocked, 1 = partial, 2 = full. - return updateTriStateProperty(LockMode, s); -} diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 199acc1..02727cc 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -537,10 +537,6 @@ Property HV_2; Property Pump4OkToRun; /// @brief Pump 5 is in safe state to start Property Pump5OkToRun; - /// @brief Lock mode - /// - /// 0 = keypad unlocked, 1 = partial lock, 2 = full lock - Property LockMode; #pragma endregion @@ -774,7 +770,6 @@ Property HV_2; boolean update_Pump3OkToRun(const String&); boolean update_Pump4OkToRun(const String&); boolean update_Pump5OkToRun(const String&); - boolean update_LockMode(const String&); #pragma endregion @@ -1397,9 +1392,6 @@ int getL_24HOURS() { return L_24HOURS.getValue(); } bool getPump5OkToRun() { return Pump5OkToRun.getValue(); } void setPump5OkToRunCallback(void (*callback)(bool)) { Pump5OkToRun.setCallback(callback); } - int getLockMode() { return LockMode.getValue(); } - void setLockModeCallback(void (*callback)(int)) { LockMode.setCallback(callback); } - const std::array lockModeMap = {"Unlocked", "Partially Locked", "Locked"}; }; #endif diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index c5fcbf7..fd3e9e4 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -163,7 +163,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["filtration"]["blockDuration"] = si.FiltBlockHrs.get(); json["filtration"]["hours"] = si.FiltHrs.get(); - json["lockmode"] = si.lockModeMap[si.getLockMode()]; + json["lockmode"] = si.LockMode.getLabel(); JsonObject pumps = json["pumps"].to(); // Add pump data by calling the function for each pump diff --git a/src/main.cpp b/src/main.cpp index c112620..216f502 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -422,7 +422,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "status_spaMode"; ADConf.deviceClass = ""; ADConf.entityCategory = ""; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.spaModeStrings); + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.Mode); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Filtration Block Duration"; @@ -430,11 +430,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "filtration_blockDuration"; ADConf.deviceClass = ""; ADConf.entityCategory = "config"; - // TODO (future improvement): generateSelectAdJSON takes const std::array, but - // FiltBlockHrs_Map is LabelValue[]. A local array bridges the type mismatch until - // generateSelectAdJSON is updated to accept the label map directly. - static const std::array FiltBlockHrsSelect = {"24","12","8","6","4","3","2","1"}; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, FiltBlockHrsSelect); + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.FiltBlockHrs); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); // Simply used to populate the select options for filtration hours 1 to 24 @@ -453,7 +449,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "lock_mode"; ADConf.deviceClass = ""; ADConf.entityCategory = "config"; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.lockModeMap); + generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.LockMode); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); } @@ -655,11 +651,10 @@ void setSpaProperty(String property, String p) { debugE("Failed to set FiltHrs: %s", ex.what()); } } else if (property == "lock_mode") { - for (int i = 0; i < si.lockModeMap.size(); i++) { - if (si.lockModeMap[i] == p) { - si.setLockMode(i); - break; - } + try { + si.LockMode.setLabel(p.c_str()); + } catch (const std::exception& ex) { + debugE("Failed to set LockMode from label '%s': %s", p.c_str(), ex.what()); } } else { debugE("Unhandled property - %s",property.c_str()); From 4c90195f52e1251536c07bac23f8f25e328ac4e4 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 14:07:54 +1100 Subject: [PATCH 25/44] Remove unneeded getSTMP --- lib/SpaInterface/SpaInterface.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 465a474..6e7b5fb 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -565,10 +565,6 @@ class SpaInterface : public SpaProperties { /// @return bool isInitialised(); - /// @brief Water temperature set point multiplied by 10 (380 = 38.0 actual) - /// @return - int getSTMP() { return STMP.get(); } - /// @brief Set the function to be called when properties have been updated. /// @param f void setUpdateCallback(void (*f)()); From f266d18c0f6e82fc15cee9d3f5891425d7b5d0e3 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 14:13:10 +1100 Subject: [PATCH 26/44] Migrate SpaTIme --- lib/SpaInterface/SpaInterface.cpp | 21 +++++++++++++++------ lib/SpaInterface/SpaInterface.h | 12 +++++++----- lib/SpaInterface/SpaProperties.cpp | 15 --------------- lib/SpaInterface/SpaProperties.h | 12 +----------- lib/SpaUtils/SpaUtils.cpp | 22 +++++++++++----------- src/main.cpp | 6 +++++- 6 files changed, 39 insertions(+), 49 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 2b92188..e2bfb18 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -493,23 +493,23 @@ bool SpaInterface::setSpaTime(time_t t){ tmp = String(day(t)); outcome = outcome && sendCommandCheckResult("S03:"+tmp,tmp); delay(100); - + tmp = String(hour(t)); outcome = outcome && sendCommandCheckResult("S04:"+tmp,tmp); delay(100); - + tmp = String(minute(t)); outcome = outcome && sendCommandCheckResult("S05:"+tmp,tmp); delay(100); - + int weekDay = weekday(t); // day of the week (1-7), Sunday is day 1 (Arduino Time Library) // Convert to the format required by Spa: day of the week (0-6), Monday is day 0 if (weekDay == 1) weekDay = 6; else weekDay -= 2; outcome = outcome && setSpaDayOfWeek(weekDay); - - return outcome; + if (outcome) SpaTime.update(t); + return outcome; } bool SpaInterface::setOutlet_Blower(int mode){ @@ -831,7 +831,16 @@ void SpaInterface::updateMeasures() { update_CaseTemperature(statusResponseRaw[R2+3]); update_PortCurrent(statusResponseRaw[R2+4]); SpaDayOfWeek.update(statusResponseRaw[R2+5].toInt()); - update_SpaTime(statusResponseRaw[R2+11], statusResponseRaw[R2+10], statusResponseRaw[R2+9], statusResponseRaw[R2+6], statusResponseRaw[R2+7], statusResponseRaw[R2+8]); + { + tmElements_t tm; + tm.Year = CalendarYrToTm(statusResponseRaw[R2+11].toInt()); + tm.Month = statusResponseRaw[R2+10].toInt(); + tm.Day = statusResponseRaw[R2+9].toInt(); + tm.Hour = statusResponseRaw[R2+6].toInt(); + tm.Minute = statusResponseRaw[R2+7].toInt(); + tm.Second = statusResponseRaw[R2+8].toInt(); + SpaTime.update(makeTime(tm)); + } update_HeaterTemperature(statusResponseRaw[R2+12]); update_PoolTemperature(statusResponseRaw[R2+13]); update_WaterPresent(statusResponseRaw[R2+14]); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 6e7b5fb..8a22afd 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -215,6 +215,9 @@ class SpaInterface : public SpaProperties { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (0..2). bool setLockMode(int mode); + /// @brief Internal writer used by `SpaTime` RWProperty. + /// @details Sends S01..S05 + S06 (via setSpaDayOfWeek) to the controller. + bool setSpaTime(time_t t); public: /// @brief Init SpaInterface. @@ -558,6 +561,10 @@ class SpaInterface : public SpaProperties { /// @details Read/write. 0=Unlocked, 1=Partially Locked, 2=Locked. RWProperty LockMode{this, &SpaInterface::setLockMode, LockMode_Map}; + /// @brief Spa RTC clock value. + /// @details Read/write. Writing sends S01..S05 + S06 to the controller. + RWProperty SpaTime{this, &SpaInterface::setSpaTime}; + /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); @@ -575,11 +582,6 @@ class SpaInterface : public SpaProperties { bool setRB_TP_Light(int mode); - /// @brief Sets the clock on the spa - /// @param t Time - /// @return True if successful - bool setSpaTime(time_t t); - /// @brief Unified array of RWProperty pointers for each migrated pump, used for /// both reading state and sending commands. Grows as pumps are migrated. using PumpStatus = RWProperty SpaInterface::*; diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 8df3bcd..01c8f8a 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -40,21 +40,6 @@ static boolean updateTriStateProperty(Property& prop, const String& s) { return true; } -boolean SpaProperties::update_SpaTime(const String& year, const String& month, const String& day, const String& hour, const String& minute, const String& second){ - - tmElements_t tm; - tm.Year=CalendarYrToTm(year.toInt()); - tm.Month=month.toInt(); - tm.Day=day.toInt(); - tm.Hour=hour.toInt(); - tm.Minute=minute.toInt(); - tm.Second=second.toInt(); - - SpaTime.update_Value(makeTime(tm)); - - return true; -} - boolean SpaProperties::update_MainsVoltage(const String& s){ return updateIntProperty(MainsVoltage, s); } diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 02727cc..ab0785d 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -46,9 +46,7 @@ class SpaProperties /// @brief 12v port current (mA) Property PortCurrent; - /// @brief Current time on Spa RTC - Property SpaTime; - /// @brief Heater temperature ('C) + /// @brief Heater temperature ('C) Property HeaterTemperature; /// @brief Pool temperature ('C). Note this seems to return rubbish most of the time. Property PoolTemperature; @@ -543,11 +541,9 @@ Property HV_2; protected: #pragma region R2 - boolean update_SpaTime(const String& year, const String& month, const String& day, const String& hour, const String& minute, const String& second); boolean update_MainsVoltage(const String&); boolean update_CaseTemperature(const String&); boolean update_PortCurrent(const String&); - boolean update_SpaTime(const String&); boolean update_HeaterTemperature(const String&); boolean update_PoolTemperature(const String&); boolean update_WaterPresent(const String&); @@ -785,12 +781,6 @@ Property HV_2; int getPortCurrent() { return PortCurrent.getValue(); } void setPortCurrentCallback(void (*callback)(int)) { PortCurrent.setCallback(callback); } - /// @brief Gets the current time from the spa clock - /// @return - time_t getSpaTime() { return SpaTime.getValue(); } - void setSpaTimeCallback(void (*callback)(time_t)) { SpaTime.setCallback(callback); } - - /// @brief Get current heater temperature multiplied by 10 (245 = 24.5 actual) /// @return int getHeaterTemperature() { return HeaterTemperature.getValue(); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index fd3e9e4..1ffe5bf 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -173,17 +173,17 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String } } - String y=String(year(si.getSpaTime())); - String m=String(month(si.getSpaTime())); - if (month(si.getSpaTime())<10) m = "0"+m; - String d=String(day(si.getSpaTime())); - if (day(si.getSpaTime())<10) d = "0"+d; - String h=String(hour(si.getSpaTime())); - if (hour(si.getSpaTime())<10) h = "0"+h; - String min=String(minute(si.getSpaTime())); - if (minute(si.getSpaTime())<10) min = "0"+min; - String s=String(second(si.getSpaTime())); - if (second(si.getSpaTime())<10) s = "0"+s; + String y=String(year(si.SpaTime.get())); + String m=String(month(si.SpaTime.get())); + if (month(si.SpaTime.get())<10) m = "0"+m; + String d=String(day(si.SpaTime.get())); + if (day(si.SpaTime.get())<10) d = "0"+d; + String h=String(hour(si.SpaTime.get())); + if (hour(si.SpaTime.get())<10) h = "0"+h; + String min=String(minute(si.SpaTime.get())); + if (minute(si.SpaTime.get())<10) min = "0"+min; + String s=String(second(si.SpaTime.get())); + if (second(si.SpaTime.get())<10) s = "0"+s; json["status"]["datetime"]=y+"-"+m+"-"+d+" "+h+":"+min+":"+s; json["status"]["dayOfWeek"]=si.SpaDayOfWeek.getLabel(); diff --git a/src/main.cpp b/src/main.cpp index 216f502..5782b4f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -536,7 +536,11 @@ void setSpaProperty(String property, String p) { tm.Hour=p.substring(11,13).toInt(); tm.Minute=p.substring(14,16).toInt(); tm.Second=p.substring(17).toInt(); - si.setSpaTime(makeTime(tm)); + try { + si.SpaTime.set(makeTime(tm)); + } catch (const std::exception& ex) { + debugE("Failed to set SpaTime: %s", ex.what()); + } } else if (property == "status_dayOfWeek") { try { si.SpaDayOfWeek.setLabel(p.c_str()); From e3f9cdf543d15bcf3c4b28479cf7379bf4a09b72 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 14:26:35 +1100 Subject: [PATCH 27/44] Tidy up of migrations --- lib/SpaInterface/SpaInterface.cpp | 8 -------- lib/SpaInterface/SpaInterface.h | 11 ++++++++--- lib/SpaInterface/SpaProperties.h | 6 ++++++ lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 6 +++--- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index e2bfb18..57f4daf 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -11,14 +11,6 @@ SpaInterface::SpaInterface() : port(SPA_SERIAL) { SpaInterface::~SpaInterface() {} -SpaInterface::PumpStatus SpaInterface::pumpStatuses[] = { - &SpaInterface::RB_TP_Pump1, - &SpaInterface::RB_TP_Pump2, - &SpaInterface::RB_TP_Pump3, - &SpaInterface::RB_TP_Pump4, - &SpaInterface::RB_TP_Pump5, -}; - void SpaInterface::setSpaPollFrequency(int updateFrequency) { _updateFrequency = updateFrequency; } diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 8a22afd..3277d81 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -291,7 +291,7 @@ class SpaInterface : public SpaProperties { return; } } - throw std::invalid_argument(String("updateFromLabel: unknown label '") + label + "'"); + throw std::invalid_argument((String("updateFromLabel: unknown label '") + label + "'").c_str()); } friend class SpaInterface; @@ -585,8 +585,13 @@ class SpaInterface : public SpaProperties { /// @brief Unified array of RWProperty pointers for each migrated pump, used for /// both reading state and sending commands. Grows as pumps are migrated. using PumpStatus = RWProperty SpaInterface::*; - static PumpStatus pumpStatuses[]; - static const int pumpStatusesCount = 5; + static constexpr PumpStatus pumpStatuses[] = { + &SpaInterface::RB_TP_Pump1, + &SpaInterface::RB_TP_Pump2, + &SpaInterface::RB_TP_Pump3, + &SpaInterface::RB_TP_Pump4, + &SpaInterface::RB_TP_Pump5, + }; }; diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index ab0785d..5df9141 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -781,6 +781,12 @@ Property HV_2; int getPortCurrent() { return PortCurrent.getValue(); } void setPortCurrentCallback(void (*callback)(int)) { PortCurrent.setCallback(callback); } + /// @brief Gets the current time from the spa clock + /// @return + time_t getSpaTime() { return SpaTime.getValue(); } + void setSpaTimeCallback(void (*callback)(time_t)) { SpaTime.setCallback(callback); } + + /// @brief Get current heater temperature multiplied by 10 (245 = 24.5 actual) /// @return int getHeaterTemperature() { return HeaterTemperature.getValue(); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 1ffe5bf..3cf719a 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -78,7 +78,7 @@ bool getPumpModesJson(SpaInterface &si, int pumpNumber, JsonObject pumps) { } } - int pumpState = (pumpNumber - 1 < SpaInterface::pumpStatusesCount) + int pumpState = (pumpNumber - 1 < array_count(SpaInterface::pumpStatuses)) ? (si.*(SpaInterface::pumpStatuses[pumpNumber - 1])).get() : 0; if (pumpInstallState.endsWith("4") && possibleStates.length() > 1) { if (pumpState == 4) pumps[pumpKey]["mode"] = "Auto"; diff --git a/src/main.cpp b/src/main.cpp index 5782b4f..f9e3291 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -495,7 +495,7 @@ void setSpaProperty(String property, String p) { if (p == "1") p = "0"; else if (p == "2") p = "3"; else if (p == "3") p = "2"; - if (pumpNum - 1 < SpaInterface::pumpStatusesCount) + if (pumpNum - 1 < array_count(SpaInterface::pumpStatuses)) try { (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(p.toInt()); } catch (const std::exception& ex) { @@ -503,7 +503,7 @@ void setSpaProperty(String property, String p) { } } else if (property.startsWith("pump") && property.endsWith("_mode")) { int pumpNum = property.charAt(4) - '0'; - if (pumpNum - 1 < SpaInterface::pumpStatusesCount) { + if (pumpNum - 1 < array_count(SpaInterface::pumpStatuses)) { try { if (p == "Auto") (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(4); else (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(3); // When we change mode to manual set speed to low, as this matches the auto display speed @@ -514,7 +514,7 @@ void setSpaProperty(String property, String p) { } else if (property.startsWith("pump") && property.endsWith("_state")) { int pumpNum = property.charAt(4) - '0'; String pumpState = (si.*(pumpInstallStateFunctions[pumpNum-1]))(); - if (pumpNum - 1 < SpaInterface::pumpStatusesCount) { + if (pumpNum - 1 < array_count(SpaInterface::pumpStatuses)) { try { if (getPumpSpeedType(pumpState) == "2") (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(p=="OFF"?0:2); // When we turn on the pump use speed high else (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(p=="OFF"?0:1); From 7eecf1bd5d055764add12f1e4655adc0644b15d1 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 14:30:01 +1100 Subject: [PATCH 28/44] Tidy up SpaTime, Migrate RB_TP_Light --- lib/SpaInterface/SpaInterface.cpp | 14 +++++++------- lib/SpaInterface/SpaInterface.h | 8 ++++++-- lib/SpaInterface/SpaProperties.cpp | 3 --- lib/SpaInterface/SpaProperties.h | 12 ++---------- lib/SpaUtils/SpaUtils.cpp | 2 +- src/main.cpp | 6 +++++- 6 files changed, 21 insertions(+), 24 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 57f4daf..9a631d0 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -159,14 +159,14 @@ bool SpaInterface::setRB_TP_Pump5(int mode){ } bool SpaInterface::setRB_TP_Light(int mode){ - debugD("setRB_TP_Light - %i",mode); - if (mode == getRB_TP_Light()) { - debugD("No RB_TP_Light change detected - current %i, new %i", getRB_TP_Light(), mode); + debugD("setRB_TP_Light - %i", mode); + if (mode < 0 || mode > 1) throw std::out_of_range("RB_TP_Light value out of range (0..1)"); + if (mode == RB_TP_Light.get()) { + debugD("No RB_TP_Light change detected - current %i, new %i", RB_TP_Light.get(), mode); return true; } - - if (sendCommandCheckResult("W14","W14")) { - update_RB_TP_Light(String(mode)); + if (sendCommandCheckResult("W14", "W14")) { + RB_TP_Light.update(mode); return true; } return false; @@ -914,7 +914,7 @@ void SpaInterface::updateMeasures() { update_RB_TP_Ozone(statusResponseRaw[R5 + 11]); update_RB_TP_Heater(statusResponseRaw[R5 + 12]); update_RB_TP_Auto(statusResponseRaw[R5 + 13]); - update_RB_TP_Light(statusResponseRaw[R5 + 14]); + RB_TP_Light.update(statusResponseRaw[R5 + 14].toInt()); update_WTMP(statusResponseRaw[R5 + 15]); update_CleanCycle(statusResponseRaw[R5 + 16]); RB_TP_Pump1.update(statusResponseRaw[R5 + 18].toInt()); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 3277d81..1389d2b 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -218,6 +218,9 @@ class SpaInterface : public SpaProperties { /// @brief Internal writer used by `SpaTime` RWProperty. /// @details Sends S01..S05 + S06 (via setSpaDayOfWeek) to the controller. bool setSpaTime(time_t t); + /// @brief Internal writer used by `RB_TP_Light` RWProperty. + /// @details Sends `W14` to toggle the light; updates cached value to `mode`. + bool setRB_TP_Light(int mode); public: /// @brief Init SpaInterface. @@ -579,8 +582,9 @@ class SpaInterface : public SpaProperties { /// @brief Clear the call back function. void clearUpdateCallback(); - bool setRB_TP_Light(int mode); - + /// @brief Light on/off state. + /// @details Read/write. 0=Off, 1=On. + RWProperty RB_TP_Light{this, &SpaInterface::setRB_TP_Light}; /// @brief Unified array of RWProperty pointers for each migrated pump, used for /// both reading state and sending commands. Grows as pumps are migrated. diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp index 01c8f8a..8a4cfb9 100644 --- a/lib/SpaInterface/SpaProperties.cpp +++ b/lib/SpaInterface/SpaProperties.cpp @@ -316,9 +316,6 @@ boolean SpaProperties::update_RB_TP_Blower(const String& s){ return updateIntProperty(RB_TP_Blower, s); } -boolean SpaProperties::update_RB_TP_Light(const String& s){ - return updateIntProperty(RB_TP_Light, s); -} boolean SpaProperties::update_RB_TP_Auto(const String& s) { return updateBool01Property(RB_TP_Auto, s); diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h index 5df9141..b16c62c 100644 --- a/lib/SpaInterface/SpaProperties.h +++ b/lib/SpaInterface/SpaProperties.h @@ -201,7 +201,7 @@ Property HV_2; // Unknown encoding - Attribute TouchPad2; // Unknown encoding - Attribute TouchPad1; Property RB_TP_Blower; - Property RB_TP_Light; + /// @brief Auto enabled /// /// True when auto enabled @@ -620,7 +620,7 @@ Property HV_2; // Unknown encoding - TouchPad2.update_Value(); // Unknown encoding - TouchPad1.update_Value(); boolean update_RB_TP_Blower(const String&); - boolean update_RB_TP_Light(const String&); + boolean update_RB_TP_Auto(const String&); boolean update_RB_TP_Heater(const String&); boolean update_RB_TP_Ozone(const String&); @@ -781,12 +781,6 @@ Property HV_2; int getPortCurrent() { return PortCurrent.getValue(); } void setPortCurrentCallback(void (*callback)(int)) { PortCurrent.setCallback(callback); } - /// @brief Gets the current time from the spa clock - /// @return - time_t getSpaTime() { return SpaTime.getValue(); } - void setSpaTimeCallback(void (*callback)(time_t)) { SpaTime.setCallback(callback); } - - /// @brief Get current heater temperature multiplied by 10 (245 = 24.5 actual) /// @return int getHeaterTemperature() { return HeaterTemperature.getValue(); } @@ -1012,8 +1006,6 @@ Property HV_2; int getRB_TP_Blower() { return RB_TP_Blower.getValue(); } void setRB_TP_BlowerCallback(void (*callback)(int)) { RB_TP_Blower.setCallback(callback); } - int getRB_TP_Light() { return RB_TP_Light.getValue(); } - void setRB_TP_LightCallback(void (*callback)(int)) { RB_TP_Light.setCallback(callback); } bool getRB_TP_Auto() { return RB_TP_Auto.getValue(); } void setRB_TP_AutoCallback(void (*callback)(bool)) { RB_TP_Auto.setCallback(callback); } diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 3cf719a..5ef6554 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -200,7 +200,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["sleepTimers"]["timer2"]["end"]=convertToTime(si.L_2SNZ_END.get()); json["lights"]["speed"] = si.LSPDValue.get(); - json["lights"]["state"] = si.getRB_TP_Light()? "ON": "OFF"; + json["lights"]["state"] = si.RB_TP_Light.get()? "ON": "OFF"; json["lights"]["effect"] = si.ColorMode.getLabel(); json["lights"]["brightness"] = si.LBRTValue.get(); diff --git a/src/main.cpp b/src/main.cpp index f9e3291..f5053ae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -548,7 +548,11 @@ void setSpaProperty(String property, String p) { debugE("Failed to set SpaDayOfWeek '%s': %s", p.c_str(), ex.what()); } } else if (property == "lights_state") { - si.setRB_TP_Light(p=="ON"?1:0); + try { + si.RB_TP_Light.set(p=="ON"?1:0); + } catch (const std::exception& ex) { + debugE("Failed to set RB_TP_Light: %s", ex.what()); + } } else if (property == "lights_effect") { try { si.ColorMode.setLabel(p.c_str()); From a03cb69e3e885362bec29eb3e39b1b4d44a9e67c Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 8 Mar 2026 16:54:08 +1100 Subject: [PATCH 29/44] Final depreciation of SpaProperites --- lib/SpaInterface/SpaInterface.cpp | 366 ++++---- lib/SpaInterface/SpaInterface.h | 232 ++++- lib/SpaInterface/SpaProperties.cpp | 755 --------------- lib/SpaInterface/SpaProperties.h | 1386 ---------------------------- lib/SpaUtils/SpaUtils.cpp | 30 +- lib/WebUI/WebUI.cpp | 2 +- src/main.cpp | 8 +- 7 files changed, 422 insertions(+), 2357 deletions(-) delete mode 100644 lib/SpaInterface/SpaProperties.cpp delete mode 100644 lib/SpaInterface/SpaProperties.h diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 9a631d0..79a9b0b 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -158,6 +158,10 @@ bool SpaInterface::setRB_TP_Pump5(int mode){ return false; } +bool SpaInterface::setStatusResponse(String s) { + return true; +} + bool SpaInterface::setRB_TP_Light(int mode){ debugD("setRB_TP_Light - %i", mode); if (mode < 0 || mode > 1) throw std::out_of_range("RB_TP_Light value out of range (0..1)"); @@ -474,7 +478,7 @@ bool SpaInterface::setSpaTime(time_t t){ String tmp; bool outcome; - tmp = String(year(t)); + tmp = String(year(t) % 100); outcome = sendCommandCheckResult("S01:"+tmp, tmp); delay(100); @@ -629,11 +633,11 @@ bool SpaInterface::readStatus() { int majorFirmwarwVersion = 0; if (_initialised) { - uint spaceIndex = getSVER().indexOf(' ', 4); + uint spaceIndex = SVER.get().indexOf(' ', 4); if (spaceIndex != -1) { - majorFirmwarwVersion = getSVER().substring(4, spaceIndex).toInt(); // Skip the 'V' character + majorFirmwarwVersion = SVER.get().substring(4, spaceIndex).toInt(); // Skip the 'V' character } - debugV("Firmware: %s, majorFirmwareVersion: %i", getSVER().c_str(), majorFirmwarwVersion); + debugV("Firmware: %s, majorFirmwareVersion: %i", SVER.get().c_str(), majorFirmwarwVersion); } // read the first field and validate the response @@ -742,7 +746,7 @@ bool SpaInterface::readStatus() { //Flush the remaining data from the buffer as the last field is meaningless statusResponseTmp = statusResponseTmp + flushSerialReadBuffer(true); - statusResponse.update_Value(statusResponseTmp); + statusResponse.update(statusResponseTmp); if ((majorFirmwarwVersion > 2 && registerCounter < 12) || (majorFirmwarwVersion < 3 && registerCounter < 11)) { debugE("Throwing exception - not enough registers, we only read: %i", registerCounter); @@ -819,9 +823,9 @@ void SpaInterface::clearUpdateCallback() { void SpaInterface::updateMeasures() { #pragma region R2 MainsCurrent.update(statusResponseRaw[R2+1].toInt()); - update_MainsVoltage(statusResponseRaw[R2+2]); - update_CaseTemperature(statusResponseRaw[R2+3]); - update_PortCurrent(statusResponseRaw[R2+4]); + MainsVoltage.update(statusResponseRaw[R2+2].toInt()); + CaseTemperature.update(statusResponseRaw[R2+3].toInt()); + PortCurrent.update(statusResponseRaw[R2+4].toInt()); SpaDayOfWeek.update(statusResponseRaw[R2+5].toInt()); { tmElements_t tm; @@ -833,90 +837,90 @@ void SpaInterface::updateMeasures() { tm.Second = statusResponseRaw[R2+8].toInt(); SpaTime.update(makeTime(tm)); } - update_HeaterTemperature(statusResponseRaw[R2+12]); - update_PoolTemperature(statusResponseRaw[R2+13]); - update_WaterPresent(statusResponseRaw[R2+14]); - update_AwakeMinutesRemaining(statusResponseRaw[R2+16]); - update_FiltPumpRunTimeTotal(statusResponseRaw[R2+17]); - update_FiltPumpReqMins(statusResponseRaw[R2+18]); - update_LoadTimeOut(statusResponseRaw[R2+19]); - update_HourMeter(statusResponseRaw[R2+20]); - update_Relay1(statusResponseRaw[R2+21]); - update_Relay2(statusResponseRaw[R2+22]); - update_Relay3(statusResponseRaw[R2+23]); - update_Relay4(statusResponseRaw[R2+24]); - update_Relay5(statusResponseRaw[R2+25]); - update_Relay6(statusResponseRaw[R2+26]); - update_Relay7(statusResponseRaw[R2+27]); - update_Relay8(statusResponseRaw[R2+28]); - update_Relay9(statusResponseRaw[R2+29]); + HeaterTemperature.update(statusResponseRaw[R2+12].toInt()); + PoolTemperature.update(statusResponseRaw[R2+13].toInt()); + WaterPresent.update(statusResponseRaw[R2+14] == "1"); + AwakeMinutesRemaining.update(statusResponseRaw[R2+16].toInt()); + FiltPumpRunTimeTotal.update(statusResponseRaw[R2+17].toInt()); + FiltPumpReqMins.update(statusResponseRaw[R2+18].toInt()); + LoadTimeOut.update(statusResponseRaw[R2+19].toInt()); + HourMeter.update(statusResponseRaw[R2+20].toInt()); + Relay1.update(statusResponseRaw[R2+21].toInt()); + Relay2.update(statusResponseRaw[R2+22].toInt()); + Relay3.update(statusResponseRaw[R2+23].toInt()); + Relay4.update(statusResponseRaw[R2+24].toInt()); + Relay5.update(statusResponseRaw[R2+25].toInt()); + Relay6.update(statusResponseRaw[R2+26].toInt()); + Relay7.update(statusResponseRaw[R2+27].toInt()); + Relay8.update(statusResponseRaw[R2+28].toInt()); + Relay9.update(statusResponseRaw[R2+29].toInt()); #pragma endregion #pragma region R3 - update_CLMT(statusResponseRaw[R3+1]); - update_PHSE(statusResponseRaw[R3+2]); - update_LLM1(statusResponseRaw[R3+3]); - update_LLM2(statusResponseRaw[R3+4]); - update_LLM3(statusResponseRaw[R3+5]); - update_SVER(statusResponseRaw[R3+6]); - update_Model(statusResponseRaw[R3+7]); - update_SerialNo1(statusResponseRaw[R3+8]); - update_SerialNo2(statusResponseRaw[R3+9]); - update_D1(statusResponseRaw[R3+10]); - update_D2(statusResponseRaw[R3+11]); - update_D3(statusResponseRaw[R3+12]); - update_D4(statusResponseRaw[R3+13]); - update_D5(statusResponseRaw[R3+14]); - update_D6(statusResponseRaw[R3+15]); - update_Pump(statusResponseRaw[R3+16]); - update_LS(statusResponseRaw[R3+17]); - update_HV(statusResponseRaw[R3+18]); - update_SnpMR(statusResponseRaw[R3+19]); - update_Status(statusResponseRaw[R3+20]); - update_PrimeCount(statusResponseRaw[R3+21]); - update_EC(statusResponseRaw[R3+22]); - update_HAMB(statusResponseRaw[R3+23]); - update_HCON(statusResponseRaw[R3+24]); + CLMT.update(statusResponseRaw[R3+1].toInt()); + PHSE.update(statusResponseRaw[R3+2].toInt()); + LLM1.update(statusResponseRaw[R3+3].toInt()); + LLM2.update(statusResponseRaw[R3+4].toInt()); + LLM3.update(statusResponseRaw[R3+5].toInt()); + SVER.update(statusResponseRaw[R3+6]); + Model.update(statusResponseRaw[R3+7]); + SerialNo1.update(statusResponseRaw[R3+8]); + SerialNo2.update(statusResponseRaw[R3+9]); + D1.update(statusResponseRaw[R3+10] == "1"); + D2.update(statusResponseRaw[R3+11] == "1"); + D3.update(statusResponseRaw[R3+12] == "1"); + D4.update(statusResponseRaw[R3+13] == "1"); + D5.update(statusResponseRaw[R3+14] == "1"); + D6.update(statusResponseRaw[R3+15] == "1"); + Pump.update(statusResponseRaw[R3+16]); + LS.update(statusResponseRaw[R3+17].toInt()); + HV.update(statusResponseRaw[R3+18] == "1"); + SnpMR.update(statusResponseRaw[R3+19].toInt()); + Status.update(statusResponseRaw[R3+20]); + PrimeCount.update(statusResponseRaw[R3+21].toInt()); + EC.update(statusResponseRaw[R3+22].toInt()); + HAMB.update(statusResponseRaw[R3+23].toInt()); + HCON.update(statusResponseRaw[R3+24].toInt()); // update_HV_2(statusResponseRaw[R3+25]); #pragma endregion #pragma region R4 try { Mode.updateFromLabel(statusResponseRaw[R4+1].c_str()); } catch (const std::exception& ex) { debugE("Mode update failed: %s", ex.what()); } - update_Ser1_Timer(statusResponseRaw[R4+2]); - update_Ser2_Timer(statusResponseRaw[R4+3]); - update_Ser3_Timer(statusResponseRaw[R4+4]); - update_HeatMode(statusResponseRaw[R4+5]); - update_PumpIdleTimer(statusResponseRaw[R4+6]); - update_PumpRunTimer(statusResponseRaw[R4+7]); - update_AdtPoolHys(statusResponseRaw[R4+8]); - update_AdtHeaterHys(statusResponseRaw[R4+9]); - update_Power(statusResponseRaw[R4+10]); - update_Power_kWh(statusResponseRaw[R4+11]); - update_Power_Today(statusResponseRaw[R4+12]); - update_Power_Yesterday(statusResponseRaw[R4+13]); - update_ThermalCutOut(statusResponseRaw[R4+14]); - update_Test_D1(statusResponseRaw[R4+15]); - update_Test_D2(statusResponseRaw[R4+16]); - update_Test_D3(statusResponseRaw[R4+17]); - update_ElementHeatSourceOffset(statusResponseRaw[R4+18]); - update_Frequency(statusResponseRaw[R4+19]); - update_HPHeatSourceOffset_Heat(statusResponseRaw[R4+20]); - update_HPHeatSourceOffset_Cool(statusResponseRaw[R4+21]); - update_HeatSourceOffTime(statusResponseRaw[R4+22]); - update_Vari_Speed(statusResponseRaw[R4+24]); - update_Vari_Percent(statusResponseRaw[R4+25]); - update_Vari_Mode(statusResponseRaw[R4+23]); + Ser1_Timer.update(statusResponseRaw[R4+2].toInt()); + Ser2_Timer.update(statusResponseRaw[R4+3].toInt()); + Ser3_Timer.update(statusResponseRaw[R4+4].toInt()); + HeatMode.update(statusResponseRaw[R4+5].toInt()); + PumpIdleTimer.update(statusResponseRaw[R4+6].toInt()); + PumpRunTimer.update(statusResponseRaw[R4+7].toInt()); + AdtPoolHys.update(statusResponseRaw[R4+8].toInt()); + AdtHeaterHys.update(statusResponseRaw[R4+9].toInt()); + Power.update(statusResponseRaw[R4+10].toInt()); + Power_kWh.update(statusResponseRaw[R4+11].toInt()); + Power_Today.update(statusResponseRaw[R4+12].toInt()); + Power_Yesterday.update(statusResponseRaw[R4+13].toInt()); + ThermalCutOut.update(statusResponseRaw[R4+14].toInt()); + Test_D1.update(statusResponseRaw[R4+15].toInt()); + Test_D2.update(statusResponseRaw[R4+16].toInt()); + Test_D3.update(statusResponseRaw[R4+17].toInt()); + ElementHeatSourceOffset.update(statusResponseRaw[R4+18].toInt()); + Frequency.update(statusResponseRaw[R4+19].toInt()); + HPHeatSourceOffset_Heat.update(statusResponseRaw[R4+20].toInt()); + HPHeatSourceOffset_Cool.update(statusResponseRaw[R4+21].toInt()); + HeatSourceOffTime.update(statusResponseRaw[R4+22].toInt()); + Vari_Speed.update(statusResponseRaw[R4+24].toInt()); + Vari_Percent.update(statusResponseRaw[R4+25].toInt()); + Vari_Mode.update(statusResponseRaw[R4+23].toInt()); #pragma endregion #pragma region R5 //R5 // Unknown encoding - TouchPad2.updateValue(); // Unknown encoding - TouchPad1.updateValue(); //RB_TP_Blower.updateValue(statusResponseRaw[R5 + 5]); - update_RB_TP_Sleep(statusResponseRaw[R5 + 10]); - update_RB_TP_Ozone(statusResponseRaw[R5 + 11]); - update_RB_TP_Heater(statusResponseRaw[R5 + 12]); - update_RB_TP_Auto(statusResponseRaw[R5 + 13]); + RB_TP_Sleep.update(statusResponseRaw[R5 + 10] == "1"); + RB_TP_Ozone.update(statusResponseRaw[R5 + 11] == "1"); + RB_TP_Heater.update(statusResponseRaw[R5 + 12] == "1"); + RB_TP_Auto.update(statusResponseRaw[R5 + 13] == "1"); RB_TP_Light.update(statusResponseRaw[R5 + 14].toInt()); - update_WTMP(statusResponseRaw[R5 + 15]); - update_CleanCycle(statusResponseRaw[R5 + 16]); + WTMP.update(statusResponseRaw[R5 + 15].toInt()); + CleanCycle.update(statusResponseRaw[R5 + 16] == "1"); RB_TP_Pump1.update(statusResponseRaw[R5 + 18].toInt()); RB_TP_Pump2.update(statusResponseRaw[R5 + 19].toInt()); RB_TP_Pump3.update(statusResponseRaw[R5 + 20].toInt()); @@ -932,101 +936,101 @@ void SpaInterface::updateMeasures() { FiltHrs.update(statusResponseRaw[R6 + 6].toInt()); FiltBlockHrs.update(statusResponseRaw[R6 + 7].toInt()); STMP.update(statusResponseRaw[R6 + 8].toInt()); - update_L_24HOURS(statusResponseRaw[R6 + 9]); - update_PSAV_LVL(statusResponseRaw[R6 + 10]); - update_PSAV_BGN(statusResponseRaw[R6 + 11]); - update_PSAV_END(statusResponseRaw[R6 + 12]); + L_24HOURS.update(statusResponseRaw[R6 + 9].toInt()); + PSAV_LVL.update(statusResponseRaw[R6 + 10].toInt()); + PSAV_BGN.update(statusResponseRaw[R6 + 11].toInt()); + PSAV_END.update(statusResponseRaw[R6 + 12].toInt()); L_1SNZ_DAY.update(statusResponseRaw[R6 + 13].toInt()); L_2SNZ_DAY.update(statusResponseRaw[R6 + 14].toInt()); L_1SNZ_BGN.update(statusResponseRaw[R6 + 15].toInt()); L_2SNZ_BGN.update(statusResponseRaw[R6 + 16].toInt()); L_1SNZ_END.update(statusResponseRaw[R6 + 17].toInt()); L_2SNZ_END.update(statusResponseRaw[R6 + 18].toInt()); - update_DefaultScrn(statusResponseRaw[R6 + 19]); - update_TOUT(statusResponseRaw[R6 + 20]); - update_VPMP(statusResponseRaw[R6 + 21]); - update_HIFI(statusResponseRaw[R6 + 22]); - update_BRND(statusResponseRaw[R6 + 23]); + DefaultScrn.update(statusResponseRaw[R6 + 19].toInt()); + TOUT.update(statusResponseRaw[R6 + 20].toInt()); + VPMP.update(statusResponseRaw[R6 + 21] == "1"); + HIFI.update(statusResponseRaw[R6 + 22] == "1"); + BRND.update(statusResponseRaw[R6 + 23].toInt()); // Note: We only have 23 registers in V2 firmware if (R6 > 23) { - update_PRME(statusResponseRaw[R6 + 24]); - update_ELMT(statusResponseRaw[R6 + 25]); - update_TYPE(statusResponseRaw[R6 + 26]); - update_GAS(statusResponseRaw[R6 + 27]); + PRME.update(statusResponseRaw[R6 + 24].toInt()); + ELMT.update(statusResponseRaw[R6 + 25].toInt()); + TYPE.update(statusResponseRaw[R6 + 26].toInt()); + GAS.update(statusResponseRaw[R6 + 27].toInt()); } #pragma endregion #pragma region R7 - update_WCLNTime(statusResponseRaw[R7 + 1]); + WCLNTime.update(statusResponseRaw[R7 + 1].toInt()); // The following 2 may be reversed - update_TemperatureUnits(statusResponseRaw[R7 + 3]); - update_OzoneOff(statusResponseRaw[R7 + 2]); - update_Ozone24(statusResponseRaw[R7 + 4]); - update_Circ24(statusResponseRaw[R7 + 6]); - update_CJET(statusResponseRaw[R7 + 5]); + TemperatureUnits.update(statusResponseRaw[R7 + 3] == "1"); + OzoneOff.update(statusResponseRaw[R7 + 2] == "1"); + Ozone24.update(statusResponseRaw[R7 + 4] == "1"); + Circ24.update(statusResponseRaw[R7 + 6] == "1"); + CJET.update(statusResponseRaw[R7 + 5] == "1"); // 0 = off, 1 = step, 2 = variable - update_VELE(statusResponseRaw[R7 + 7]); + VELE.update(statusResponseRaw[R7 + 7] == "1"); //update_StartDD(statusResponseRaw[R7 + 8]); //update_StartMM(statusResponseRaw[R7 + 9]); //update_StartYY(statusResponseRaw[R7 + 10]); - update_V_Max(statusResponseRaw[R7 + 11]); - update_V_Min(statusResponseRaw[R7 + 12]); - update_V_Max_24(statusResponseRaw[R7 + 13]); - update_V_Min_24(statusResponseRaw[R7 + 14]); - update_CurrentZero(statusResponseRaw[R7 + 15]); - update_CurrentAdjust(statusResponseRaw[R7 + 16]); - update_VoltageAdjust(statusResponseRaw[R7 + 17]); + V_Max.update(statusResponseRaw[R7 + 11].toInt()); + V_Min.update(statusResponseRaw[R7 + 12].toInt()); + V_Max_24.update(statusResponseRaw[R7 + 13].toInt()); + V_Min_24.update(statusResponseRaw[R7 + 14].toInt()); + CurrentZero.update(statusResponseRaw[R7 + 15].toInt()); + CurrentAdjust.update(statusResponseRaw[R7 + 16].toInt()); + VoltageAdjust.update(statusResponseRaw[R7 + 17].toInt()); // 168 is unknown - update_Ser1(statusResponseRaw[R7 + 19]); - update_Ser2(statusResponseRaw[R7 + 20]); - update_Ser3(statusResponseRaw[R7 + 21]); - update_VMAX(statusResponseRaw[R7 + 22]); - update_AHYS(statusResponseRaw[R7 + 23]); - update_HUSE(statusResponseRaw[R7 + 24]); + Ser1.update(statusResponseRaw[R7 + 19].toInt()); + Ser2.update(statusResponseRaw[R7 + 20].toInt()); + Ser3.update(statusResponseRaw[R7 + 21].toInt()); + VMAX.update(statusResponseRaw[R7 + 22].toInt()); + AHYS.update(statusResponseRaw[R7 + 23].toInt()); + HUSE.update(statusResponseRaw[R7 + 24] == "1"); HELE.update(statusResponseRaw[R7 + 25] == "1"); HPMP.update(statusResponseRaw[R7 + 26].toInt()); - update_PMIN(statusResponseRaw[R7 + 27]); - update_PFLT(statusResponseRaw[R7 + 28]); - update_PHTR(statusResponseRaw[R7 + 29]); - update_PMAX(statusResponseRaw[R7 + 30]); + PMIN.update(statusResponseRaw[R7 + 27].toInt()); + PFLT.update(statusResponseRaw[R7 + 28].toInt()); + PHTR.update(statusResponseRaw[R7 + 29].toInt()); + PMAX.update(statusResponseRaw[R7 + 30].toInt()); #pragma endregion #pragma region R9 - update_F1_HR(statusResponseRaw[R9 + 2]); - update_F1_Time(statusResponseRaw[R9 + 3]); - update_F1_ER(statusResponseRaw[R9 + 4]); - update_F1_I(statusResponseRaw[R9 + 5]); - update_F1_V(statusResponseRaw[R9 + 6]); - update_F1_PT(statusResponseRaw[R9 + 7]); - update_F1_HT(statusResponseRaw[R9 + 8]); - update_F1_CT(statusResponseRaw[R9 + 9]); - update_F1_PU(statusResponseRaw[R9 + 10]); - update_F1_VE(statusResponseRaw[R9 + 11]); - update_F1_ST(statusResponseRaw[R9 + 12]); + F1_HR.update(statusResponseRaw[R9 + 2].toInt()); + F1_Time.update(statusResponseRaw[R9 + 3].toInt()); + F1_ER.update(statusResponseRaw[R9 + 4].toInt()); + F1_I.update(statusResponseRaw[R9 + 5].toInt()); + F1_V.update(statusResponseRaw[R9 + 6].toInt()); + F1_PT.update(statusResponseRaw[R9 + 7].toInt()); + F1_HT.update(statusResponseRaw[R9 + 8].toInt()); + F1_CT.update(statusResponseRaw[R9 + 9].toInt()); + F1_PU.update(statusResponseRaw[R9 + 10].toInt()); + F1_VE.update(statusResponseRaw[R9 + 11] == "1"); + F1_ST.update(statusResponseRaw[R9 + 12].toInt()); #pragma endregion #pragma region RA - update_F2_HR(statusResponseRaw[RA + 2]); - update_F2_Time(statusResponseRaw[RA + 3]); - update_F2_ER(statusResponseRaw[RA + 4]); - update_F2_I(statusResponseRaw[RA + 5]); - update_F2_V(statusResponseRaw[RA + 6]); - update_F2_PT(statusResponseRaw[RA + 7]); - update_F2_HT(statusResponseRaw[RA + 8]); - update_F2_CT(statusResponseRaw[RA + 9]); - update_F2_PU(statusResponseRaw[RA + 10]); - update_F2_VE(statusResponseRaw[RA + 11]); - update_F2_ST(statusResponseRaw[RA + 12]); + F2_HR.update(statusResponseRaw[RA + 2].toInt()); + F2_Time.update(statusResponseRaw[RA + 3].toInt()); + F2_ER.update(statusResponseRaw[RA + 4].toInt()); + F2_I.update(statusResponseRaw[RA + 5].toInt()); + F2_V.update(statusResponseRaw[RA + 6].toInt()); + F2_PT.update(statusResponseRaw[RA + 7].toInt()); + F2_HT.update(statusResponseRaw[RA + 8].toInt()); + F2_CT.update(statusResponseRaw[RA + 9].toInt()); + F2_PU.update(statusResponseRaw[RA + 10].toInt()); + F2_VE.update(statusResponseRaw[RA + 11] == "1"); + F2_ST.update(statusResponseRaw[RA + 12].toInt()); #pragma endregion #pragma region RB - update_F3_HR(statusResponseRaw[RB + 2]); - update_F3_Time(statusResponseRaw[RB + 3]); - update_F3_ER(statusResponseRaw[RB + 4]); - update_F3_I(statusResponseRaw[RB + 5]); - update_F3_V(statusResponseRaw[RB + 6]); - update_F3_PT(statusResponseRaw[RB + 7]); - update_F3_HT(statusResponseRaw[RB + 8]); - update_F3_CT(statusResponseRaw[RB + 9]); - update_F3_PU(statusResponseRaw[RB + 10]); - update_F3_VE(statusResponseRaw[RB + 11]); - update_F3_ST(statusResponseRaw[RB + 12]); + F3_HR.update(statusResponseRaw[RB + 2].toInt()); + F3_Time.update(statusResponseRaw[RB + 3].toInt()); + F3_ER.update(statusResponseRaw[RB + 4].toInt()); + F3_I.update(statusResponseRaw[RB + 5].toInt()); + F3_V.update(statusResponseRaw[RB + 6].toInt()); + F3_PT.update(statusResponseRaw[RB + 7].toInt()); + F3_HT.update(statusResponseRaw[RB + 8].toInt()); + F3_CT.update(statusResponseRaw[RB + 9].toInt()); + F3_PU.update(statusResponseRaw[RB + 10].toInt()); + F3_VE.update(statusResponseRaw[RB + 11] == "1"); + F3_ST.update(statusResponseRaw[RB + 12].toInt()); #pragma endregion #pragma region RC //Outlet_Heater.updateValue(statusResponseRaw[]); @@ -1039,7 +1043,7 @@ void SpaInterface::updateMeasures() { Outlet_Blower.update(statusResponseRaw[RC + 10].toInt()); #pragma endregion #pragma region RE - update_HP_Present(statusResponseRaw[RE + 1]); + HP_Present.update(statusResponseRaw[RE + 1].toInt()); //HP_FlowSwitch.updateValue(statusResponseRaw[]); //HP_HighSwitch.updateValue(statusResponseRaw[]); //HP_LowSwitch.updateValue(statusResponseRaw[]); @@ -1048,27 +1052,27 @@ void SpaInterface::updateMeasures() { //HP_D1.updateValue(statusResponseRaw[]); //HP_D2.updateValue(statusResponseRaw[]); //HP_D3.updateValue(statusResponseRaw[]); - update_HP_Ambient(statusResponseRaw[RE + 10]); - update_HP_Condensor(statusResponseRaw[RE + 11]); - update_HP_Compressor_State(statusResponseRaw[RE + 12]); - update_HP_Fan_State(statusResponseRaw[RE + 13]); - update_HP_4W_Valve(statusResponseRaw[RE + 14]); - update_HP_Heater_State(statusResponseRaw[RE + 15]); - update_HP_State(statusResponseRaw[RE + 16]); - update_HP_Mode(statusResponseRaw[RE + 17]); - update_HP_Defrost_Timer(statusResponseRaw[RE + 18]); - update_HP_Comp_Run_Timer(statusResponseRaw[RE + 19]); - update_HP_Low_Temp_Timer(statusResponseRaw[RE + 20]); - update_HP_Heat_Accum_Timer(statusResponseRaw[RE + 21]); - update_HP_Sequence_Timer(statusResponseRaw[RE + 22]); - update_HP_Warning(statusResponseRaw[RE + 23]); - update_FrezTmr(statusResponseRaw[RE + 24]); - update_DBGN(statusResponseRaw[RE + 25]); - update_DEND(statusResponseRaw[RE + 26]); - update_DCMP(statusResponseRaw[RE + 27]); - update_DMAX(statusResponseRaw[RE + 28]); - update_DELE(statusResponseRaw[RE + 29]); - update_DPMP(statusResponseRaw[RE + 30]); + HP_Ambient.update(statusResponseRaw[RE + 10].toInt()); + HP_Condensor.update(statusResponseRaw[RE + 11].toInt()); + HP_Compressor_State.update(statusResponseRaw[RE + 12] == "1"); + HP_Fan_State.update(statusResponseRaw[RE + 13] == "1"); + HP_4W_Valve.update(statusResponseRaw[RE + 14] == "1"); + HP_Heater_State.update(statusResponseRaw[RE + 15] == "1"); + HP_State.update(statusResponseRaw[RE + 16].toInt()); + HP_Mode.update(statusResponseRaw[RE + 17].toInt()); + HP_Defrost_Timer.update(statusResponseRaw[RE + 18].toInt()); + HP_Comp_Run_Timer.update(statusResponseRaw[RE + 19].toInt()); + HP_Low_Temp_Timer.update(statusResponseRaw[RE + 20].toInt()); + HP_Heat_Accum_Timer.update(statusResponseRaw[RE + 21].toInt()); + HP_Sequence_Timer.update(statusResponseRaw[RE + 22].toInt()); + HP_Warning.update(statusResponseRaw[RE + 23].toInt()); + FrezTmr.update(statusResponseRaw[RE + 24].toInt()); + DBGN.update(statusResponseRaw[RE + 25].toInt()); + DEND.update(statusResponseRaw[RE + 26].toInt()); + DCMP.update(statusResponseRaw[RE + 27].toInt()); + DMAX.update(statusResponseRaw[RE + 28].toInt()); + DELE.update(statusResponseRaw[RE + 29].toInt()); + DPMP.update(statusResponseRaw[RE + 30].toInt()); //CMAX.updateValue(statusResponseRaw[]); //HP_Compressor.updateValue(statusResponseRaw[]); //HP_Pump_State.updateValue(statusResponseRaw[]); @@ -1079,16 +1083,16 @@ void SpaInterface::updateMeasures() { if (RG < 0) return; #pragma region RG - update_Pump1InstallState(statusResponseRaw[RG + 7]); - update_Pump2InstallState(statusResponseRaw[RG + 8]); - update_Pump3InstallState(statusResponseRaw[RG + 9]); - update_Pump4InstallState(statusResponseRaw[RG + 10]); - update_Pump5InstallState(statusResponseRaw[RG + 11]); - update_Pump1OkToRun(statusResponseRaw[RG + 1]); - update_Pump2OkToRun(statusResponseRaw[RG + 2]); - update_Pump3OkToRun(statusResponseRaw[RG + 3]); - update_Pump4OkToRun(statusResponseRaw[RG + 4]); - update_Pump5OkToRun(statusResponseRaw[RG + 5]); + Pump1InstallState.update(statusResponseRaw[RG + 7]); + Pump2InstallState.update(statusResponseRaw[RG + 8]); + Pump3InstallState.update(statusResponseRaw[RG + 9]); + Pump4InstallState.update(statusResponseRaw[RG + 10]); + Pump5InstallState.update(statusResponseRaw[RG + 11]); + Pump1OkToRun.update(statusResponseRaw[RG + 1] == "1"); + Pump2OkToRun.update(statusResponseRaw[RG + 2] == "1"); + Pump3OkToRun.update(statusResponseRaw[RG + 3] == "1"); + Pump4OkToRun.update(statusResponseRaw[RG + 4] == "1"); + Pump5OkToRun.update(statusResponseRaw[RG + 5] == "1"); LockMode.update(statusResponseRaw[RG + 12].toInt()); #pragma endregion diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 1389d2b..7ac6347 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -5,7 +5,9 @@ #include #include #include -#include "SpaProperties.h" +#include +#include + extern RemoteDebug Debug; #define FAILEDREADFREQUENCY 1000 //(ms) Frequency to retry on a failed read of the status registers. @@ -13,7 +15,7 @@ extern RemoteDebug Debug; template constexpr size_t array_count(const T (&)[N]) { return N; } -class SpaInterface : public SpaProperties { +class SpaInterface { private: /// @brief How often to pole the spa for updates in seconds. int _updateFrequency = 60; @@ -221,6 +223,10 @@ class SpaInterface : public SpaProperties { /// @brief Internal writer used by `RB_TP_Light` RWProperty. /// @details Sends `W14` to toggle the light; updates cached value to `mode`. bool setRB_TP_Light(int mode); + /// @brief Internal writer used by `statusResponse` RWProperty. + /// @details No-op — statusResponse is populated internally by readStatus(). + /// Allows external injection of a raw response string (e.g. for testing). + bool setStatusResponse(String s); public: /// @brief Init SpaInterface. @@ -246,6 +252,8 @@ class SpaInterface : public SpaProperties { T get() const { return _value; } operator T() const { return _value; } + void setCallback(void (*c)(T)) { _callback = c; } + void clearCallback() { _callback = nullptr; } // Returns the matching label for the current value, or fallback if not found. const char* getLabel(const char* fallback = "Unknown") const { if (!_map || _mapSize == 0) { @@ -276,11 +284,16 @@ class SpaInterface : public SpaProperties { bool _hasValue = false; const LabelValue* _map = nullptr; size_t _mapSize = 0; + void (*_callback)(T) = nullptr; // Called by SpaInterface when a fresh value is received from the spa. void update(T newValue) { + T oldValue = _value; _value = newValue; _hasValue = true; + if (_callback && oldValue != newValue) { + _callback(_value); + } } // Reverse-lookup by label string; throws std::invalid_argument if the label // is not found in the map or if no map is configured. @@ -467,12 +480,205 @@ class SpaInterface : public SpaProperties { void setSpaPollFrequency(int updateFrequency); /// @brief Complete RF command response in a single string - Property statusResponse; + RWProperty statusResponse{this, &SpaInterface::setStatusResponse}; + + const std::array autoPumpOptions = {"Manual", "Auto"}; + + /// @brief Mains voltage (V). + /// @details Read-only. + ROProperty MainsVoltage; /// @brief Mains current draw x10. /// @details Read-only; value 77 represents 7.7A. ROProperty MainsCurrent; + // R2 + ROProperty CaseTemperature; + ROProperty PortCurrent; + ROProperty HeaterTemperature; + ROProperty PoolTemperature; + ROProperty AwakeMinutesRemaining; + ROProperty FiltPumpRunTimeTotal; + ROProperty FiltPumpReqMins; + ROProperty LoadTimeOut; + ROProperty HourMeter; + ROProperty Relay1; + ROProperty Relay2; + ROProperty Relay3; + ROProperty Relay4; + ROProperty Relay5; + ROProperty Relay6; + ROProperty Relay7; + ROProperty Relay8; + ROProperty Relay9; + ROProperty WaterPresent; + // R3 + ROProperty CLMT; + ROProperty PHSE; + ROProperty LLM1; + ROProperty LLM2; + ROProperty LLM3; + ROProperty SVER; + ROProperty Model; + ROProperty SerialNo1; + ROProperty SerialNo2; + ROProperty D1; + ROProperty D2; + ROProperty D3; + ROProperty D4; + ROProperty D5; + ROProperty D6; + ROProperty Pump; + ROProperty LS; + ROProperty HV; + ROProperty SnpMR; + ROProperty Status; + ROProperty PrimeCount; + ROProperty EC; + ROProperty HAMB; + ROProperty HCON; + // R4 + ROProperty Ser1_Timer; + ROProperty Ser2_Timer; + ROProperty Ser3_Timer; + ROProperty HeatMode; + ROProperty PumpIdleTimer; + ROProperty PumpRunTimer; + ROProperty AdtPoolHys; + ROProperty AdtHeaterHys; + ROProperty Power; + ROProperty Power_kWh; + ROProperty Power_Today; + ROProperty Power_Yesterday; + ROProperty ThermalCutOut; + ROProperty Test_D1; + ROProperty Test_D2; + ROProperty Test_D3; + ROProperty ElementHeatSourceOffset; + ROProperty Frequency; + ROProperty HPHeatSourceOffset_Heat; + ROProperty HPHeatSourceOffset_Cool; + ROProperty HeatSourceOffTime; + ROProperty Vari_Speed; + ROProperty Vari_Percent; + ROProperty Vari_Mode; + // R5 + ROProperty RB_TP_Blower; + ROProperty WTMP; + ROProperty RB_TP_Auto; + ROProperty RB_TP_Heater; + ROProperty RB_TP_Ozone; + ROProperty RB_TP_Sleep; + ROProperty CleanCycle; + // R6 + ROProperty L_24HOURS; + ROProperty PSAV_LVL; + ROProperty PSAV_BGN; + ROProperty PSAV_END; + ROProperty DefaultScrn; + ROProperty TOUT; + ROProperty BRND; + ROProperty PRME; + ROProperty ELMT; + ROProperty TYPE; + ROProperty GAS; + ROProperty VPMP; + ROProperty HIFI; + // R7 + ROProperty WCLNTime; + ROProperty V_Max; + ROProperty V_Min; + ROProperty V_Max_24; + ROProperty V_Min_24; + ROProperty CurrentZero; + ROProperty CurrentAdjust; + ROProperty VoltageAdjust; + ROProperty Ser1; + ROProperty Ser2; + ROProperty Ser3; + ROProperty VMAX; + ROProperty AHYS; + ROProperty PMIN; + ROProperty PFLT; + ROProperty PHTR; + ROProperty PMAX; + ROProperty TemperatureUnits; + ROProperty OzoneOff; + ROProperty Ozone24; + ROProperty Circ24; + ROProperty CJET; + ROProperty VELE; + ROProperty HUSE; + // R9/RA/RB fault logs + ROProperty F1_HR; + ROProperty F1_Time; + ROProperty F1_ER; + ROProperty F1_I; + ROProperty F1_V; + ROProperty F1_PT; + ROProperty F1_HT; + ROProperty F1_CT; + ROProperty F1_PU; + ROProperty F1_ST; + ROProperty F1_VE; + ROProperty F2_HR; + ROProperty F2_Time; + ROProperty F2_ER; + ROProperty F2_I; + ROProperty F2_V; + ROProperty F2_PT; + ROProperty F2_HT; + ROProperty F2_CT; + ROProperty F2_PU; + ROProperty F2_ST; + ROProperty F2_VE; + ROProperty F3_HR; + ROProperty F3_Time; + ROProperty F3_ER; + ROProperty F3_I; + ROProperty F3_V; + ROProperty F3_PT; + ROProperty F3_HT; + ROProperty F3_CT; + ROProperty F3_PU; + ROProperty F3_ST; + ROProperty F3_VE; + // RE heatpump + ROProperty HP_Present; + ROProperty HP_Ambient; + ROProperty HP_Condensor; + ROProperty HP_State; + ROProperty HP_Mode; + ROProperty HP_Defrost_Timer; + ROProperty HP_Comp_Run_Timer; + ROProperty HP_Low_Temp_Timer; + ROProperty HP_Heat_Accum_Timer; + ROProperty HP_Sequence_Timer; + ROProperty HP_Warning; + ROProperty FrezTmr; + ROProperty DBGN; + ROProperty DEND; + ROProperty DCMP; + ROProperty DMAX; + ROProperty DELE; + ROProperty DPMP; + ROProperty HP_Compressor_State; + ROProperty HP_Fan_State; + ROProperty HP_4W_Valve; + ROProperty HP_Heater_State; + + // RG + ROProperty Pump1InstallState; + ROProperty Pump2InstallState; + ROProperty Pump3InstallState; + ROProperty Pump4InstallState; + ROProperty Pump5InstallState; + ROProperty Pump1OkToRun; + ROProperty Pump2OkToRun; + ROProperty Pump3OkToRun; + ROProperty Pump4OkToRun; + ROProperty Pump5OkToRun; + /// @brief Water temperature set point x10. /// @details Read/write. Valid range 50..410 (5.0C..41.0C). RWProperty STMP{this, &SpaInterface::setSTMP}; @@ -596,19 +802,15 @@ class SpaInterface : public SpaProperties { &SpaInterface::RB_TP_Pump4, &SpaInterface::RB_TP_Pump5, }; -}; - -// Define the function pointer type for getPumpInstallState functions -typedef String (SpaInterface::*GetPumpStateInstallFunction)(); - -// Declare the array of function pointers for each pump's install state as static -static GetPumpStateInstallFunction pumpInstallStateFunctions[] = { - &SpaInterface::getPump1InstallState, - &SpaInterface::getPump2InstallState, - &SpaInterface::getPump3InstallState, - &SpaInterface::getPump4InstallState, - &SpaInterface::getPump5InstallState + using PumpInstallStatePtr = ROProperty SpaInterface::*; + static constexpr PumpInstallStatePtr pumpInstallStateFunctions[] = { + &SpaInterface::Pump1InstallState, + &SpaInterface::Pump2InstallState, + &SpaInterface::Pump3InstallState, + &SpaInterface::Pump4InstallState, + &SpaInterface::Pump5InstallState, + }; }; diff --git a/lib/SpaInterface/SpaProperties.cpp b/lib/SpaInterface/SpaProperties.cpp deleted file mode 100644 index 8a4cfb9..0000000 --- a/lib/SpaInterface/SpaProperties.cpp +++ /dev/null @@ -1,755 +0,0 @@ -#include "SpaProperties.h" - - -static boolean updateIntProperty(Property& prop, const String& s) { - // Accept only base-10 integers with no trailing characters. - const char* raw = s.c_str(); - char* end = nullptr; - long value = strtol(raw, &end, 10); - if (raw == end || *end != '\0') { - return false; - } - - prop.update_Value(static_cast(value)); - return true; -} - -static boolean updateBool01Property(Property& prop, const String& s) { - // Accept only "0" or "1" to avoid accidental truthy values. - if (s!="0" && s!="1") { - return false; - } - - prop.update_Value(s == "1"); - return true; -} - -static boolean updateStringProperty(Property& prop, const String& s) { - // Store raw string values without validation. - prop.update_Value(s); - return true; -} - -static boolean updateTriStateProperty(Property& prop, const String& s) { - // Accept only "0", "1", or "2" for explicit tri-state fields. - if (s!="0" && s!="1" && s!="2") { - return false; - } - - prop.update_Value(s.toInt()); - return true; -} - -boolean SpaProperties::update_MainsVoltage(const String& s){ - return updateIntProperty(MainsVoltage, s); -} - -boolean SpaProperties::update_CaseTemperature(const String& s){ - return updateIntProperty(CaseTemperature, s); -} - -boolean SpaProperties::update_PortCurrent(const String& s){ - return updateIntProperty(PortCurrent, s); -} - -boolean SpaProperties::update_HeaterTemperature(const String& s){ - return updateIntProperty(HeaterTemperature, s); -} - -boolean SpaProperties::update_PoolTemperature(const String& s){ - return updateIntProperty(PoolTemperature, s); -} - -boolean SpaProperties::update_WaterPresent(const String& s) { - return updateBool01Property(WaterPresent, s); -} - -boolean SpaProperties::update_AwakeMinutesRemaining(const String& s){ - return updateIntProperty(AwakeMinutesRemaining, s); -} - -boolean SpaProperties::update_FiltPumpRunTimeTotal(const String& s){ - return updateIntProperty(FiltPumpRunTimeTotal, s); -} - -boolean SpaProperties::update_FiltPumpReqMins(const String& s){ - return updateIntProperty(FiltPumpReqMins, s); -} - -boolean SpaProperties::update_LoadTimeOut(const String& s){ - return updateIntProperty(LoadTimeOut, s); -} - -boolean SpaProperties::update_HourMeter(const String& s){ - return updateIntProperty(HourMeter, s); -} - -boolean SpaProperties::update_Relay1(const String& s){ - return updateIntProperty(Relay1, s); -} - -boolean SpaProperties::update_Relay2(const String& s){ - return updateIntProperty(Relay2, s); -} - -boolean SpaProperties::update_Relay3(const String& s){ - return updateIntProperty(Relay3, s); -} - -boolean SpaProperties::update_Relay4(const String& s){ - return updateIntProperty(Relay4, s); -} - -boolean SpaProperties::update_Relay5(const String& s){ - return updateIntProperty(Relay5, s); -} - -boolean SpaProperties::update_Relay6(const String& s){ - return updateIntProperty(Relay6, s); -} - -boolean SpaProperties::update_Relay7(const String& s){ - return updateIntProperty(Relay7, s); -} - -boolean SpaProperties::update_Relay8(const String& s){ - return updateIntProperty(Relay8, s); -} - -boolean SpaProperties::update_Relay9(const String& s){ - return updateIntProperty(Relay9, s); -} - -boolean SpaProperties::update_CLMT(const String& s){ - return updateIntProperty(CLMT, s); -} - -boolean SpaProperties::update_PHSE(const String& s){ - return updateIntProperty(PHSE, s); -} - -boolean SpaProperties::update_LLM1(const String& s){ - return updateIntProperty(LLM1, s); -} - -boolean SpaProperties::update_LLM2(const String& s){ - return updateIntProperty(LLM2, s); -} - -boolean SpaProperties::update_LLM3(const String& s){ - return updateIntProperty(LLM3, s); -} - -boolean SpaProperties::update_SVER(const String& s){ - return updateStringProperty(SVER, s); -} - -boolean SpaProperties::update_Model(const String& s){ - return updateStringProperty(Model, s); -} - -boolean SpaProperties::update_SerialNo1(const String& s){ - return updateStringProperty(SerialNo1, s); -} - -boolean SpaProperties::update_SerialNo2(const String& s){ - return updateStringProperty(SerialNo2, s); -} - -boolean SpaProperties::update_D1(const String& s) { - return updateBool01Property(D1, s); -} - -boolean SpaProperties::update_D2(const String& s) { - return updateBool01Property(D2, s); -} - -boolean SpaProperties::update_D3(const String& s) { - return updateBool01Property(D3, s); -} - -boolean SpaProperties::update_D4(const String& s) { - return updateBool01Property(D4, s); -} - -boolean SpaProperties::update_D5(const String& s) { - return updateBool01Property(D5, s); -} - -boolean SpaProperties::update_D6(const String& s) { - return updateBool01Property(D6, s); -} - -boolean SpaProperties::update_Pump(const String& s){ - return updateStringProperty(Pump, s); -} - -boolean SpaProperties::update_LS(const String& s){ - return updateIntProperty(LS, s); -} - -boolean SpaProperties::update_HV(const String& s) { - return updateBool01Property(HV, s); -} - -boolean SpaProperties::update_SnpMR(const String& s){ - return updateIntProperty(SnpMR, s); -} - -boolean SpaProperties::update_Status(const String& s){ - return updateStringProperty(Status, s); -} - -boolean SpaProperties::update_PrimeCount(const String& s){ - return updateIntProperty(PrimeCount, s); -} - -boolean SpaProperties::update_EC(const String& s){ - return updateIntProperty(EC, s); -} - -boolean SpaProperties::update_HAMB(const String& s){ - return updateIntProperty(HAMB, s); -} - -boolean SpaProperties::update_HCON(const String& s){ - return updateIntProperty(HCON, s); -} - -boolean SpaProperties::update_Ser1_Timer(const String& s){ - return updateIntProperty(Ser1_Timer, s); -} - -boolean SpaProperties::update_Ser2_Timer(const String& s){ - return updateIntProperty(Ser2_Timer, s); -} - -boolean SpaProperties::update_Ser3_Timer(const String& s){ - return updateIntProperty(Ser3_Timer, s); -} - -boolean SpaProperties::update_HeatMode(const String& s){ - return updateIntProperty(HeatMode, s); -} - -boolean SpaProperties::update_PumpIdleTimer(const String& s){ - return updateIntProperty(PumpIdleTimer, s); -} - -boolean SpaProperties::update_PumpRunTimer(const String& s){ - return updateIntProperty(PumpRunTimer, s); -} - -boolean SpaProperties::update_AdtPoolHys(const String& s){ - return updateIntProperty(AdtPoolHys, s); -} - -boolean SpaProperties::update_AdtHeaterHys(const String& s){ - return updateIntProperty(AdtHeaterHys, s); -} - -boolean SpaProperties::update_Power(const String& s){ - return updateIntProperty(Power, s); -} - -boolean SpaProperties::update_Power_kWh(const String& s){ - return updateIntProperty(Power_kWh, s); -} - -boolean SpaProperties::update_Power_Today(const String& s){ - return updateIntProperty(Power_Today, s); -} - -boolean SpaProperties::update_Power_Yesterday(const String& s){ - return updateIntProperty(Power_Yesterday, s); -} - -boolean SpaProperties::update_ThermalCutOut(const String& s){ - return updateIntProperty(ThermalCutOut, s); -} - -boolean SpaProperties::update_Test_D1(const String& s){ - return updateIntProperty(Test_D1, s); -} - -boolean SpaProperties::update_Test_D2(const String& s){ - return updateIntProperty(Test_D2, s); -} - -boolean SpaProperties::update_Test_D3(const String& s){ - return updateIntProperty(Test_D3, s); -} - -boolean SpaProperties::update_ElementHeatSourceOffset(const String& s){ - return updateIntProperty(ElementHeatSourceOffset, s); -} - -boolean SpaProperties::update_Frequency(const String& s){ - return updateIntProperty(Frequency, s); -} - -boolean SpaProperties::update_HPHeatSourceOffset_Heat(const String& s){ - return updateIntProperty(HPHeatSourceOffset_Heat, s); -} - -boolean SpaProperties::update_HPHeatSourceOffset_Cool(const String& s){ - return updateIntProperty(HPHeatSourceOffset_Cool, s); -} - -boolean SpaProperties::update_HeatSourceOffTime(const String& s){ - return updateIntProperty(HeatSourceOffTime, s); -} - -boolean SpaProperties::update_Vari_Speed(const String& s){ - return updateIntProperty(Vari_Speed, s); -} - -boolean SpaProperties::update_Vari_Percent(const String& s){ - return updateIntProperty(Vari_Percent, s); -} - -boolean SpaProperties::update_Vari_Mode(const String& s){ - return updateIntProperty(Vari_Mode, s); -} - -boolean SpaProperties::update_RB_TP_Blower(const String& s){ - return updateIntProperty(RB_TP_Blower, s); -} - - -boolean SpaProperties::update_RB_TP_Auto(const String& s) { - return updateBool01Property(RB_TP_Auto, s); -} - -boolean SpaProperties::update_RB_TP_Heater(const String& s) { - return updateBool01Property(RB_TP_Heater, s); -} - -boolean SpaProperties::update_RB_TP_Ozone(const String& s) { - return updateBool01Property(RB_TP_Ozone, s); -} - -boolean SpaProperties::update_RB_TP_Sleep(const String& s) { - return updateBool01Property(RB_TP_Sleep, s); -} - -boolean SpaProperties::update_WTMP(const String& s){ - return updateIntProperty(WTMP, s); -} - -boolean SpaProperties::update_CleanCycle(const String& s) { - return updateBool01Property(CleanCycle, s); -} - -boolean SpaProperties::update_L_24HOURS(const String& s){ - return updateIntProperty(L_24HOURS, s); -} - -boolean SpaProperties::update_PSAV_LVL(const String& s){ - return updateIntProperty(PSAV_LVL, s); -} - -boolean SpaProperties::update_PSAV_BGN(const String& s){ - return updateIntProperty(PSAV_BGN, s); -} - -boolean SpaProperties::update_PSAV_END(const String& s){ - return updateIntProperty(PSAV_END, s); -} - -boolean SpaProperties::update_DefaultScrn(const String& s){ - return updateIntProperty(DefaultScrn, s); -} - -boolean SpaProperties::update_TOUT(const String& s){ - return updateIntProperty(TOUT, s); -} - -boolean SpaProperties::update_VPMP(const String& s) { - return updateBool01Property(VPMP, s); -} - -boolean SpaProperties::update_HIFI(const String& s) { - return updateBool01Property(HIFI, s); -} - -boolean SpaProperties::update_BRND(const String& s){ - return updateIntProperty(BRND, s); -} - -boolean SpaProperties::update_PRME(const String& s){ - return updateIntProperty(PRME, s); -} - -boolean SpaProperties::update_ELMT(const String& s){ - return updateIntProperty(ELMT, s); -} - -boolean SpaProperties::update_TYPE(const String& s){ - return updateIntProperty(TYPE, s); -} - -boolean SpaProperties::update_GAS(const String& s){ - return updateIntProperty(GAS, s); -} - -boolean SpaProperties::update_WCLNTime(const String& s){ - return updateIntProperty(WCLNTime, s); -} - -boolean SpaProperties::update_TemperatureUnits(const String& s) { - return updateBool01Property(TemperatureUnits, s); -} - -boolean SpaProperties::update_OzoneOff(const String& s) { - return updateBool01Property(OzoneOff, s); -} - -boolean SpaProperties::update_Ozone24(const String& s) { - return updateBool01Property(Ozone24, s); -} - -boolean SpaProperties::update_Circ24(const String& s) { - return updateBool01Property(Circ24, s); -} - -boolean SpaProperties::update_CJET(const String& s) { - return updateBool01Property(CJET, s); -} - -boolean SpaProperties::update_VELE(const String& s) { - return updateBool01Property(VELE, s); -} - -boolean SpaProperties::update_V_Max(const String& s){ - return updateIntProperty(V_Max, s); -} - -boolean SpaProperties::update_V_Min(const String& s){ - return updateIntProperty(V_Min, s); -} - -boolean SpaProperties::update_V_Max_24(const String& s){ - return updateIntProperty(V_Max_24, s); -} - -boolean SpaProperties::update_V_Min_24(const String& s){ - return updateIntProperty(V_Min_24, s); -} - -boolean SpaProperties::update_CurrentZero(const String& s){ - return updateIntProperty(CurrentZero, s); -} - -boolean SpaProperties::update_CurrentAdjust(const String& s){ - return updateIntProperty(CurrentAdjust, s); -} - -boolean SpaProperties::update_VoltageAdjust(const String& s){ - return updateIntProperty(VoltageAdjust, s); -} - -boolean SpaProperties::update_Ser1(const String& s){ - return updateIntProperty(Ser1, s); -} - -boolean SpaProperties::update_Ser2(const String& s){ - return updateIntProperty(Ser2, s); -} - -boolean SpaProperties::update_Ser3(const String& s){ - return updateIntProperty(Ser3, s); -} - -boolean SpaProperties::update_VMAX(const String& s){ - return updateIntProperty(VMAX, s); -} - -boolean SpaProperties::update_AHYS(const String& s){ - return updateIntProperty(AHYS, s); -} - -boolean SpaProperties::update_HUSE(const String& s){ - // HUSE is a bool; only accept 0/1. - return updateBool01Property(HUSE, s); -} - -boolean SpaProperties::update_PMIN(const String& s){ - return updateIntProperty(PMIN, s); -} - -boolean SpaProperties::update_PFLT(const String& s){ - return updateIntProperty(PFLT, s); -} - -boolean SpaProperties::update_PHTR(const String& s){ - return updateIntProperty(PHTR, s); -} - -boolean SpaProperties::update_PMAX(const String& s){ - return updateIntProperty(PMAX, s); -} - -boolean SpaProperties::update_F1_HR(const String& s){ - return updateIntProperty(F1_HR, s); -} - -boolean SpaProperties::update_F1_Time(const String& s){ - return updateIntProperty(F1_Time, s); -} - -boolean SpaProperties::update_F1_ER(const String& s){ - return updateIntProperty(F1_ER, s); -} - -boolean SpaProperties::update_F1_I(const String& s){ - return updateIntProperty(F1_I, s); -} - -boolean SpaProperties::update_F1_V(const String& s){ - return updateIntProperty(F1_V, s); -} - -boolean SpaProperties::update_F1_PT(const String& s){ - return updateIntProperty(F1_PT, s); -} - -boolean SpaProperties::update_F1_HT(const String& s){ - return updateIntProperty(F1_HT, s); -} - -boolean SpaProperties::update_F1_CT(const String& s){ - return updateIntProperty(F1_CT, s); -} - -boolean SpaProperties::update_F1_ST(const String& s){ - return updateIntProperty(F1_ST, s); -} - -boolean SpaProperties::update_F1_PU(const String& s){ - return updateIntProperty(F1_PU, s); -} - -boolean SpaProperties::update_F1_VE(const String& s) { - return updateBool01Property(F1_VE, s); -} - -boolean SpaProperties::update_F2_HR(const String& s){ - return updateIntProperty(F2_HR, s); -} - -boolean SpaProperties::update_F2_Time(const String& s){ - return updateIntProperty(F2_Time, s); -} - -boolean SpaProperties::update_F2_ER(const String& s){ - return updateIntProperty(F2_ER, s); -} - -boolean SpaProperties::update_F2_I(const String& s){ - return updateIntProperty(F2_I, s); -} - -boolean SpaProperties::update_F2_V(const String& s){ - return updateIntProperty(F2_V, s); -} - -boolean SpaProperties::update_F2_PT(const String& s){ - return updateIntProperty(F2_PT, s); -} - -boolean SpaProperties::update_F2_HT(const String& s){ - return updateIntProperty(F2_HT, s); -} - -boolean SpaProperties::update_F2_CT(const String& s){ - return updateIntProperty(F2_CT, s); -} - -boolean SpaProperties::update_F2_ST(const String& s){ - return updateIntProperty(F2_ST, s); -} - -boolean SpaProperties::update_F2_PU(const String& s){ - return updateIntProperty(F2_PU, s); -} - -boolean SpaProperties::update_F2_VE(const String& s) { - return updateBool01Property(F2_VE, s); -} - -boolean SpaProperties::update_F3_HR(const String& s){ - return updateIntProperty(F3_HR, s); -} - -boolean SpaProperties::update_F3_Time(const String& s){ - return updateIntProperty(F3_Time, s); -} - -boolean SpaProperties::update_F3_ER(const String& s){ - return updateIntProperty(F3_ER, s); -} - -boolean SpaProperties::update_F3_I(const String& s){ - return updateIntProperty(F3_I, s); -} - -boolean SpaProperties::update_F3_V(const String& s){ - return updateIntProperty(F3_V, s); -} - -boolean SpaProperties::update_F3_PT(const String& s){ - return updateIntProperty(F3_PT, s); -} - -boolean SpaProperties::update_F3_HT(const String& s){ - return updateIntProperty(F3_HT, s); -} - -boolean SpaProperties::update_F3_CT(const String& s){ - return updateIntProperty(F3_CT, s); -} - -boolean SpaProperties::update_F3_ST(const String& s){ - return updateIntProperty(F3_ST, s); -} - -boolean SpaProperties::update_F3_PU(const String& s){ - return updateIntProperty(F3_PU, s); -} - -boolean SpaProperties::update_F3_VE(const String& s) { - return updateBool01Property(F3_VE, s); -} - -boolean SpaProperties::update_HP_Present(const String& s){ - return updateIntProperty(HP_Present, s); -} - -boolean SpaProperties::update_HP_Ambient(const String& s){ - return updateIntProperty(HP_Ambient, s); -} - - -boolean SpaProperties::update_HP_Condensor(const String& s){ - return updateIntProperty(HP_Condensor, s); -} - -boolean SpaProperties::update_HP_Compressor_State(const String& s) { - return updateBool01Property(HP_Compressor_State, s); -} - -boolean SpaProperties::update_HP_Fan_State(const String& s) { - return updateBool01Property(HP_Fan_State, s); -} - -boolean SpaProperties::update_HP_4W_Valve(const String& s) { - return updateBool01Property(HP_4W_Valve, s); -} - -boolean SpaProperties::update_HP_Heater_State(const String& s) { - return updateBool01Property(HP_Heater_State, s); -} - - -boolean SpaProperties::update_HP_State(const String& s){ - return updateIntProperty(HP_State, s); -} - -boolean SpaProperties::update_HP_Mode(const String& s){ - return updateIntProperty(HP_Mode, s); -} - -boolean SpaProperties::update_HP_Defrost_Timer(const String& s){ - return updateIntProperty(HP_Defrost_Timer, s); -} - -boolean SpaProperties::update_HP_Comp_Run_Timer(const String& s){ - return updateIntProperty(HP_Comp_Run_Timer, s); -} - -boolean SpaProperties::update_HP_Low_Temp_Timer(const String& s){ - return updateIntProperty(HP_Low_Temp_Timer, s); -} - -boolean SpaProperties::update_HP_Heat_Accum_Timer(const String& s){ - return updateIntProperty(HP_Heat_Accum_Timer, s); -} - -boolean SpaProperties::update_HP_Sequence_Timer(const String& s){ - return updateIntProperty(HP_Sequence_Timer, s); -} - -boolean SpaProperties::update_HP_Warning(const String& s){ - return updateIntProperty(HP_Warning, s); -} - -boolean SpaProperties::update_FrezTmr(const String& s){ - return updateIntProperty(FrezTmr, s); -} - -boolean SpaProperties::update_DBGN(const String& s){ - return updateIntProperty(DBGN, s); -} - -boolean SpaProperties::update_DEND(const String& s){ - return updateIntProperty(DEND, s); -} - -boolean SpaProperties::update_DCMP(const String& s){ - return updateIntProperty(DCMP, s); -} - -boolean SpaProperties::update_DMAX(const String& s){ - return updateIntProperty(DMAX, s); -} - -boolean SpaProperties::update_DELE(const String& s){ - return updateIntProperty(DELE, s); -} - -boolean SpaProperties::update_DPMP(const String& s){ - return updateIntProperty(DPMP, s); -} - -boolean SpaProperties::update_Pump1InstallState(const String& s){ - return updateStringProperty(Pump1InstallState, s); -} - -boolean SpaProperties::update_Pump2InstallState(const String& s){ - return updateStringProperty(Pump2InstallState, s); -} - -boolean SpaProperties::update_Pump3InstallState(const String& s){ - return updateStringProperty(Pump3InstallState, s); -} - -boolean SpaProperties::update_Pump4InstallState(const String& s){ - return updateStringProperty(Pump4InstallState, s); -} - -boolean SpaProperties::update_Pump5InstallState(const String& s){ - return updateStringProperty(Pump5InstallState, s); -} - -boolean SpaProperties::update_Pump1OkToRun(const String& s) { - return updateBool01Property(Pump1OkToRun, s); -} - -boolean SpaProperties::update_Pump2OkToRun(const String& s) { - return updateBool01Property(Pump2OkToRun, s); -} - -boolean SpaProperties::update_Pump3OkToRun(const String& s) { - return updateBool01Property(Pump3OkToRun, s); -} - -boolean SpaProperties::update_Pump4OkToRun(const String& s) { - return updateBool01Property(Pump4OkToRun, s); -} - -boolean SpaProperties::update_Pump5OkToRun(const String& s) { - return updateBool01Property(Pump5OkToRun, s); -} - - diff --git a/lib/SpaInterface/SpaProperties.h b/lib/SpaInterface/SpaProperties.h deleted file mode 100644 index b16c62c..0000000 --- a/lib/SpaInterface/SpaProperties.h +++ /dev/null @@ -1,1386 +0,0 @@ -#ifndef SPAPROPERTIES_H -#define SPAPROPERTIES_H - -#include -#include -#include -#include -#include - - -template -// Simple value holder with an optional change callback. -class Property -{ -private: - T _value; - void (*_callback)(T) = nullptr; - -public: - T getValue() { return _value; } - void update_Value(T newval) - { - T oldvalue = _value; - _value = newval; - // Only notify on actual value change. - if ((_callback) && (oldvalue != newval)) - { - _callback(_value); - } - }; - void setCallback(void (*c)(T)) { _callback = c; }; - void clearCallback() { _callback = nullptr; }; -}; - -/// @brief represents the properties of the spa. -class SpaProperties -{ - -private: - -#pragma region R2 - /// @brief Mains voltage (V) - Property MainsVoltage; - /// @brief Internal case temperature ('C) - Property CaseTemperature; - /// @brief 12v port current (mA) - Property PortCurrent; - - /// @brief Heater temperature ('C) - Property HeaterTemperature; - /// @brief Pool temperature ('C). Note this seems to return rubbish most of the time. - Property PoolTemperature; - /// @brief Water present - Property WaterPresent; - /// @brief AwakeMinutesRemaining (min) - Property AwakeMinutesRemaining; - /// @brief FiltPumpRunTimeTotal (min) - Property FiltPumpRunTimeTotal; - /// @brief FiltPumpReqMins - // TODO - the value here does not match the value in the snampshot data (1442 != 2:00) - Property FiltPumpReqMins; - /// @brief LoadTimeOut (sec) - Property LoadTimeOut; - /// @brief HourMeter (hours) - Property HourMeter; - /// @brief Relay1 (?) - Property Relay1; - /// @brief Relay2 (?) - Property Relay2; - /// @brief Relay3 (?) - Property Relay3; - /// @brief Relay4 (?) - Property Relay4; - /// @brief Relay5 (?) - Property Relay5; - /// @brief Relay6 (?) - Property Relay6; - /// @brief Relay7 (?) - Property Relay7; - /// @brief Relay8 (?) - Property Relay8; - /// @brief Relay9 (?) - Property Relay9; -#pragma endregion -#pragma region R3 - // R3 - /// @brief Current limit (A) - Property CLMT; - /// @brief Power phases in use - Property PHSE; - /// @brief Load limit - Phase 1 - /// - /// Number of services that can be active on this phase before the keypad stops new services from starting. - /// See SV-Series-OEM-Install-Manual.pdf page 20. - Property LLM1; - /// @brief Load limit - Phase 2 - /// - /// Number of services that can be active on this phase before the keypad stops new services from starting. - /// See SV-Series-OEM-Install-Manual.pdf page 20. - Property LLM2; - /// @brief Load limit - Phase 3 - /// - /// Number of services that can be active on this phase before the keypad stops new services from starting. - /// See SV-Series-OEM-Install-Manual.pdf page 20. - Property LLM3; - /// @brief Software version - Property SVER; - /// @brief Model - Property Model; - /// @brief SerialNo1 - Property SerialNo1; - /// @brief SerialNo2 - Property SerialNo2; - /// @brief Dipswitch 1 - Property D1; - /// @brief Dipswitch 2 - Property D2; - /// @brief Dipswitch 3 - Property D3; - /// @brief Dipswitch 4 - Property D4; - /// @brief Dipswitch 5 - Property D5; - /// @brief Dipswitch 6 - Property D6; - /// @brief Pump - Property Pump; - /// @brief Load shed count - /// - /// Number of services active at which the heater will turn itself off. - Property LS; - /// @brief HV - Property HV; - /// @brief MR / name clash with MR constant from specreg.h - Property SnpMR; - /// @brief Status (Filtering, etc) - Property Status; - /// @brief PrimeCount - Property PrimeCount; - /// @brief Heat element current draw (A) - Property EC; - /// @brief HAMB - Property HAMB; - /// @brief HCON - Property HCON; -// Unclear encoding of HV_2 -/// @brief HV_2 -Property HV_2; -// -#pragma endregion -#pragma region R4 - // R4 - /// @brief Service Timer 1 (wks) 0 = off - Property Ser1_Timer; - /// @brief Service Timer 2 (wks) 0 = off - Property Ser2_Timer; - /// @brief Service Timer 3 (wks) 0 = off - Property Ser3_Timer; - /// @brief Heat mode - /// - /// 1 = Standby - /// 2 = HeatMix - Property HeatMode; - /// @brief Pump idle time (sec) - Property PumpIdleTimer; - /// @brief Pump run time (sec) - Property PumpRunTimer; - /// @brief Pool temperature adaptive hysteresis - Property AdtPoolHys; - /// @brief Heater temperature adaptive hysteresis - Property AdtHeaterHys; - /// @brief Power consumtion * 10 - Property Power; - Property Power_kWh; - // (kWh) - Property Power_Today; - // (kWh) - Property Power_Yesterday; - // 0 = ok - Property ThermalCutOut; - Property Test_D1; - Property Test_D2; - Property Test_D3; - Property ElementHeatSourceOffset; - Property Frequency; - Property HPHeatSourceOffset_Heat; - // 100 = 0!? - Property HPHeatSourceOffset_Cool; - Property HeatSourceOffTime; - Property Vari_Speed; - Property Vari_Percent; - // 5 = Filt - // 4 = Off - /// @brief Varible speed mode - /// - /// 5 = Filtering, 4 = Off - Property Vari_Mode; -#pragma endregion -#pragma region R5 - // R5 - // Unknown encoding - Attribute TouchPad2; - // Unknown encoding - Attribute TouchPad1; - Property RB_TP_Blower; - - /// @brief Auto enabled - /// - /// True when auto enabled - Property RB_TP_Auto; - /// @brief Heating running - /// - /// True when heating/cooling active - Property RB_TP_Heater; - /// @brief Cleaning (UV/Ozone running) - /// - /// True when Ozone/UV is cleaning spa. - Property RB_TP_Ozone; - /// @brief Sleeping - /// - /// True when spa is sleeping due to sleep timer - Property RB_TP_Sleep; - /// @brief Water temperature ('C) - Property WTMP; - /// @brief Clean cycle running - /// - /// True when a clean cycle is running - Property CleanCycle; -#pragma endregion -#pragma region R6 - // R6 -/// @brief Water temperature set point ('C) - // 1 = 12 hrs - Property L_24HOURS; - /// @brief Power save level - /// - /// 0 = off, 1 = low, 2 = high - Property PSAV_LVL; - /// @brief Peak power start time - /// - /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) - Property PSAV_BGN; - /// @brief Peak power end time - /// - /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) - Property PSAV_END; - - /// @brief Sleep time 1 start time - /// - /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) - Property L_1SNZ_BGN; - /// @brief Sleep time 2 start time - /// - /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) - Property L_2SNZ_BGN; - /// @brief Sleep time 1 end time - /// - /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) - Property L_1SNZ_END; - /// @brief Sleep time 1 end time - /// - /// Formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) - Property L_2SNZ_END; - /// @brief Default screen for control panels - /// - /// 0 = WTPM - Property DefaultScrn; - /// @brief Time out duration (min) - /// - /// Time in min before pump and blower time out (min 10, max 30) - Property TOUT; - Property VPMP; - Property HIFI; - /// @brief BRND - /// - /// 2 = VORT - Property BRND; - /// @brief PRME - /// - /// 0 = 10secF - Property PRME; - Property ELMT; - /// @brief TYPE - /// - /// 3 = SV3 - Property TYPE; - Property GAS; -#pragma endregion -#pragma region R7 - // R7 - /// @brief Daily clean cycle start time - /// - /// Time with the formula h*256+m (ie: for 20:00, integer will be 20*256+0 = 5120; for 13:47, integer will be 13*256+47 = 3375) - Property WCLNTime; - /// @brief Use 'F instead of 'C as the temp. UMO - Property TemperatureUnits; - Property OzoneOff; - /// @brief Sanitiser 24 hrs - /// - /// True if sanitiser (ozone) power outlet on permanently, false if automatically controlled. - /// See SV-Series-OEM-Install-Manual.pdf page 20. - Property Ozone24; - /// @brief Circulation pump 24hrs - /// - /// True if circulation pump is always on, false if automatically controlled. - /// See SV-Series-OEM-Install-Manual.pdf page 20. - Property Circ24; - Property CJET; - /// @brief Variable heat element operation - /// - /// If true allows variable power to be fed to the heating element. - /// See SV-Series-OEM-Install-Manual.pdf page 19. - Property VELE; - - /// TODO #2 - Not Implemented - /// @brief Date of comissioning - /// Property ComissionDate; - - /// @brief Highest voltage ever recorded (V) - Property V_Max; - /// @brief Lowest voltage ever recorded (V) - Property V_Min; - /// @brief Highest voltage in past 24 hrs (V) - Property V_Max_24; - /// @brief Lowest voltage in past 24 hrs (V) - Property V_Min_24; - Property CurrentZero; - Property CurrentAdjust; - Property VoltageAdjust; - Property Ser1; - Property Ser2; - Property Ser3; - /// @brief Variable heat element max power (A) - /// - /// Maximum current that the heat element is allowed to draw (between 3 and 25A) - /// See SV-Series-OEM-Install-Manual.pdf page 19. - Property VMAX; - /// @brief Adaptive Hysteresis - /// - /// Maximum adaptive hysteresis value (0=disabled). See SV-Series-OEM-Install-Manual.pdf page 20. - Property AHYS; - /// @brief HUSE - /// - /// 1 = Off - Property HUSE; - /// @brief Varible pump minimum speed setting - /// - /// Min 20%, Max 100% - /// See SV-Series-OEM-Install-Manual.pdf page 21. - Property PMIN; - /// @brief Variable pump filtration speed setting - /// - /// Min 20%, Max 100% - /// See SV-Series-OEM-Install-Manual.pdf page 21. - Property PFLT; - /// @brief Varible pump heater speed setting - /// - /// Min 20%, Max 100% - /// See SV-Series-OEM-Install-Manual.pdf page 21. - Property PHTR; - /// @brief Varible pump maximum speed setting - /// - /// Maximum speed the varible pump will run at. - /// Min 20%, Max 100% - Property PMAX; -#pragma endregion -#pragma region R9 - // R9 - /// @brief Fault runtime occurance (hrs) - Property F1_HR; - /// @brief Fault time of day occurance - Property F1_Time; - /// @brief Fault error codes - /// - /// 6 = ER612VOverload - High current detected on 12v line - Property F1_ER; - /// @brief Supply current draw at time of error (A) - Property F1_I; - /// @brief Supply voltage at time of error (V) - Property F1_V; - /// @brief Pool temperature at time of error ('C) - Property F1_PT; - /// @brief Heater temperature at time of error ('C) - Property F1_HT; - Property F1_CT; - Property F1_PU; - Property F1_VE; - /// @brief Heater setpoint at time of error ('C) - Property F1_ST; -#pragma endregion -#pragma region RA - // RA - /// @brief Fault runtime occurance (hrs) - Property F2_HR; - /// @brief Fault time of day occurance - Property F2_Time; - /// @brief Fault error codes - /// - /// 6 = ER612VOverload - High current detected on 12v line - Property F2_ER; - /// @brief Supply current draw at time of error (A) - Property F2_I; - /// @brief Supply voltage at time of error (V) - Property F2_V; - /// @brief Pool temperature at time of error ('C) - Property F2_PT; - /// @brief Heater temperature at time of error ('C) - Property F2_HT; - Property F2_CT; - Property F2_PU; - Property F2_VE; - /// @brief Heater setpoint at time of error ('C) - Property F2_ST; -#pragma endregion -#pragma region RB - // RB - /// @brief Fault runtime occurance (hrs) - Property F3_HR; - /// @brief Fault time of day occurance - Property F3_Time; - /// @brief Fault error codes - /// - /// 6 = ER612VOverload - High current detected on 12v line - Property F3_ER; - /// @brief Supply current draw at time of error (A) - Property F3_I; - /// @brief Supply voltage at time of error (V) - Property F3_V; - /// @brief Pool temperature at time of error ('C) - Property F3_PT; - /// @brief Heater temperature at time of error ('C) - Property F3_HT; - Property F3_CT; - Property F3_PU; - Property F3_VE; - /// @brief Heater setpoint at time of error ('C) - Property F3_ST; -#pragma endregion -#pragma region RC - // RC - // Encoding of the RC registers is not obvious - // Attribute Outlet_Heater; - // Attribute Outlet_Circ; - // Attribute Outlet_Sanitise; - // Attribute Outlet_Pump1; - // Attribute Outlet_Pump2; - // Attribute Outlet_Pump4; - // Attribute Outlet_Pump5; -#pragma endregion -#pragma region RE - // RE - /// @brief Heatpump installed / interface version - Property HP_Present; - // Encoding of these registers is not clear - // Attribute HP_FlowSwitch; - // Attribute HP_HighSwitch; - // Attribute HP_LowSwitch; - // Attribute HP_CompCutOut; - // Attribute HP_ExCutOut; - // Attribute HP_D1; - // Attribute HP_D2; - // Attribute HP_D3; - /// @brief Ambient air temperature ('C) - Property HP_Ambient; - /// @brief Compressor temperature ('C) - Property HP_Condensor; - /// @brief Compressor running - Property HP_Compressor_State; - /// @brief Fan running - Property HP_Fan_State; - Property HP_4W_Valve; - Property HP_Heater_State; - /// @brief Heatpump state - /// - /// 0 = Standby - Property HP_State; - /// @brief Heatpump mode - /// - /// 1 = Heat - Property HP_Mode; - Property HP_Defrost_Timer; - Property HP_Comp_Run_Timer; - Property HP_Low_Temp_Timer; - Property HP_Heat_Accum_Timer; - Property HP_Sequence_Timer; - Property HP_Warning; - Property FrezTmr; - Property DBGN; - Property DEND; - Property DCMP; - Property DMAX; - Property DELE; - Property DPMP; -// Attribute CMAX; -// Attribute HP_Compressor; -// Attribute HP_Pump_State; -// Attribute HP_Status; -#pragma endregion -#pragma region RG - /// @brief Pump 1 install state - /// - /// (eg 1-1-014) First part (1- or 0-) indicates whether the pump is installed/fitted. If so (1- - /// means it is), the second part (1- above) indicates it's speed type. The third - /// part (014 above) represents it's possible states (0 OFF, 1 ON, 4 AUTO) - Property Pump1InstallState; - /// @brief Pump 2 install state - /// - /// (eg 1-1-014) First part (1- or 0-) indicates whether the pump is installed/fitted. If so (1- - /// means it is), the second part (1- above) indicates it's speed type. The third - /// part (014 above) represents it's possible states (0 OFF, 1 ON, 4 AUTO) - Property Pump2InstallState; - /// @brief Pump 3 install state - /// - /// (eg 1-1-014) First part (1- or 0-) indicates whether the pump is installed/fitted. If so (1- - /// means it is), the second part (1- above) indicates it's speed type. The third - /// part (014 above) represents it's possible states (0 OFF, 1 ON, 4 AUTO) - Property Pump3InstallState; - /// @brief Pump 4 install state - /// - /// (eg 1-1-014) First part (1- or 0-) indicates whether the pump is installed/fitted. If so (1- - /// means it is), the second part (1- above) indicates it's speed type. The third - /// part (014 above) represents it's possible states (0 OFF, 1 ON, 4 AUTO) - Property Pump4InstallState; - /// @brief Pump 5 install state - /// - /// (eg 1-1-014) First part (1- or 0-) indicates whether the pump is installed/fitted. If so (1- - /// means it is), the second part (1- above) indicates it's speed type. The third - /// part (014 above) represents it's possible states (0 OFF, 1 ON, 4 AUTO) - Property Pump5InstallState; - /// @brief Pump 1 is in safe state to start - Property Pump1OkToRun; - /// @brief Pump 2 is in safe state to start - Property Pump2OkToRun; - /// @brief Pump 3 is in safe state to start - Property Pump3OkToRun; - /// @brief Pump 4 is in safe state to start - Property Pump4OkToRun; - /// @brief Pump 5 is in safe state to start - Property Pump5OkToRun; - -#pragma endregion - - -protected: -#pragma region R2 - boolean update_MainsVoltage(const String&); - boolean update_CaseTemperature(const String&); - boolean update_PortCurrent(const String&); - boolean update_HeaterTemperature(const String&); - boolean update_PoolTemperature(const String&); - boolean update_WaterPresent(const String&); - boolean update_AwakeMinutesRemaining(const String&); - boolean update_FiltPumpRunTimeTotal(const String&); - boolean update_FiltPumpReqMins(const String&); - boolean update_LoadTimeOut(const String&); - boolean update_HourMeter(const String&); - boolean update_Relay1(const String&); - boolean update_Relay2(const String&); - boolean update_Relay3(const String&); - boolean update_Relay4(const String&); - boolean update_Relay5(const String&); - boolean update_Relay6(const String&); - boolean update_Relay7(const String&); - boolean update_Relay8(const String&); - boolean update_Relay9(const String&); -#pragma endregion -#pragma region R3 - boolean update_CLMT(const String&); - boolean update_PHSE(const String&); - boolean update_LLM1(const String&); - boolean update_LLM2(const String&); - boolean update_LLM3(const String&); - boolean update_SVER(const String&); - boolean update_Model(const String&); - boolean update_SerialNo1(const String&); - boolean update_SerialNo2(const String&); - boolean update_D1(const String&); - boolean update_D2(const String&); - boolean update_D3(const String&); - boolean update_D4(const String&); - boolean update_D5(const String&); - boolean update_D6(const String&); - boolean update_Pump(const String&); - boolean update_LS(const String&); - boolean update_HV(const String&); - boolean update_SnpMR(const String&); - boolean update_Status(const String&); - boolean update_PrimeCount(const String&); - boolean update_EC(const String&); - boolean update_HAMB(const String&); - boolean update_HCON(const String&); -// boolean update_HV_2(const String&); -#pragma endregion -#pragma region R4 - boolean update_Ser1_Timer(const String&); - boolean update_Ser2_Timer(const String&); - boolean update_Ser3_Timer(const String&); - boolean update_HeatMode(const String&); - boolean update_PumpIdleTimer(const String&); - boolean update_PumpRunTimer(const String&); - boolean update_AdtPoolHys(const String&); - boolean update_AdtHeaterHys(const String&); - boolean update_Power(const String&); - boolean update_Power_kWh(const String&); - boolean update_Power_Today(const String&); - boolean update_Power_Yesterday(const String&); - boolean update_ThermalCutOut(const String&); - boolean update_Test_D1(const String&); - boolean update_Test_D2(const String&); - boolean update_Test_D3(const String&); - boolean update_ElementHeatSourceOffset(const String&); - boolean update_Frequency(const String&); - boolean update_HPHeatSourceOffset_Heat(const String&); - boolean update_HPHeatSourceOffset_Cool(const String&); - boolean update_HeatSourceOffTime(const String&); - boolean update_Vari_Speed(const String&); - boolean update_Vari_Percent(const String&); - boolean update_Vari_Mode(const String&); -#pragma endregion -#pragma region R5 - // R5 - // Unknown encoding - TouchPad2.update_Value(); - // Unknown encoding - TouchPad1.update_Value(); - boolean update_RB_TP_Blower(const String&); - - boolean update_RB_TP_Auto(const String&); - boolean update_RB_TP_Heater(const String&); - boolean update_RB_TP_Ozone(const String&); - boolean update_RB_TP_Sleep(const String&); - boolean update_WTMP(const String&); - boolean update_CleanCycle(const String&); -#pragma endregion -#pragma region R6 - boolean update_L_24HOURS(const String&); - boolean update_PSAV_LVL(const String&); - boolean update_PSAV_BGN(const String&); - boolean update_PSAV_END(const String&); - boolean update_DefaultScrn(const String&); - boolean update_TOUT(const String&); - boolean update_VPMP(const String&); - boolean update_HIFI(const String&); - boolean update_BRND(const String&); - boolean update_PRME(const String&); - boolean update_ELMT(const String&); - boolean update_TYPE(const String&); - boolean update_GAS(const String&); -#pragma endregion -#pragma region R7 - boolean update_WCLNTime(const String&); - // The following 2 may be reversed - boolean update_TemperatureUnits(const String&); - boolean update_OzoneOff(const String&); - boolean update_Ozone24(const String&); - // The following 2 may be reversed - boolean update_Circ24(const String&); - boolean update_CJET(const String&); - boolean update_VELE(const String&); - boolean update_V_Max(const String&); - boolean update_V_Min(const String&); - boolean update_V_Max_24(const String&); - boolean update_V_Min_24(const String&); - boolean update_CurrentZero(const String&); - boolean update_CurrentAdjust(const String&); - boolean update_VoltageAdjust(const String&); - boolean update_Ser1(const String&); - boolean update_Ser2(const String&); - boolean update_Ser3(const String&); - boolean update_VMAX(const String&); - boolean update_AHYS(const String&); - boolean update_HUSE(const String&); - boolean update_PMIN(const String&); - boolean update_PFLT(const String&); - boolean update_PHTR(const String&); - boolean update_PMAX(const String&); -#pragma endregion -#pragma region R9 - boolean update_F1_HR(const String&); - boolean update_F1_Time(const String&); - boolean update_F1_ER(const String&); - boolean update_F1_I(const String&); - boolean update_F1_V(const String&); - boolean update_F1_PT(const String&); - boolean update_F1_HT(const String&); - boolean update_F1_CT(const String&); - boolean update_F1_PU(const String&); - boolean update_F1_VE(const String&); - boolean update_F1_ST(const String&); -#pragma endregion -#pragma region RA - boolean update_F2_HR(const String&); - boolean update_F2_Time(const String&); - boolean update_F2_ER(const String&); - boolean update_F2_I(const String&); - boolean update_F2_V(const String&); - boolean update_F2_PT(const String&); - boolean update_F2_HT(const String&); - boolean update_F2_CT(const String&); - boolean update_F2_PU(const String&); - boolean update_F2_VE(const String&); - boolean update_F2_ST(const String&); -#pragma endregion -#pragma region RB - boolean update_F3_HR(const String&); - boolean update_F3_Time(const String&); - boolean update_F3_ER(const String&); - boolean update_F3_I(const String&); - boolean update_F3_V(const String&); - boolean update_F3_PT(const String&); - boolean update_F3_HT(const String&); - boolean update_F3_CT(const String&); - boolean update_F3_PU(const String&); - boolean update_F3_VE(const String&); - boolean update_F3_ST(const String&); -#pragma endregion -#pragma region RC - // Outlet_Heater.update_Value(String); - // Outlet_Circ.update_Value(String); - // Outlet_Sanitise.update_Value(String); - // Outlet_Pump1.update_Value(String); - // Outlet_Pump2.update_Value(String); - // Outlet_Pump4.update_Value(String); - // Outlet_Pump5.update_Value(String); -#pragma endregion -#pragma region RE - boolean update_HP_Present(const String&); - // HP_FlowSwitch.update_Value(String); - // HP_HighSwitch.update_Value(String); - // HP_LowSwitch.update_Value(String); - // HP_CompCutOut.update_Value(String); - // HP_ExCutOut.update_Value(String); - // HP_D1.update_Value(String); - // HP_D2.update_Value(String); - // HP_D3.update_Value(String); - boolean update_HP_Ambient(const String&); - boolean update_HP_Condensor(const String&); - boolean update_HP_Compressor_State(const String&); - boolean update_HP_Fan_State(const String&); - boolean update_HP_4W_Valve(const String&); - boolean update_HP_Heater_State(const String&); - boolean update_HP_State(const String&); - boolean update_HP_Mode(const String&); - boolean update_HP_Defrost_Timer(const String&); - boolean update_HP_Comp_Run_Timer(const String&); - boolean update_HP_Low_Temp_Timer(const String&); - boolean update_HP_Heat_Accum_Timer(const String&); - boolean update_HP_Sequence_Timer(const String&); - boolean update_HP_Warning(const String&); - boolean update_FrezTmr(const String&); - boolean update_DBGN(const String&); - boolean update_DEND(const String&); - boolean update_DCMP(const String&); - boolean update_DMAX(const String&); - boolean update_DELE(const String&); - boolean update_DPMP(const String&); -// CMAX.update_Value(String); -// HP_Compressor.update_Value(String); -// HP_Pump_State.update_Value(String); -// HP_Status.update_Value(String); -#pragma endregion -#pragma region RG - boolean update_Pump1InstallState(const String&); - boolean update_Pump2InstallState(const String&); - boolean update_Pump3InstallState(const String&); - boolean update_Pump4InstallState(const String&); - boolean update_Pump5InstallState(const String&); - boolean update_Pump1OkToRun(const String&); - boolean update_Pump2OkToRun(const String&); - boolean update_Pump3OkToRun(const String&); - boolean update_Pump4OkToRun(const String&); - boolean update_Pump5OkToRun(const String&); -#pragma endregion - - -public: - int getMainsVoltage() { return MainsVoltage.getValue(); } - void setMainsVoltageCallback(void (*callback)(int)) { MainsVoltage.setCallback(callback); } - - /// @brief Gets current case temperature multiplied by 10 (245 = 24.5 actual) - /// @return - int getCaseTemperature() { return CaseTemperature.getValue(); } - void setCaseTemperatureCallback(void (*callback)(int)) { CaseTemperature.setCallback(callback); } - - int getPortCurrent() { return PortCurrent.getValue(); } - void setPortCurrentCallback(void (*callback)(int)) { PortCurrent.setCallback(callback); } - - /// @brief Get current heater temperature multiplied by 10 (245 = 24.5 actual) - /// @return - int getHeaterTemperature() { return HeaterTemperature.getValue(); } - void setHeaterTemperatureCallback(void (*callback)(int)) { HeaterTemperature.setCallback(callback); } - - int getPoolTemperature() { return PoolTemperature.getValue(); } - void setPoolTemperatureCallback(void (*callback)(int)) { PoolTemperature.setCallback(callback); } - - bool getWaterPresent() { return WaterPresent.getValue(); } - void setWaterPresentCallback(void (*callback)(bool)) { WaterPresent.setCallback(callback); } - - int getAwakeMinutesRemaining() { return AwakeMinutesRemaining.getValue(); } - void setAwakeMinutesRemainingCallback(void (*callback)(int)) { AwakeMinutesRemaining.setCallback(callback); } - - int getFiltPumpRunTimeTotal() { return FiltPumpRunTimeTotal.getValue(); } - void setFiltPumpRunTimeTotalCallback(void (*callback)(int)) { FiltPumpRunTimeTotal.setCallback(callback); } - - int getFiltPumpReqMins() { return FiltPumpReqMins.getValue(); } - void setFiltPumpReqMinsCallback(void (*callback)(int)) { FiltPumpReqMins.setCallback(callback); } - - int getLoadTimeOut() { return LoadTimeOut.getValue(); } - void setLoadTimeOutCallback(void (*callback)(int)) { LoadTimeOut.setCallback(callback); } - - /// @brief Get runtime hours multiplied by 10 (899 = 89.9 actual) - /// @return - int getHourMeter() { return HourMeter.getValue(); } - void setHourMeterCallback(void (*callback)(int)) { HourMeter.setCallback(callback); } - - int getRelay1() { return Relay1.getValue(); } - void setRelay1Callback(void (*callback)(int)) { Relay1.setCallback(callback); } - - int getRelay2() { return Relay2.getValue(); } - void setRelay2Callback(void (*callback)(int)) { Relay2.setCallback(callback); } - - int getRelay3() { return Relay3.getValue(); } - void setRelay3Callback(void (*callback)(int)) { Relay3.setCallback(callback); } - - int getRelay4() { return Relay4.getValue(); } - void setRelay4Callback(void (*callback)(int)) { Relay4.setCallback(callback); } - - int getRelay5() { return Relay5.getValue(); } - void setRelay5Callback(void (*callback)(int)) { Relay5.setCallback(callback); } - - int getRelay6() { return Relay6.getValue(); } - void setRelay6Callback(void (*callback)(int)) { Relay6.setCallback(callback); } - - int getRelay7() { return Relay7.getValue(); } - void setRelay7Callback(void (*callback)(int)) { Relay7.setCallback(callback); } - - int getRelay8() { return Relay8.getValue(); } - void setRelay8Callback(void (*callback)(int)) { Relay8.setCallback(callback); } - - int getRelay9() { return Relay9.getValue(); } - void setRelay9Callback(void (*callback)(int)) { Relay9.setCallback(callback); } - - int getCLMT() { return CLMT.getValue(); } - void setCLMTCallback(void (*callback)(int)) { CLMT.setCallback(callback); } - - int getPHSE() { return PHSE.getValue(); } - void setPHSECallback(void (*callback)(int)) { PHSE.setCallback(callback); } - - int getLLM1() { return LLM1.getValue(); } - void setLLM1Callback(void (*callback)(int)) { LLM1.setCallback(callback); } - - int getLLM2() { return LLM2.getValue(); } - void setLLM2Callback(void (*callback)(int)) { LLM2.setCallback(callback); } - - int getLLM3() { return LLM3.getValue(); } - void setLLM3Callback(void (*callback)(int)) { LLM3.setCallback(callback); } - - String getSVER() { return SVER.getValue(); } - void setSVERCallback(void (*callback)(String)) { SVER.setCallback(callback); } - - String getModel() { return Model.getValue(); } - void setModelCallback(void (*callback)(String)) { Model.setCallback(callback); } - - String getSerialNo1() { return SerialNo1.getValue(); } - void setSerialNo1Callback(void (*callback)(String)) { SerialNo1.setCallback(callback); } - - String getSerialNo2() { return SerialNo2.getValue(); } - void setSerialNo2Callback(void (*callback)(String)) { SerialNo2.setCallback(callback); } - - bool getD1() { return D1.getValue(); } - void setD1Callback(void (*callback)(bool)) { D1.setCallback(callback); } - - bool getD2() { return D2.getValue(); } - void setD2Callback(void (*callback)(bool)) { D2.setCallback(callback); } - - bool getD3() { return D3.getValue(); } - void setD3Callback(void (*callback)(bool)) { D3.setCallback(callback); } - - bool getD4() { return D4.getValue(); } - void setD4Callback(void (*callback)(bool)) { D4.setCallback(callback); } - - bool getD5() { return D5.getValue(); } - void setD5Callback(void (*callback)(bool)) { D5.setCallback(callback); } - - bool getD6() { return D6.getValue(); } - void setD6Callback(void (*callback)(bool)) { D6.setCallback(callback); } - - String getPump() { return Pump.getValue(); } - void setPumpCallback(void (*callback)(String)) { Pump.setCallback(callback); } - - int getLS() { return LS.getValue(); } - void setLSCallback(void (*callback)(int)) { LS.setCallback(callback); } - - bool getHV() { return HV.getValue(); } - void setHVCallback(void (*callback)(bool)) { HV.setCallback(callback); } - - int getSnpMR() { return SnpMR.getValue(); } - void setSnpMRCallback(void (*callback)(int)) { SnpMR.setCallback(callback); } - - String getStatus() { return Status.getValue(); } - void setStatusCallback(void (*callback)(String)) { Status.setCallback(callback); } - - int getPrimeCount() { return PrimeCount.getValue(); } - void setPrimeCountCallback(void (*callback)(int)) { PrimeCount.setCallback(callback); } - - /// @brief Get EC value multiplied by 10 (66 = 6.6 actual) - /// @return - int getEC() { return EC.getValue(); } - void setECCallback(void (*callback)(int)) { EC.setCallback(callback); } - - int getHAMB() { return HAMB.getValue(); } - void setHAMBCallback(void (*callback)(int)) { HAMB.setCallback(callback); } - - int getHCON() { return HCON.getValue(); } - void setHCONCallback(void (*callback)(int)) { HCON.setCallback(callback); } - - - int getSer1_Timer() { return Ser1_Timer.getValue(); } - void setSer1_TimerCallback(void (*callback)(int)) { Ser1_Timer.setCallback(callback); } - - int getSer2_Timer() { return Ser2_Timer.getValue(); } - void setSer2_TimerCallback(void (*callback)(int)) { Ser2_Timer.setCallback(callback); } - - int getSer3_Timer() { return Ser3_Timer.getValue(); } - void setSer3_TimerCallback(void (*callback)(int)) { Ser3_Timer.setCallback(callback); } - - int getHeatMode() { return HeatMode.getValue(); } - void setHeatModeCallback(void (*callback)(int)) { HeatMode.setCallback(callback); } - - int getPumpIdleTimer() { return PumpIdleTimer.getValue(); } - void setPumpIdleTimerCallback(void (*callback)(int)) { PumpIdleTimer.setCallback(callback); } - - int getPumpRunTimer() { return PumpRunTimer.getValue(); } - void setPumpRunTimerCallback(void (*callback)(int)) { PumpRunTimer.setCallback(callback); } - - /// @brief Get pool hysteris value multiplied by 10 (66 = 6.6 actual) - /// @return - int getAdtPoolHys() { return AdtPoolHys.getValue(); } - void setAdtPoolHysCallback(void (*callback)(int)) { AdtPoolHys.setCallback(callback); } - - /// @brief Get heater hysteris value multiplied by 10 (66 = 6.6 actual) - /// @return - int getAdtHeaterHys() { return AdtHeaterHys.getValue(); } - void setAdtHeaterHysCallback(void (*callback)(int)) { AdtHeaterHys.setCallback(callback); } - - /// @brief Get current power consumption (W) multiplied by 10 (24350 = 2435.0) - /// @return - int getPower() { return Power.getValue(); } - void setPowerCallback(void (*callback)(int)) { Power.setCallback(callback); } - - /// @brief Get energy consumption since last reset (kWh) multiplied by 100 (24350 = 243.50) - /// @return - int getPower_kWh() { return Power_kWh.getValue(); } - void setPower_kWhCallback(void (*callback)(int)) { Power_kWh.setCallback(callback); } - - /// @brief Get energy consumption today (kWh) multiplied by 100 (24350 = 243.50) - /// @return - int getPower_Today() { return Power_Today.getValue(); } - void setPower_TodayCallback(void (*callback)(int)) { Power_Today.setCallback(callback); } - - /// @brief Get energy consumption yesterday (kWh) multiplied by 100 (24350 = 243.50) - /// @return - int getPower_Yesterday() { return Power_Yesterday.getValue(); } - void setPower_YesterdayCallback(void (*callback)(int)) { Power_Yesterday.setCallback(callback); } - - int getThermalCutOut() { return ThermalCutOut.getValue(); } - void setThermalCutOutCallback(void (*callback)(int)) { ThermalCutOut.setCallback(callback); } - - int getTest_D1() { return Test_D1.getValue(); } - void setTest_D1Callback(void (*callback)(int)) { Test_D1.setCallback(callback); } - - int getTest_D2() { return Test_D2.getValue(); } - void setTest_D2Callback(void (*callback)(int)) { Test_D2.setCallback(callback); } - - int getTest_D3() { return Test_D3.getValue(); } - void setTest_D3Callback(void (*callback)(int)) { Test_D3.setCallback(callback); } - - /// @brief Get Heat Element Source Offset multiplied by 10 (543 = 54.3 actual) - /// @return - int getElementHeatSourceOffset() { return ElementHeatSourceOffset.getValue(); } - void setElementHeatSourceOffsetCallback(void (*callback)(int)) { ElementHeatSourceOffset.setCallback(callback); } - - int getFrequency() { return Frequency.getValue(); } - void setFrequencyCallback(void (*callback)(int)) { Frequency.setCallback(callback); } - - /// @brief Get Heat Pump Heating Source Offset multiplied by 10 (543 = 54.3 actual) - /// @return - int getHPHeatSourceOffset_Heat() { return HPHeatSourceOffset_Heat.getValue(); } - void setHPHeatSourceOffset_HeatCallback(void (*callback)(int)) { HPHeatSourceOffset_Heat.setCallback(callback); } - - /// @brief Get Heat Pump Cooling Source Offset multiplied by 10 (543 = 54.3 actual) - /// @return - int getHPHeatSourceOffset_Cool() { return HPHeatSourceOffset_Cool.getValue(); } - void setHPHeatSourceOffset_CoolCallback(void (*callback)(int)) { HPHeatSourceOffset_Cool.setCallback(callback); } - - int getHeatSourceOffTime() { return HeatSourceOffTime.getValue(); } - void setHeatSourceOffTimeCallback(void (*callback)(int)) { HeatSourceOffTime.setCallback(callback); } - - int getVari_Speed() { return Vari_Speed.getValue(); } - void setVari_SpeedCallback(void (*callback)(int)) { Vari_Speed.setCallback(callback); } - - int getVari_Percent() { return Vari_Percent.getValue(); } - void setVari_PercentCallback(void (*callback)(int)) { Vari_Percent.setCallback(callback); } - - int getVari_Mode() { return Vari_Mode.getValue(); } - void setVari_ModeCallback(void (*callback)(int)) { Vari_Mode.setCallback(callback); } - - const std::array autoPumpOptions = {"Manual", "Auto"}; - - int getRB_TP_Blower() { return RB_TP_Blower.getValue(); } - void setRB_TP_BlowerCallback(void (*callback)(int)) { RB_TP_Blower.setCallback(callback); } - - - bool getRB_TP_Auto() { return RB_TP_Auto.getValue(); } - void setRB_TP_AutoCallback(void (*callback)(bool)) { RB_TP_Auto.setCallback(callback); } - - bool getRB_TP_Heater() { return RB_TP_Heater.getValue(); } - void setRB_TP_HeaterCallback(void (*callback)(bool)) { RB_TP_Heater.setCallback(callback); } - - bool getRB_TP_Ozone() { return RB_TP_Ozone.getValue(); } - void setRB_TP_OzoneCallback(void (*callback)(bool)) { RB_TP_Ozone.setCallback(callback); } - - bool getRB_TP_Sleep() { return RB_TP_Sleep.getValue(); } - void setRB_TP_SleepCallback(void (*callback)(bool)) { RB_TP_Sleep.setCallback(callback); } - - /// @brief Get current water temperature divide by 10 to get actual temp (384 = 38.4 ) - /// @return - int getWTMP() { return WTMP.getValue(); } - void setWTMPCallback(void (*callback)(int)) { WTMP.setCallback(callback); } - - bool getCleanCycle() { return CleanCycle.getValue(); } - void setCleanCycleCallback(void (*callback)(bool)) { CleanCycle.setCallback(callback); } - -int getL_24HOURS() { return L_24HOURS.getValue(); } - void setL_24HOURSCallback(void (*callback)(int)) { L_24HOURS.setCallback(callback); } - - int getPSAV_LVL() { return PSAV_LVL.getValue(); } - void setPSAV_LVLCallback(void (*callback)(int)) { PSAV_LVL.setCallback(callback); } - - int getPSAV_BGN() { return PSAV_BGN.getValue(); } - void setPSAV_BGNCallback(void (*callback)(int)) { PSAV_BGN.setCallback(callback); } - - int getPSAV_END() { return PSAV_END.getValue(); } - void setPSAV_ENDCallback(void (*callback)(int)) { PSAV_END.setCallback(callback); } - - //int getL_1SNZ_BGN() { return L_1SNZ_BGN.getValue(); } - //void setL_1SNZ_BGNCallback(void (*callback)(int)) { L_1SNZ_BGN.setCallback(callback); } - - //int getL_2SNZ_BGN() { return L_2SNZ_BGN.getValue(); } - //void setL_2SNZ_BGNCallback(void (*callback)(int)) { L_2SNZ_BGN.setCallback(callback); } - - //int getL_1SNZ_END() { return L_1SNZ_END.getValue(); } - //void setL_1SNZ_ENDCallback(void (*callback)(int)) { L_1SNZ_END.setCallback(callback); } - - //int getL_2SNZ_END() { return L_2SNZ_END.getValue(); } - //void setL_2SNZ_ENDCallback(void (*callback)(int)) { L_2SNZ_END.setCallback(callback); } - - int getDefaultScrn() { return DefaultScrn.getValue(); } - void setDefaultScrnCallback(void (*callback)(int)) { DefaultScrn.setCallback(callback); } - - int getTOUT() { return TOUT.getValue(); } - void setTOUTCallback(void (*callback)(int)) { TOUT.setCallback(callback); } - - bool getVPMP() { return VPMP.getValue(); } - void setVPMPCallback(void (*callback)(bool)) { VPMP.setCallback(callback); } - - bool getHIFI() { return HIFI.getValue(); } - void setHIFICallback(void (*callback)(bool)) { HIFI.setCallback(callback); } - - int getBRND() { return BRND.getValue(); } - void setBRNDCallback(void (*callback)(int)) { BRND.setCallback(callback); } - - int getPRME() { return PRME.getValue(); } - void setPRMECallback(void (*callback)(int)) { PRME.setCallback(callback); } - - int getELMT() { return ELMT.getValue(); } - void setELMTCallback(void (*callback)(int)) { ELMT.setCallback(callback); } - - int getTYPE() { return TYPE.getValue(); } - void setTYPECallback(void (*callback)(int)) { TYPE.setCallback(callback); } - - int getGAS() { return GAS.getValue(); } - void setGASCallback(void (*callback)(int)) { GAS.setCallback(callback); } - - int getWCLNTime() { return WCLNTime.getValue(); } - void setWCLNTimeCallback(void (*callback)(int)) { WCLNTime.setCallback(callback); } - - bool getTemperatureUnits() { return TemperatureUnits.getValue(); } - void setTemperatureUnitsCallback(void (*callback)(bool)) { TemperatureUnits.setCallback(callback); } - - bool getOzoneOff() { return OzoneOff.getValue(); } - void setOzoneOffCallback(void (*callback)(bool)) { OzoneOff.setCallback(callback); } - - bool getCirc24() { return Circ24.getValue(); } - void setCirc24Callback(void (*callback)(bool)) { Circ24.setCallback(callback); } - - bool getCJET() { return CJET.getValue(); } - void setCJETCallback(void (*callback)(bool)) { CJET.setCallback(callback); } - - bool getVELE() { return VELE.getValue(); } - void setVELECallback(void (*callback)(bool)) { VELE.setCallback(callback); } - - int getV_Max() { return V_Max.getValue(); } - void setV_MaxCallback(void (*callback)(int)) { V_Max.setCallback(callback); } - - int getV_Min() { return V_Min.getValue(); } - void setV_MinCallback(void (*callback)(int)) { V_Min.setCallback(callback); } - - int getV_Max_24() { return V_Max_24.getValue(); } - void setV_Max_24Callback(void (*callback)(int)) { V_Max_24.setCallback(callback); } - - int getV_Min_24() { return V_Min_24.getValue(); } - void setV_Min_24Callback(void (*callback)(int)) { V_Min_24.setCallback(callback); } - - int getCurrentZero() { return CurrentZero.getValue(); } - void setCurrentZeroCallback(void (*callback)(int)) { CurrentZero.setCallback(callback); } - - /// @brief Get Current measurement adjustment multiplied by 10 (77 = 7.7 actual) - /// @return - int getCurrentAdjust() { return CurrentAdjust.getValue(); } - void setCurrentAdjustCallback(void (*callback)(int)) { CurrentAdjust.setCallback(callback); } - - /// @brief Get Voltage measurement adjustment multiplied by 10 (77 = 7.7 actual) - /// @return - int getVoltageAdjust() { return VoltageAdjust.getValue(); } - void setVoltageAdjustCallback(void (*callback)(int)) { VoltageAdjust.setCallback(callback); } - - int getSer1() { return Ser1.getValue(); } - void setSer1Callback(void (*callback)(int)) { Ser1.setCallback(callback); } - - int getSer2() { return Ser2.getValue(); } - void setSer2Callback(void (*callback)(int)) { Ser2.setCallback(callback); } - - int getSer3() { return Ser3.getValue(); } - void setSer3Callback(void (*callback)(int)) { Ser3.setCallback(callback); } - - int getVMAX() { return VMAX.getValue(); } - void setVMAXCallback(void (*callback)(int)) { VMAX.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getAHYS() { return AHYS.getValue(); } - void setAHYSCallback(void (*callback)(int)) { AHYS.setCallback(callback); } - - bool getHUSE() { return HUSE.getValue(); } - void setHUSECallback(void (*callback)(bool)) { HUSE.setCallback(callback); } - - - int getPMIN() { return PMIN.getValue(); } - void setPMINCallback(void (*callback)(int)) { PMIN.setCallback(callback); } - - int getPFLT() { return PFLT.getValue(); } - void setPFLTCallback(void (*callback)(int)) { PFLT.setCallback(callback); } - - int getPHTR() { return PHTR.getValue(); } - void setPHTRCallback(void (*callback)(int)) { PHTR.setCallback(callback); } - - int getPMAX() { return PMAX.getValue(); } - void setPMAXCallback(void (*callback)(int)) { PMAX.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF1_HR() { return F1_HR.getValue(); } - void setF1_HRCallback(void (*callback)(int)) { F1_HR.setCallback(callback); } - - int getF1_Time() { return F1_Time.getValue(); } - void setF1_TimeCallback(void (*callback)(int)) { F1_Time.setCallback(callback); } - - int getF1_ER() { return F1_ER.getValue(); } - void setF1_ERCallback(void (*callback)(int)) { F1_ER.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF1_I() { return F1_I.getValue(); } - void setF1_ICallback(void (*callback)(int)) { F1_I.setCallback(callback); } - - int getF1_V() { return F1_V.getValue(); } - void setF1_VCallback(void (*callback)(int)) { F1_V.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF1_PT() { return F1_PT.getValue(); } - void setF1_PTCallback(void (*callback)(int)) { F1_PT.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF1_HT() { return F1_HT.getValue(); } - void setF1_HTCallback(void (*callback)(int)) { F1_HT.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF1_CT() { return F1_CT.getValue(); } - void setF1_CTCallback(void (*callback)(int)) { F1_CT.setCallback(callback); } - - int getF1_PU() { return F1_PU.getValue(); } - void setF1_PUCallback(void (*callback)(int)) { F1_PU.setCallback(callback); } - - bool getF1_VE() { return F1_VE.getValue(); } - void setF1_VECallback(void (*callback)(bool)) { F1_VE.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF1_ST() { return F1_ST.getValue(); } - void setF1_STCallback(void (*callback)(int)) { F1_ST.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF2_HR() { return F2_HR.getValue(); } - void setF2_HRCallback(void (*callback)(int)) { F2_HR.setCallback(callback); } - - int getF2_Time() { return F2_Time.getValue(); } - void setF2_TimeCallback(void (*callback)(int)) { F2_Time.setCallback(callback); } - - int getF2_ER() { return F2_ER.getValue(); } - void setF2_ERCallback(void (*callback)(int)) { F2_ER.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF2_I() { return F2_I.getValue(); } - void setF2_ICallback(void (*callback)(int)) { F2_I.setCallback(callback); } - - int getF2_V() { return F2_V.getValue(); } - void setF2_VCallback(void (*callback)(int)) { F2_V.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF2_PT() { return F2_PT.getValue(); } - void setF2_PTCallback(void (*callback)(int)) { F2_PT.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF2_HT() { return F2_HT.getValue(); } - void setF2_HTCallback(void (*callback)(int)) { F2_HT.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF2_CT() { return F2_CT.getValue(); } - void setF2_CTCallback(void (*callback)(int)) { F2_CT.setCallback(callback); } - - int getF2_PU() { return F2_PU.getValue(); } - void setF2_PUCallback(void (*callback)(int)) { F2_PU.setCallback(callback); } - - bool getF2_VE() { return F2_VE.getValue(); } - void setF2_VECallback(void (*callback)(bool)) { F2_VE.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF2_ST() { return F2_ST.getValue(); } - void setF2_STCallback(void (*callback)(int)) { F2_ST.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF3_HR() { return F3_HR.getValue(); } - void setF3_HRCallback(void (*callback)(int)) { F3_HR.setCallback(callback); } - - int getF3_Time() { return F3_Time.getValue(); } - void setF3_TimeCallback(void (*callback)(int)) { F3_Time.setCallback(callback); } - - int getF3_ER() { return F3_ER.getValue(); } - void setF3_ERCallback(void (*callback)(int)) { F3_ER.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF3_I() { return F3_I.getValue(); } - void setF3_ICallback(void (*callback)(int)) { F3_I.setCallback(callback); } - - int getF3_V() { return F3_V.getValue(); } - void setF3_VCallback(void (*callback)(int)) { F3_V.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF3_PT() { return F3_PT.getValue(); } - void setF3_PTCallback(void (*callback)(int)) { F3_PT.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF3_HT() { return F3_HT.getValue(); } - void setF3_HTCallback(void (*callback)(int)) { F3_HT.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - int getF3_CT() { return F3_CT.getValue(); } - void setF3_CTCallback(void (*callback)(int)) { F3_CT.setCallback(callback); } - - int getF3_PU() { return F3_PU.getValue(); } - void setF3_PUCallback(void (*callback)(int)) { F3_PU.setCallback(callback); } - - /// @brief Actual value multiplied by 10 (5 = 0.5) - /// @return - bool getF3_VE() { return F3_VE.getValue(); } - void setF3_VECallback(void (*callback)(bool)) { F3_VE.setCallback(callback); } - - int getF3_ST() { return F3_ST.getValue(); } - void setF3_STCallback(void (*callback)(int)) { F3_ST.setCallback(callback); } - - - int getHP_Present() { return HP_Present.getValue(); } - void setHP_PresentCallback(void (*callback)(int)) { HP_Present.setCallback(callback); } - - int getHP_Ambient() { return HP_Ambient.getValue(); } - void setHP_AmbientCallback(void (*callback)(int)) { HP_Ambient.setCallback(callback); } - - int getHP_Condensor() { return HP_Condensor.getValue(); } - void setHP_CondensorCallback(void (*callback)(int)) { HP_Condensor.setCallback(callback); } - - bool getHP_Compressor_State() { return HP_Compressor_State.getValue(); } - void setHP_Compressor_StateCallback(void (*callback)(bool)) { HP_Compressor_State.setCallback(callback); } - - bool getHP_Fan_State() { return HP_Fan_State.getValue(); } - void setHP_Fan_StateCallback(void (*callback)(bool)) { HP_Fan_State.setCallback(callback); } - - bool getHP_4W_Valve() { return HP_4W_Valve.getValue(); } - void setHP_4W_ValveCallback(void (*callback)(bool)) { HP_4W_Valve.setCallback(callback); } - - bool getHP_Heater_State() { return HP_Heater_State.getValue(); } - void setHP_Heater_StateCallback(void (*callback)(bool)) { HP_Heater_State.setCallback(callback); } - - int getHP_Mode() { return HP_Mode.getValue(); } - void setHP_ModeCallback(void (*callback)(int)) { HP_Mode.setCallback(callback); } - - int getHP_Defrost_Timer() { return HP_Defrost_Timer.getValue(); } - void setHP_Defrost_TimerCallback(void (*callback)(int)) { HP_Defrost_Timer.setCallback(callback); } - - int getHP_Comp_Run_Timer() { return HP_Comp_Run_Timer.getValue(); } - void setHP_Comp_Run_TimerCallback(void (*callback)(int)) { HP_Comp_Run_Timer.setCallback(callback); } - - int getHP_Low_Temp_Timer() { return HP_Low_Temp_Timer.getValue(); } - void setHP_Low_Temp_TimerCallback(void (*callback)(int)) { HP_Low_Temp_Timer.setCallback(callback); } - - int getHP_Heat_Accum_Timer() { return HP_Heat_Accum_Timer.getValue(); } - void setHP_Heat_Accum_TimerCallback(void (*callback)(int)) { HP_Heat_Accum_Timer.setCallback(callback); } - - int getHP_Warning() { return HP_Warning.getValue(); } - void setHP_WarningCallback(void (*callback)(int)) { HP_Warning.setCallback(callback); } - - int getHP_FrezTmr() { return FrezTmr.getValue(); } - void setHP_FrezTmrCallback(void (*callback)(int)) { FrezTmr.setCallback(callback); } - - int getDBGN() { return DBGN.getValue(); } - void setDBGNCallback(void (*callback)(int)) { DBGN.setCallback(callback); } - - int getDEND() { return DEND.getValue(); } - void setDENDCallback(void (*callback)(int)) { DEND.setCallback(callback); } - - int getDCMP() { return DCMP.getValue(); } - void setDCMPCallback(void (*callback)(int)) { DCMP.setCallback(callback); } - - int getDMAX() { return DMAX.getValue(); } - void setDMAXCallback(void (*callback)(int)) { DMAX.setCallback(callback); } - - int getDELE() { return DELE.getValue(); } - void setDELECallback(void (*callback)(int)) { DELE.setCallback(callback); } - - int getDPMP() { return DPMP.getValue(); } - void setDPMPCallback(void (*callback)(int)) { DPMP.setCallback(callback); } - - String getPump1InstallState() { return Pump1InstallState.getValue(); } - void setPump1InstallStateCallback(void (*callback)(String)) { Pump1InstallState.setCallback(callback); } - - String getPump2InstallState() { return Pump2InstallState.getValue(); } - void setPump2InstallStateCallback(void (*callback)(String)) { Pump2InstallState.setCallback(callback); } - - String getPump3InstallState() { return Pump3InstallState.getValue(); } - void setPump3InstallStateCallback(void (*callback)(String)) { Pump3InstallState.setCallback(callback); } - - String getPump4InstallState() { return Pump4InstallState.getValue(); } - void setPump4InstallStateCallback(void (*callback)(String)) { Pump4InstallState.setCallback(callback); } - - String getPump5InstallState() { return Pump5InstallState.getValue(); } - void setPump5InstallStateCallback(void (*callback)(String)) { Pump5InstallState.setCallback(callback); } - - bool getPump1OkToRun() { return Pump1OkToRun.getValue(); } - void setPump1OkToRunCallback(void (*callback)(bool)) { Pump1OkToRun.setCallback(callback); } - - bool getPump2OkToRun() { return Pump2OkToRun.getValue(); } - void setPump2OkToRunCallback(void (*callback)(bool)) { Pump2OkToRun.setCallback(callback); } - - bool getPump3OkToRun() { return Pump3OkToRun.getValue(); } - void setPump3OkToRunCallback(void (*callback)(bool)) { Pump3OkToRun.setCallback(callback); } - - bool getPump4OkToRun() { return Pump4OkToRun.getValue(); } - void setPump4OkToRunCallback(void (*callback)(bool)) { Pump4OkToRun.setCallback(callback); } - - bool getPump5OkToRun() { return Pump5OkToRun.getValue(); } - void setPump5OkToRunCallback(void (*callback)(bool)) { Pump5OkToRun.setCallback(callback); } - -}; - -#endif - diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 5ef6554..b29ce0e 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -51,7 +51,7 @@ bool getPumpModesJson(SpaInterface &si, int pumpNumber, JsonObject pumps) { } // Retrieve the pump install state dynamically - String pumpInstallState = (si.*(pumpInstallStateFunctions[pumpNumber - 1]))(); + String pumpInstallState = (si.*(SpaInterface::pumpInstallStateFunctions[pumpNumber - 1])).get(); char pumpKey[6] = "pump"; // Start with "pump" pumpKey[4] = '0' + pumpNumber; // Append the pump number as a character @@ -131,26 +131,26 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String JsonDocument json; json["temperatures"]["setPoint"] = si.STMP.get() / 10.0; - json["temperatures"]["water"] = si.getWTMP() / 10.0; - json["temperatures"]["heater"] = si.getHeaterTemperature() / 10.0; - json["temperatures"]["case"] = si.getCaseTemperature(); - json["temperatures"]["heatpumpAmbient"] = si.getHP_Ambient(); - json["temperatures"]["heatpumpCondensor"] = si.getHP_Condensor(); + json["temperatures"]["water"] = si.WTMP.get() / 10.0; + json["temperatures"]["heater"] = si.HeaterTemperature.get() / 10.0; + json["temperatures"]["case"] = si.CaseTemperature.get(); + json["temperatures"]["heatpumpAmbient"] = si.HP_Ambient.get(); + json["temperatures"]["heatpumpCondensor"] = si.HP_Condensor.get(); - json["power"]["voltage"] = si.getMainsVoltage(); + json["power"]["voltage"] = si.MainsVoltage.get(); json["power"]["current"]= si.MainsCurrent.get() / 10.0; // convert value to A - json["power"]["power"] = si.getPower() / 10.0; // convert value to W - json["power"]["totalenergy"]= si.getPower_kWh() / 100.0; // convert value to kWh. + json["power"]["power"] = si.Power.get() / 10.0; // convert value to W + json["power"]["totalenergy"]= si.Power_kWh.get() / 100.0; // convert value to kWh. - json["status"]["heatingActive"] = si.getRB_TP_Heater()? "ON": "OFF"; - json["status"]["ozoneActive"] = si.getRB_TP_Ozone()? "ON": "OFF"; - json["status"]["state"] = si.getStatus(); + json["status"]["heatingActive"] = si.RB_TP_Heater.get()? "ON": "OFF"; + json["status"]["ozoneActive"] = si.RB_TP_Ozone.get()? "ON": "OFF"; + json["status"]["state"] = si.Status.get(); json["status"]["spaMode"] = si.Mode.getLabel(); - json["status"]["controller"] = si.getModel(); - String firmware = si.getSVER().substring(3); + json["status"]["controller"] = si.Model.get(); + String firmware = si.SVER.get().substring(3); firmware.replace(' ', '.'); json["status"]["firmware"] = firmware; - json["status"]["serial"] = si.getSerialNo1() + "-" + si.getSerialNo2(); + json["status"]["serial"] = si.SerialNo1.get() + "-" + si.SerialNo2.get(); json["status"]["siInitialised"] = si.isInitialised()?"true":"false"; json["status"]["mqtt"] = mqttClient.connected()?"connected":"disconnected"; diff --git a/lib/WebUI/WebUI.cpp b/lib/WebUI/WebUI.cpp index fb60ed0..cafe2cf 100644 --- a/lib/WebUI/WebUI.cpp +++ b/lib/WebUI/WebUI.cpp @@ -165,7 +165,7 @@ void WebUI::begin() { server.on("/status", HTTP_GET, [this](AsyncWebServerRequest *request) { debugD("uri: %s", request->url().c_str()); - AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", _spa->statusResponse.getValue()); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", _spa->statusResponse.get()); response->addHeader("Connection", "close"); request->send(response); }); diff --git a/src/main.cpp b/src/main.cpp index f5053ae..67c2e65 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -266,7 +266,7 @@ void mqttHaAutoDiscovery() { const String* selectedPumpOptions = nullptr; size_t arrSize = 0; for (int pumpNumber = 1; pumpNumber <= 5; pumpNumber++) { - String pumpInstallState = (si.*(pumpInstallStateFunctions[pumpNumber - 1]))(); + String pumpInstallState = (si.*(SpaInterface::pumpInstallStateFunctions[pumpNumber - 1])).get(); if (getPumpInstalledState(pumpInstallState) && getPumpPossibleStates(pumpInstallState).length() > 1) { ADConf.displayName = "Pump " + String(pumpNumber); ADConf.propertyId = "pump" + String(pumpNumber); @@ -287,7 +287,7 @@ void mqttHaAutoDiscovery() { } } - if (si.getHP_Present()) { + if (si.HP_Present.get()) { ADConf.displayName = "Heatpump Ambient Temperature"; ADConf.valueTemplate = "{{ value_json.temperatures.heatpumpAmbient }}"; ADConf.propertyId = "HPAmbTemp"; @@ -513,7 +513,7 @@ void setSpaProperty(String property, String p) { } } else if (property.startsWith("pump") && property.endsWith("_state")) { int pumpNum = property.charAt(4) - '0'; - String pumpState = (si.*(pumpInstallStateFunctions[pumpNum-1]))(); + String pumpState = (si.*(SpaInterface::pumpInstallStateFunctions[pumpNum-1])).get(); if (pumpNum - 1 < array_count(SpaInterface::pumpStatuses)) { try { if (getPumpSpeedType(pumpState) == "2") (si.*(SpaInterface::pumpStatuses[pumpNum-1])).set(p=="OFF"?0:2); // When we turn on the pump use speed high @@ -856,7 +856,7 @@ void loop() { if ( spaSerialNumber=="" ) { debugI("Initialising..."); - spaSerialNumber = si.getSerialNo1()+"-"+si.getSerialNo2(); + spaSerialNumber = si.SerialNo1.get()+"-"+si.SerialNo2.get(); debugI("Spa serial number is %s",spaSerialNumber.c_str()); mqttBase = String("sn_esp32/") + spaSerialNumber + String("/"); From 957e544ca01567c5bd671ab5e5656fb1a04fa6b3 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Thu, 12 Mar 2026 14:55:05 +1100 Subject: [PATCH 30/44] update comments --- lib/SpaInterface/SpaInterface.h | 180 +++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 7ac6347..a06aa7c 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -493,190 +493,368 @@ class SpaInterface { ROProperty MainsCurrent; // R2 + /// @brief Controller case temperature x10 (°C). e.g. 250 = 25.0°C. ROProperty CaseTemperature; + /// @brief Port current draw x10 (A). ROProperty PortCurrent; + /// @brief Heater element temperature x10 (°C). ROProperty HeaterTemperature; + /// @brief Spa pool/water temperature x10 (°C). e.g. 380 = 38.0°C. ROProperty PoolTemperature; + /// @brief Minutes remaining before spa returns to sleep. ROProperty AwakeMinutesRemaining; + /// @brief Total filtration pump run time (minutes). ROProperty FiltPumpRunTimeTotal; + /// @brief Filtration pump required run minutes remaining. ROProperty FiltPumpReqMins; + /// @brief Load management timeout counter. ROProperty LoadTimeOut; + /// @brief Total controller run time (hours). ROProperty HourMeter; + /// @brief Relay 1 output state. ROProperty Relay1; + /// @brief Relay 2 output state. ROProperty Relay2; + /// @brief Relay 3 output state. ROProperty Relay3; + /// @brief Relay 4 output state. ROProperty Relay4; + /// @brief Relay 5 output state. ROProperty Relay5; + /// @brief Relay 6 output state. ROProperty Relay6; + /// @brief Relay 7 output state. ROProperty Relay7; + /// @brief Relay 8 output state. ROProperty Relay8; + /// @brief Relay 9 output state. ROProperty Relay9; + /// @brief True when water is detected in the spa. ROProperty WaterPresent; // R3 + /// @brief Current limit setting (A). ROProperty CLMT; + /// @brief Mains phase configuration (1 = single phase, 3 = three phase). ROProperty PHSE; + /// @brief Load limit 1 — first-stage current limit (A). ROProperty LLM1; + /// @brief Load limit 2 — second-stage current limit (A). ROProperty LLM2; + /// @brief Load limit 3 — third-stage current limit (A). ROProperty LLM3; + /// @brief Controller software/firmware version string (e.g. "SW V3 SV3a"). ROProperty SVER; + /// @brief Controller model identifier string. ROProperty Model; + /// @brief Serial number part 1. ROProperty SerialNo1; + /// @brief Serial number part 2. ROProperty SerialNo2; + /// @brief Dipswitch 1 state. ROProperty D1; + /// @brief Dipswitch 2 state. ROProperty D2; + /// @brief Dipswitch 3 state. ROProperty D3; + /// @brief Dipswitch 4 state. ROProperty D4; + /// @brief Dipswitch 5 state. ROProperty D5; + /// @brief Dipswitch 6 state. ROProperty D6; + /// @brief Pump configuration string. ROProperty Pump; + /// @brief Lock state bitmask. ROProperty LS; + /// @brief True when high-voltage supply is present. ROProperty HV; + /// @brief Snooze mode remaining time (minutes). ROProperty SnpMR; + /// @brief Operational status string (e.g. "Filtering", "Heating"). ROProperty Status; + /// @brief Priming cycle count. ROProperty PrimeCount; + /// @brief Error/fault code. 0 = no fault. ROProperty EC; + /// @brief Heater ambient air temperature x10 (°C). ROProperty HAMB; + /// @brief Heater connection/conductivity value. ROProperty HCON; // R4 + /// @brief Service interval 1 countdown (hours remaining). ROProperty Ser1_Timer; + /// @brief Service interval 2 countdown (hours remaining). ROProperty Ser2_Timer; + /// @brief Service interval 3 countdown (hours remaining). ROProperty Ser3_Timer; + /// @brief Heating mode (0=Element, 1=Heat pump, 2=Both, 3=Cool). ROProperty HeatMode; + /// @brief Pump idle timer (minutes since last use). ROProperty PumpIdleTimer; + /// @brief Pump run timer (minutes of continuous run). ROProperty PumpRunTimer; + /// @brief Adaptive pool temperature hysteresis x10 (°C). ROProperty AdtPoolHys; + /// @brief Adaptive heater temperature hysteresis x10 (°C). ROProperty AdtHeaterHys; + /// @brief Instantaneous power consumption x10 (W). e.g. 35 = 3.5 kW. ROProperty Power; + /// @brief Cumulative energy consumption x100 (kWh). ROProperty Power_kWh; + /// @brief Energy consumed today x10 (Wh). ROProperty Power_Today; + /// @brief Energy consumed yesterday x10 (Wh). ROProperty Power_Yesterday; + /// @brief Thermal cut-out trip count. ROProperty ThermalCutOut; + /// @brief Test/diagnostic output D1 state. ROProperty Test_D1; + /// @brief Test/diagnostic output D2 state. ROProperty Test_D2; + /// @brief Test/diagnostic output D3 state. ROProperty Test_D3; + /// @brief Element heat source temperature offset x10 (°C). ROProperty ElementHeatSourceOffset; + /// @brief Detected mains frequency (Hz). ROProperty Frequency; + /// @brief Heat pump heat mode source temperature offset x10 (°C). ROProperty HPHeatSourceOffset_Heat; + /// @brief Heat pump cool mode source temperature offset x10 (°C). ROProperty HPHeatSourceOffset_Cool; + /// @brief Heat source off-time (minutes). ROProperty HeatSourceOffTime; + /// @brief Variable speed pump current speed setting. ROProperty Vari_Speed; + /// @brief Variable speed pump output percentage (%). ROProperty Vari_Percent; + /// @brief Variable speed pump mode (0=Auto, 1=Manual). ROProperty Vari_Mode; // R5 + /// @brief Blower/air injector operating state (0=Off, 1=On). ROProperty RB_TP_Blower; + /// @brief Target water temperature set point x10 (°C). Read-only mirror; write via STMP. ROProperty WTMP; + /// @brief True when auto mode is active. ROProperty RB_TP_Auto; + /// @brief True when heating or cooling is actively running. ROProperty RB_TP_Heater; + /// @brief True when ozone/UV sanitiser is running. ROProperty RB_TP_Ozone; + /// @brief True when spa is sleeping due to a sleep timer. ROProperty RB_TP_Sleep; + /// @brief True when a clean cycle is in progress. ROProperty CleanCycle; // R6 + /// @brief 24-hour operation flag (0=Off, 1=On). ROProperty L_24HOURS; + /// @brief Power save level (0=Off, 1=Low, 2=High). ROProperty PSAV_LVL; + /// @brief Power save start time encoded as h*256+m. ROProperty PSAV_BGN; + /// @brief Power save end time encoded as h*256+m. ROProperty PSAV_END; + /// @brief Default touchpad screen index shown when waking. ROProperty DefaultScrn; + /// @brief Touchpad sleep timeout (minutes, 0=never). ROProperty TOUT; + /// @brief OEM brand identifier. ROProperty BRND; + /// @brief Priming mode setting. ROProperty PRME; + /// @brief Heating element type/configuration. ROProperty ELMT; + /// @brief System type identifier. ROProperty TYPE; + /// @brief Gas heating installed (0=No, 1=Yes). ROProperty GAS; + /// @brief True when variable-speed pump is fitted. ROProperty VPMP; + /// @brief True when HiFi audio output is enabled. ROProperty HIFI; // R7 + /// @brief Water cleaning (ozone/UV) cycle duration (minutes). ROProperty WCLNTime; + /// @brief Maximum mains voltage recorded this session (V). ROProperty V_Max; + /// @brief Minimum mains voltage recorded this session (V). ROProperty V_Min; + /// @brief Maximum mains voltage recorded in the last 24 hours (V). ROProperty V_Max_24; + /// @brief Minimum mains voltage recorded in the last 24 hours (V). ROProperty V_Min_24; + /// @brief Current sensor zero-point calibration offset. ROProperty CurrentZero; + /// @brief Current sensor gain calibration adjustment. ROProperty CurrentAdjust; + /// @brief Voltage sensor calibration adjustment. ROProperty VoltageAdjust; + /// @brief Service interval 1 period (hours). ROProperty Ser1; + /// @brief Service interval 2 period (hours). ROProperty Ser2; + /// @brief Service interval 3 period (hours). ROProperty Ser3; + /// @brief Maximum permitted supply voltage (V). ROProperty VMAX; + /// @brief Adaptive hysteresis setting x10 (°C). ROProperty AHYS; + /// @brief Minimum power level for load management (kW x10). ROProperty PMIN; + /// @brief Filtration pump power draw (kW x10). ROProperty PFLT; + /// @brief Heater element power draw (kW x10). ROProperty PHTR; + /// @brief Maximum total power for load management (kW x10). ROProperty PMAX; + /// @brief True when temperature display is in °F, false for °C. ROProperty TemperatureUnits; + /// @brief True when ozone/sanitiser output is manually disabled. ROProperty OzoneOff; + /// @brief True when ozone/sanitiser runs 24 hours, false for auto-controlled. ROProperty Ozone24; + /// @brief True when circulation pump runs 24 hours, false for auto-controlled. ROProperty Circ24; + /// @brief True when the circulation jet boost is active. ROProperty CJET; + /// @brief True when variable-power element operation is enabled. ROProperty VELE; + /// @brief True when heater usage is disabled (1=Off). ROProperty HUSE; - // R9/RA/RB fault logs + // R9/RA/RB fault logs — most recent 3 faults (F1=most recent) + /// @brief Fault 1 hour-of-day at time of fault. ROProperty F1_HR; + /// @brief Fault 1 time-of-day encoded as h*256+m. ROProperty F1_Time; + /// @brief Fault 1 error code. ROProperty F1_ER; + /// @brief Fault 1 current reading x10 (A) at time of fault. ROProperty F1_I; + /// @brief Fault 1 voltage reading (V) at time of fault. ROProperty F1_V; + /// @brief Fault 1 pool temperature x10 (°C) at time of fault. ROProperty F1_PT; + /// @brief Fault 1 heater temperature x10 (°C) at time of fault. ROProperty F1_HT; + /// @brief Fault 1 case temperature x10 (°C) at time of fault. ROProperty F1_CT; + /// @brief Fault 1 pump state at time of fault. ROProperty F1_PU; + /// @brief Fault 1 system status at time of fault. ROProperty F1_ST; + /// @brief Fault 1 variable element state at time of fault. ROProperty F1_VE; + /// @brief Fault 2 hour-of-day at time of fault. ROProperty F2_HR; + /// @brief Fault 2 time-of-day encoded as h*256+m. ROProperty F2_Time; + /// @brief Fault 2 error code. ROProperty F2_ER; + /// @brief Fault 2 current reading x10 (A) at time of fault. ROProperty F2_I; + /// @brief Fault 2 voltage reading (V) at time of fault. ROProperty F2_V; + /// @brief Fault 2 pool temperature x10 (°C) at time of fault. ROProperty F2_PT; + /// @brief Fault 2 heater temperature x10 (°C) at time of fault. ROProperty F2_HT; + /// @brief Fault 2 case temperature x10 (°C) at time of fault. ROProperty F2_CT; + /// @brief Fault 2 pump state at time of fault. ROProperty F2_PU; + /// @brief Fault 2 system status at time of fault. ROProperty F2_ST; + /// @brief Fault 2 variable element state at time of fault. ROProperty F2_VE; + /// @brief Fault 3 hour-of-day at time of fault. ROProperty F3_HR; + /// @brief Fault 3 time-of-day encoded as h*256+m. ROProperty F3_Time; + /// @brief Fault 3 error code. ROProperty F3_ER; + /// @brief Fault 3 current reading x10 (A) at time of fault. ROProperty F3_I; + /// @brief Fault 3 voltage reading (V) at time of fault. ROProperty F3_V; + /// @brief Fault 3 pool temperature x10 (°C) at time of fault. ROProperty F3_PT; + /// @brief Fault 3 heater temperature x10 (°C) at time of fault. ROProperty F3_HT; + /// @brief Fault 3 case temperature x10 (°C) at time of fault. ROProperty F3_CT; + /// @brief Fault 3 pump state at time of fault. ROProperty F3_PU; + /// @brief Fault 3 system status at time of fault. ROProperty F3_ST; + /// @brief Fault 3 variable element state at time of fault. ROProperty F3_VE; // RE heatpump + /// @brief Heat pump unit installed (0=No, 1=Yes). ROProperty HP_Present; + /// @brief Heat pump ambient air temperature x10 (°C). ROProperty HP_Ambient; + /// @brief Heat pump condenser temperature x10 (°C). ROProperty HP_Condensor; + /// @brief Heat pump operating state code. ROProperty HP_State; + /// @brief Heat pump mode (0=Auto, 1=Heat, 2=Cool, 3=Off). ROProperty HP_Mode; + /// @brief Heat pump defrost cycle timer (minutes). ROProperty HP_Defrost_Timer; + /// @brief Heat pump compressor cumulative run timer (minutes). ROProperty HP_Comp_Run_Timer; + /// @brief Heat pump low ambient temperature protection timer (minutes). ROProperty HP_Low_Temp_Timer; + /// @brief Heat pump heat accumulation timer (minutes). ROProperty HP_Heat_Accum_Timer; + /// @brief Heat pump start sequence timer (seconds). ROProperty HP_Sequence_Timer; + /// @brief Heat pump warning/fault code. 0 = no warning. ROProperty HP_Warning; + /// @brief Freeze protection activation timer (minutes). ROProperty FrezTmr; + /// @brief Defrost cycle start temperature x10 (°C). ROProperty DBGN; + /// @brief Defrost cycle end temperature x10 (°C). ROProperty DEND; + /// @brief Defrost compressor run time limit (minutes). ROProperty DCMP; + /// @brief Defrost maximum duration (minutes). ROProperty DMAX; + /// @brief Defrost element activation delay (minutes). ROProperty DELE; + /// @brief Defrost pump operating mode. ROProperty DPMP; + /// @brief True when heat pump compressor is running. ROProperty HP_Compressor_State; + /// @brief True when heat pump fan is running. ROProperty HP_Fan_State; + /// @brief True when heat pump 4-way reversing valve is active (cool mode). ROProperty HP_4W_Valve; + /// @brief True when heat pump auxiliary heater element is active. ROProperty HP_Heater_State; // RG + /// @brief Pump 1 installation configuration string. + /// @details Format "I-S-PPP" where I=installed (1/0), S=speed type (1=single, 2=dual), + /// PPP=possible states (e.g. "014" = Off/On/Auto). Example: "1-1-014". ROProperty Pump1InstallState; + /// @brief Pump 2 installation configuration string. See Pump1InstallState for format. ROProperty Pump2InstallState; + /// @brief Pump 3 installation configuration string. See Pump1InstallState for format. ROProperty Pump3InstallState; + /// @brief Pump 4 installation configuration string. See Pump1InstallState for format. ROProperty Pump4InstallState; + /// @brief Pump 5 installation configuration string. See Pump1InstallState for format. ROProperty Pump5InstallState; + /// @brief True when pump 1 is in a safe state to start. ROProperty Pump1OkToRun; + /// @brief True when pump 2 is in a safe state to start. ROProperty Pump2OkToRun; + /// @brief True when pump 3 is in a safe state to start. ROProperty Pump3OkToRun; + /// @brief True when pump 4 is in a safe state to start. ROProperty Pump4OkToRun; + /// @brief True when pump 5 is in a safe state to start. ROProperty Pump5OkToRun; /// @brief Water temperature set point x10. From baf1d206d9fc8cf9b300a11900f166446d8aa9ab Mon Sep 17 00:00:00 2001 From: wayne-love Date: Thu, 12 Mar 2026 17:30:19 +1100 Subject: [PATCH 31/44] Comment tidy up --- lib/SpaInterface/SpaInterface.h | 231 +++++++++++++++----------------- 1 file changed, 106 insertions(+), 125 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index a06aa7c..c3edf11 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -497,9 +497,15 @@ class SpaInterface { ROProperty CaseTemperature; /// @brief Port current draw x10 (A). ROProperty PortCurrent; + /// @brief Current day of week on Spa RTC. + /// @details Read/write. 0=Monday .. 6=Sunday. + RWProperty SpaDayOfWeek{this, &SpaInterface::setSpaDayOfWeek, SpaDayOfWeek_Map}; + /// @brief Spa RTC clock value. + /// @details Read/write. Writing sends S01..S05 + S06 to the controller. + RWProperty SpaTime{this, &SpaInterface::setSpaTime}; /// @brief Heater element temperature x10 (°C). ROProperty HeaterTemperature; - /// @brief Spa pool/water temperature x10 (°C). e.g. 380 = 38.0°C. + /// @brief Pool temperature x10 (°C). Note: often returns unreliable values; use WTMP for actual water temperature. ROProperty PoolTemperature; /// @brief Minutes remaining before spa returns to sleep. ROProperty AwakeMinutesRemaining; @@ -509,7 +515,7 @@ class SpaInterface { ROProperty FiltPumpReqMins; /// @brief Load management timeout counter. ROProperty LoadTimeOut; - /// @brief Total controller run time (hours). + /// @brief Total controller run time x10 (hours). e.g. 33207 = 3320.7 hours. ROProperty HourMeter; /// @brief Relay 1 output state. ROProperty Relay1; @@ -532,15 +538,15 @@ class SpaInterface { /// @brief True when water is detected in the spa. ROProperty WaterPresent; // R3 - /// @brief Current limit setting (A). + /// @brief Current limit setting (A). Range 10–60A; should match the circuit breaker rating feeding the spa (C.LMT OEM setting). ROProperty CLMT; - /// @brief Mains phase configuration (1 = single phase, 3 = three phase). + /// @brief Mains phase configuration (1=Single Phase, 2=Dual Phase, 3=Three Phase). ROProperty PHSE; - /// @brief Load limit 1 — first-stage current limit (A). + /// @brief Phase 1 load limit — maximum number of loads (pumps/blower) allowed to run simultaneously on phase 1. Range 1–5 (x.LLM OEM setting). ROProperty LLM1; - /// @brief Load limit 2 — second-stage current limit (A). + /// @brief Phase 2 load limit — maximum number of loads (pumps/blower) allowed to run simultaneously on phase 2. Range 1–5 (x.LLM OEM setting). ROProperty LLM2; - /// @brief Load limit 3 — third-stage current limit (A). + /// @brief Phase 3 load limit — maximum number of loads (pumps/blower) allowed to run simultaneously on phase 3. Range 1–5 (x.LLM OEM setting). ROProperty LLM3; /// @brief Controller software/firmware version string (e.g. "SW V3 SV3a"). ROProperty SVER; @@ -550,23 +556,21 @@ class SpaInterface { ROProperty SerialNo1; /// @brief Serial number part 2. ROProperty SerialNo2; - /// @brief Dipswitch 1 state. + /// @brief Dipswitch 1 state. SW1: Circulation pump fitted (ON=Fitted, OFF=Not Fitted). ROProperty D1; - /// @brief Dipswitch 2 state. + /// @brief Dipswitch 2 state. SW2: Pump 1 type (ON=Two Speed, OFF=Single Speed; if OFF, Pump 2 assumed fitted). ROProperty D2; - /// @brief Dipswitch 3 state. + /// @brief Dipswitch 3 state. SW3: SV2/SV4: Pump 3 type (ON=Two Speed, OFF=Single Speed); SV3: Pump 3 fitted (ON=Fitted, OFF=Not Fitted). Not used on SV2/SV2-VH. ROProperty D3; - /// @brief Dipswitch 4 state. + /// @brief Dipswitch 4 state. SW4: SV2/SV4: Pump 4 fitted (ON=Fitted, OFF=Not Fitted; not used on SV2/SV2-VH); SV3: Not used. ROProperty D4; - /// @brief Dipswitch 5 state. + /// @brief Dipswitch 5 state. SW5: Phase input selection (ON=2/3 Phase, OFF=Single Phase). Enables SW6 when ON. ROProperty D5; - /// @brief Dipswitch 6 state. + /// @brief Dipswitch 6 state. SW6: Multi-phase type, enabled when SW5=ON (ON=Three Phase, OFF=Two Phase). ROProperty D6; /// @brief Pump configuration string. ROProperty Pump; - /// @brief Lock state bitmask. ROProperty LS; - /// @brief True when high-voltage supply is present. ROProperty HV; /// @brief Snooze mode remaining time (minutes). ROProperty SnpMR; @@ -581,6 +585,9 @@ class SpaInterface { /// @brief Heater connection/conductivity value. ROProperty HCON; // R4 + /// @brief Spa operating mode. + /// @details Read/write. 0=NORM, 1=ECON, 2=AWAY, 3=WEEK. + RWProperty Mode{this, &SpaInterface::setMode, Mode_Map}; /// @brief Service interval 1 countdown (hours remaining). ROProperty Ser1_Timer; /// @brief Service interval 2 countdown (hours remaining). @@ -630,21 +637,63 @@ class SpaInterface { /// @brief Variable speed pump mode (0=Auto, 1=Manual). ROProperty Vari_Mode; // R5 - /// @brief Blower/air injector operating state (0=Off, 1=On). + /// @brief Blower/air injector operating state. Note: encoding unknown; this property is never populated from the RF response. ROProperty RB_TP_Blower; - /// @brief Target water temperature set point x10 (°C). Read-only mirror; write via STMP. - ROProperty WTMP; - /// @brief True when auto mode is active. - ROProperty RB_TP_Auto; - /// @brief True when heating or cooling is actively running. - ROProperty RB_TP_Heater; - /// @brief True when ozone/UV sanitiser is running. - ROProperty RB_TP_Ozone; /// @brief True when spa is sleeping due to a sleep timer. ROProperty RB_TP_Sleep; + /// @brief True when ozone/UV sanitiser is running. + ROProperty RB_TP_Ozone; + /// @brief True when heating or cooling is actively running. + ROProperty RB_TP_Heater; + /// @brief True when auto mode is active. + ROProperty RB_TP_Auto; + /// @brief Light on/off state. + /// @details Read/write. 0=Off, 1=On. + RWProperty RB_TP_Light{this, &SpaInterface::setRB_TP_Light}; + /// @brief Actual (measured) water temperature x10 (°C). e.g. 376 = 37.6°C. For the set point see STMP. + ROProperty WTMP; /// @brief True when a clean cycle is in progress. ROProperty CleanCycle; + /// @brief Pump 1 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump1{this, &SpaInterface::setRB_TP_Pump1}; + /// @brief Pump 2 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump2{this, &SpaInterface::setRB_TP_Pump2}; + /// @brief Pump 3 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump3{this, &SpaInterface::setRB_TP_Pump3}; + /// @brief Pump 4 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump4{this, &SpaInterface::setRB_TP_Pump4}; + /// @brief Pump 5 operating state. + /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). + RWProperty RB_TP_Pump5{this, &SpaInterface::setRB_TP_Pump5}; // R6 + /// @brief Variable pump/blower speed. + /// @details Read/write. Valid range 1..5. + RWProperty VARIValue{this, &SpaInterface::setVARIValue}; + /// @brief Light brightness. + /// @details Read/write. Valid range 1..5. + RWProperty LBRTValue{this, &SpaInterface::setLBRTValue}; + /// @brief Light color index. + /// @details Read/write. Valid range 0..31. + RWProperty CurrClr{this, &SpaInterface::setCurrClr, CurrClr_Map}; + /// @brief Light effect/mode. + /// @details Read/write. 0=White, 1=Color, 2=Fade, 3=Step, 4=Party. + RWProperty ColorMode{this, &SpaInterface::setColorMode, ColorMode_Map}; + /// @brief Light effect speed. + /// @details Read/write. Valid range 1..5. + RWProperty LSPDValue{this, &SpaInterface::setLSPDValue, LSPDValue_Map}; + /// @brief Filtration run time per block (hours). + /// @details Read/write. Valid range 1..24. + RWProperty FiltHrs{this, &SpaInterface::setFiltHrs}; + /// @brief Filtration block duration (hours). + /// @details Read/write. Valid values: 1, 2, 3, 4, 6, 8, 12, 24. + RWProperty FiltBlockHrs{this, &SpaInterface::setFiltBlockHrs, FiltBlockHrs_Map}; + /// @brief Water temperature set point x10. + /// @details Read/write. Valid range 50..410 (5.0°C..41.0°C). + RWProperty STMP{this, &SpaInterface::setSTMP}; /// @brief 24-hour operation flag (0=Off, 1=On). ROProperty L_24HOURS; /// @brief Power save level (0=Off, 1=Low, 2=High). @@ -653,9 +702,27 @@ class SpaInterface { ROProperty PSAV_BGN; /// @brief Power save end time encoded as h*256+m. ROProperty PSAV_END; + /// @brief Sleep timer 1 day mode bitmap. + /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. + RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, SNZ_DAY_Map}; + /// @brief Sleep timer 2 day mode bitmap. + /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. + RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, SNZ_DAY_Map}; + /// @brief Sleep timer 1 start time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). + RWProperty L_1SNZ_BGN{this, &SpaInterface::setL_1SNZ_BGN}; + /// @brief Sleep timer 2 start time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). + RWProperty L_2SNZ_BGN{this, &SpaInterface::setL_2SNZ_BGN}; + /// @brief Sleep timer 1 finish time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). + RWProperty L_1SNZ_END{this, &SpaInterface::setL_1SNZ_END}; + /// @brief Sleep timer 2 finish time. + /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). + RWProperty L_2SNZ_END{this, &SpaInterface::setL_2SNZ_END}; /// @brief Default touchpad screen index shown when waking. ROProperty DefaultScrn; - /// @brief Touchpad sleep timeout (minutes, 0=never). + /// @brief Pump and blower auto time-out duration (minutes). Range 10–60. Set via W74. ROProperty TOUT; /// @brief OEM brand identifier. ROProperty BRND; @@ -672,7 +739,7 @@ class SpaInterface { /// @brief True when HiFi audio output is enabled. ROProperty HIFI; // R7 - /// @brief Water cleaning (ozone/UV) cycle duration (minutes). + /// @brief Auto sanitise cycle start time encoded as h*256+m. Range 0–5947. e.g. 2304 = 9:00. Set via W73. ROProperty WCLNTime; /// @brief Maximum mains voltage recorded this session (V). ROProperty V_Max; @@ -718,8 +785,14 @@ class SpaInterface { ROProperty CJET; /// @brief True when variable-power element operation is enabled. ROProperty VELE; - /// @brief True when heater usage is disabled (1=Off). + /// @brief True when heat pump operation is enabled while the spa pool is in use (H.USE OEM setting). When false, heat pump is suspended during spa use. ROProperty HUSE; + /// @brief Aux element (booster) state. + /// @details Read/write. false=Off, true=On. + RWProperty HELE{this, &SpaInterface::setHELE}; + /// @brief Heatpump operating mode. + /// @details Read/write. 0=Auto, 1=Heat, 2=Cool, 3=Off. + RWProperty HPMP{this, &SpaInterface::setHPMP, HPMP_Map}; // R9/RA/RB fault logs — most recent 3 faults (F1=most recent) /// @brief Fault 1 hour-of-day at time of fault. ROProperty F1_HR; @@ -787,6 +860,10 @@ class SpaInterface { ROProperty F3_ST; /// @brief Fault 3 variable element state at time of fault. ROProperty F3_VE; + // RC + /// @brief Air blower operating mode. + /// @details Read/write. 0=Variable, 1=Ramp, 2=Off. + RWProperty Outlet_Blower{this, &SpaInterface::setOutlet_Blower, Outlet_Blower_Map}; // RE heatpump /// @brief Heat pump unit installed (0=No, 1=Yes). ROProperty HP_Present; @@ -856,120 +933,24 @@ class SpaInterface { ROProperty Pump4OkToRun; /// @brief True when pump 5 is in a safe state to start. ROProperty Pump5OkToRun; - - /// @brief Water temperature set point x10. - /// @details Read/write. Valid range 50..410 (5.0C..41.0C). - RWProperty STMP{this, &SpaInterface::setSTMP}; - - /// @brief Heatpump operating mode. - /// @details Read/write. 0=Auto, 1=Heat, 2=Cool, 3=Off. - RWProperty HPMP{this, &SpaInterface::setHPMP, HPMP_Map}; - - /// @brief Light effect/mode. - /// @details Read/write. 0=White, 1=Color, 2=Fade, 3=Step, 4=Party. - RWProperty ColorMode{this, &SpaInterface::setColorMode, ColorMode_Map}; - - /// @brief Light brightness. - /// @details Read/write. Valid range 1..5. - RWProperty LBRTValue{this, &SpaInterface::setLBRTValue}; - - /// @brief Light effect speed. - /// @details Read/write. Valid range 1..5. - RWProperty LSPDValue{this, &SpaInterface::setLSPDValue, LSPDValue_Map}; - - /// @brief Light color index. - /// @details Read/write. Valid range 0..31. - RWProperty CurrClr{this, &SpaInterface::setCurrClr, CurrClr_Map}; - - /// @brief Sleep timer 1 day mode bitmap. - /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. - RWProperty L_1SNZ_DAY{this, &SpaInterface::setL_1SNZ_DAY, SNZ_DAY_Map}; - /// @brief Sleep timer 2 day mode bitmap. - /// @details Read/write. Typical values: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays. - RWProperty L_2SNZ_DAY{this, &SpaInterface::setL_2SNZ_DAY, SNZ_DAY_Map}; - - /// @brief Sleep timer 1 start time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). - RWProperty L_1SNZ_BGN{this, &SpaInterface::setL_1SNZ_BGN}; - /// @brief Sleep timer 1 finish time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). - RWProperty L_1SNZ_END{this, &SpaInterface::setL_1SNZ_END}; - /// @brief Sleep timer 2 start time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). - RWProperty L_2SNZ_BGN{this, &SpaInterface::setL_2SNZ_BGN}; - /// @brief Sleep timer 2 finish time. - /// @details Read/write. Valid range 0..5947 encoded as h*256+m (24-hour clock). - RWProperty L_2SNZ_END{this, &SpaInterface::setL_2SNZ_END}; - - /// @brief Pump 1 operating state. - /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). - RWProperty RB_TP_Pump1{this, &SpaInterface::setRB_TP_Pump1}; - /// @brief Pump 2 operating state. - /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). - RWProperty RB_TP_Pump2{this, &SpaInterface::setRB_TP_Pump2}; - /// @brief Pump 3 operating state. - /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). - RWProperty RB_TP_Pump3{this, &SpaInterface::setRB_TP_Pump3}; - /// @brief Pump 4 operating state. - /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). - RWProperty RB_TP_Pump4{this, &SpaInterface::setRB_TP_Pump4}; - /// @brief Pump 5 operating state. - /// @details Read/write. 0=Off, 1=On, 4=Auto (if supported). - RWProperty RB_TP_Pump5{this, &SpaInterface::setRB_TP_Pump5}; - /// @brief Aux element (booster) state. - /// @details Read/write. false=Off, true=On. - RWProperty HELE{this, &SpaInterface::setHELE}; - - /// @brief Current day of week on Spa RTC. - /// @details Read/write. 0=Monday .. 6=Sunday. - RWProperty SpaDayOfWeek{this, &SpaInterface::setSpaDayOfWeek, SpaDayOfWeek_Map}; - - /// @brief Air blower operating mode. - /// @details Read/write. 0=Variable, 1=Ramp, 2=Off. - RWProperty Outlet_Blower{this, &SpaInterface::setOutlet_Blower, Outlet_Blower_Map}; - - /// @brief Spa operating mode. - /// @details Read/write. 0=NORM, 1=ECON, 2=AWAY, 3=WEEK. - RWProperty Mode{this, &SpaInterface::setMode, Mode_Map}; - - /// @brief Variable pump/blower speed. - /// @details Read/write. Valid range 1..5. - RWProperty VARIValue{this, &SpaInterface::setVARIValue}; - - /// @brief Filtration block duration (hours). - /// @details Read/write. Valid values: 1, 2, 3, 4, 6, 8, 12, 24. - RWProperty FiltBlockHrs{this, &SpaInterface::setFiltBlockHrs, FiltBlockHrs_Map}; - - /// @brief Filtration run time per block (hours). - /// @details Read/write. Valid range 1..24. - RWProperty FiltHrs{this, &SpaInterface::setFiltHrs}; - /// @brief Keypad lock mode. /// @details Read/write. 0=Unlocked, 1=Partially Locked, 2=Locked. RWProperty LockMode{this, &SpaInterface::setLockMode, LockMode_Map}; - /// @brief Spa RTC clock value. - /// @details Read/write. Writing sends S01..S05 + S06 to the controller. - RWProperty SpaTime{this, &SpaInterface::setSpaTime}; - /// @brief To be called by loop function of main sketch. Does regular updates, etc. void loop(); /// @brief Have we sucessfuly read the registers from the SpaNet controller. - /// @return + /// @return bool isInitialised(); /// @brief Set the function to be called when properties have been updated. - /// @param f + /// @param f void setUpdateCallback(void (*f)()); /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Light on/off state. - /// @details Read/write. 0=Off, 1=On. - RWProperty RB_TP_Light{this, &SpaInterface::setRB_TP_Light}; - /// @brief Unified array of RWProperty pointers for each migrated pump, used for /// both reading state and sending commands. Grows as pumps are migrated. using PumpStatus = RWProperty SpaInterface::*; From 192924431b806391ab5708dc92a64973da2c1839 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 14 Mar 2026 08:50:05 +1100 Subject: [PATCH 32/44] Add ss (send serial) command to debug interface. --- lib/SpaInterface/SpaInterface.cpp | 46 +++++++++++++++++++++++++++++++ lib/SpaInterface/SpaInterface.h | 11 ++++++++ 2 files changed, 57 insertions(+) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 79a9b0b..017e9d0 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -2,11 +2,15 @@ #define BAUD_RATE 38400 +SpaInterface* SpaInterface::_instance = nullptr; + SpaInterface::SpaInterface() : port(SPA_SERIAL) { SPA_SERIAL.setRxBufferSize(1024); //required for unit testing SPA_SERIAL.setTxBufferSize(1024); //required for unit testing SPA_SERIAL.begin(BAUD_RATE, SERIAL_8N1, RX_PIN, TX_PIN); SPA_SERIAL.setTimeout(250); + + _instance = this; } SpaInterface::~SpaInterface() {} @@ -73,6 +77,42 @@ bool SpaInterface::sendCommandCheckResult(String cmd, String expected){ return outcome; } +void SpaInterface::_processDebugCommand() { + if (_instance == nullptr) return; + + String cmd = Debug.getLastCommand(); + if (!cmd.startsWith("spa ")) return; + + String payload = cmd.substring(4); + debugI("TX: %s", payload.c_str()); + + _instance->flushSerialReadBuffer(); + _instance->port.print('\n'); + _instance->port.flush(); + delay(50); + _instance->port.printf("%s\n", payload.c_str()); + _instance->port.flush(); + + // Drain until 100ms of silence (max 2s absolute) + String response = ""; + unsigned long lastByte = millis(); + unsigned long deadline = millis() + 2000; + while (millis() - lastByte < 100 && millis() < deadline) { + while (_instance->port.available()) { + response += (char)_instance->port.read(); + lastByte = millis(); + } + } + + if (response.length() > 0) { + debugI("RX:\n%s", response.c_str()); + } else { + debugI("RX: (no response)"); + } + + _instance->_resultRegistersDirty = true; +} + bool SpaInterface::setRB_TP_Pump1(int mode){ debugD("setRB_TP_Pump1 - %i", mode); if (mode < 0 || mode > 4) { @@ -794,6 +834,12 @@ void SpaInterface::updateStatus() { void SpaInterface::loop(){ + if (!_debugInitialised) { + Debug.setHelpProjectsCmds("spa - Send raw command to spa serial and print response"); + Debug.setCallBackProjectCmds(&SpaInterface::_processDebugCommand); + _debugInitialised = true; + } + if ( _lastWaitMessage + 1000 < millis()) { debugV("Waiting..."); _lastWaitMessage = millis(); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index c3edf11..e9b5192 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -93,6 +93,14 @@ class SpaInterface { String flushSerialReadBuffer(bool returnData); + /// @brief Singleton pointer used by the static RemoteDebug callback. + static SpaInterface* _instance; + + /// @brief Static callback registered with RemoteDebug. + /// Handles the `s ` project command: sends the payload to the spa + /// serial port and prints the response via RemoteDebug. + static void _processDebugCommand(); + /// @brief Stores millis time at which next update should occur unsigned long _nextUpdateDue = 0; @@ -102,6 +110,9 @@ class SpaInterface { /// @brief If the result registers have been modified locally, need to do a fress pull from the controller bool _resultRegistersDirty = true; + /// @brief True once RemoteDebug project commands have been registered (deferred to first loop() call). + bool _debugInitialised = false; + void (*updateCallback)() = nullptr; From 017965a28c5b75072119844546cee98343a68d1f Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 14 Mar 2026 08:51:29 +1100 Subject: [PATCH 33/44] Change debug cmd from spa to ss --- lib/SpaInterface/SpaInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 017e9d0..5ff8931 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -81,7 +81,7 @@ void SpaInterface::_processDebugCommand() { if (_instance == nullptr) return; String cmd = Debug.getLastCommand(); - if (!cmd.startsWith("spa ")) return; + if (!cmd.startsWith("ss ")) return; String payload = cmd.substring(4); debugI("TX: %s", payload.c_str()); @@ -835,7 +835,7 @@ void SpaInterface::updateStatus() { void SpaInterface::loop(){ if (!_debugInitialised) { - Debug.setHelpProjectsCmds("spa - Send raw command to spa serial and print response"); + Debug.setHelpProjectsCmds("ss - Send raw command to spa serial and print response"); Debug.setCallBackProjectCmds(&SpaInterface::_processDebugCommand); _debugInitialised = true; } From ed7da15ae2263575b962c6ec8a6e1ff4f1cbe1c2 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 22 Mar 2026 09:07:04 +1100 Subject: [PATCH 34/44] Fix bad option list for blower, extend ROProperty for runtime modification of value map list. --- .gitignore | 1 + lib/HAAutoDiscovery/HAAutoDiscovery.cpp | 38 ---------------------- lib/HAAutoDiscovery/HAAutoDiscovery.h | 43 ++++++++++++++++++++++++- lib/SpaInterface/SpaInterface.h | 27 ++++++++++++++-- src/main.cpp | 20 ++++-------- 5 files changed, 74 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index ac60914..f7c9bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .vscode/extensions.json .DS_Store .vscode/extensions.json +.vscode/extensions.json diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.cpp b/lib/HAAutoDiscovery/HAAutoDiscovery.cpp index 766af40..90bcc53 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.cpp +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.cpp @@ -79,44 +79,6 @@ void generateSwitchAdJSON(String& output, const AutoDiscoveryInformationTemplate serializeJson(json, output); } -void generateFanAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, int min, int max, const String* modes, const size_t modesSize) { - JsonDocument json; - generateCommonAdJSON(json, config, spa, discoveryTopic, "fan"); - - // Find the last character that is not a space or curly brace - int lastIndex = config.valueTemplate.length() - 1; - while (lastIndex >= 0 && (config.valueTemplate[lastIndex] == ' ' || config.valueTemplate[lastIndex] == '}')) { - lastIndex--; - } - json["state_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".state" + config.valueTemplate.substring(lastIndex + 1); - json["command_topic"] = spa.commandTopic + "/" + config.propertyId + "_state"; - - if (max > min) { - json["percentage_state_topic"] = spa.stateTopic; - json["percentage_command_topic"] = spa.commandTopic + "/" + config.propertyId + "_speed"; - json["percentage_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".speed" + config.valueTemplate.substring(lastIndex + 1); - - json["speed_range_min"]=min; - json["speed_range_max"]=max; - } - - if (modes != nullptr && modesSize > 0) { - json["preset_mode_state_topic"] = spa.stateTopic; - json["preset_mode_command_topic"] = spa.commandTopic + "/" + config.propertyId + "_mode"; - json["preset_mode_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".mode" + config.valueTemplate.substring(lastIndex + 1); - - JsonArray jsonModes = json["preset_modes"].to(); - //for (const auto& mode : *modes) jsonModes.add(mode); - for (size_t i = 0; i < modesSize; ++i) jsonModes.add(modes[i]); - } - - if (config.propertyId.startsWith("pump")) { - json["icon"] = "mdi:pump"; - } - - serializeJson(json, output); -} - void generateClimateAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic) { JsonDocument json; generateCommonAdJSON(json, config, spa, discoveryTopic, "climate"); diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.h b/lib/HAAutoDiscovery/HAAutoDiscovery.h index d847f38..84d88bf 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.h +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.h @@ -68,7 +68,48 @@ void generateSelectAdJSON(String& output, const AutoDiscoveryInformationTemplate serializeJson(json, output); } -void generateFanAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, int min, int max, const String* modes, const size_t modesSize=0); +template +void generateFanAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, int min, int max, const SpaInterface::ROProperty& prop) { + JsonDocument json; + generateCommonAdJSON(json, config, spa, discoveryTopic, "fan"); + + // Find the last character that is not a space or curly brace + int lastIndex = config.valueTemplate.length() - 1; + while (lastIndex >= 0 && (config.valueTemplate[lastIndex] == ' ' || config.valueTemplate[lastIndex] == '}')) { + lastIndex--; + } + json["state_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".state" + config.valueTemplate.substring(lastIndex + 1); + json["command_topic"] = spa.commandTopic + "/" + config.propertyId + "_state"; + + if (max > min) { + json["percentage_state_topic"] = spa.stateTopic; + json["percentage_command_topic"] = spa.commandTopic + "/" + config.propertyId + "_speed"; + json["percentage_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".speed" + config.valueTemplate.substring(lastIndex + 1); + + json["speed_range_min"]=min; + json["speed_range_max"]=max; + } + + const size_t count = prop.getLabelCount(); + if (count > 0) { + json["preset_mode_state_topic"] = spa.stateTopic; + json["preset_mode_command_topic"] = spa.commandTopic + "/" + config.propertyId + "_mode"; + json["preset_mode_value_template"] = config.valueTemplate.substring(0, lastIndex + 1) + ".mode" + config.valueTemplate.substring(lastIndex + 1); + + JsonArray jsonModes = json["preset_modes"].to(); + for (size_t i = 0; i < count; i++) jsonModes.add(prop.getLabelAt(i)); + + } + + if (config.propertyId.startsWith("pump")) { + json["icon"] = "mdi:pump"; + } + + serializeJson(json, output); + +} + + template void generateLightAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, const std::array& colorModes) { diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index e9b5192..b59cd98 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -265,6 +266,7 @@ class SpaInterface { operator T() const { return _value; } void setCallback(void (*c)(T)) { _callback = c; } void clearCallback() { _callback = nullptr; } + // Returns the matching label for the current value, or fallback if not found. const char* getLabel(const char* fallback = "Unknown") const { if (!_map || _mapSize == 0) { @@ -277,12 +279,32 @@ class SpaInterface { } return fallback; } + // Expose label/value map for building UI dropdowns. const LabelValue* getLabelMap(size_t& count) const { count = _mapSize; return _map; } + + /// @brief Replace the label/value map at runtime. The map data is copied + /// into the property so the caller's array does not need to outlive this call. + template + void setLabelMap(const LabelValue (&map)[N]) { + _ownedMap.assign(map, map + N); + _map = _ownedMap.data(); + _mapSize = _ownedMap.size(); + } + + /// @brief Replace the label/value map at runtime using a braced initialiser list, + /// e.g. setLabelMap({{"On",1},{"Off",0}}). The data is owned by the property. + void setLabelMap(std::initializer_list map) { + _ownedMap.assign(map); + _map = _ownedMap.data(); + _mapSize = _ownedMap.size(); + } + size_t getLabelCount() const { return _mapSize; } + const char* getLabelAt(size_t index, const char* fallback = "Unknown") const { if (!_map || index >= _mapSize) { return fallback; @@ -296,6 +318,7 @@ class SpaInterface { const LabelValue* _map = nullptr; size_t _mapSize = 0; void (*_callback)(T) = nullptr; + std::vector _ownedMap; // Called by SpaInterface when a fresh value is received from the spa. void update(T newValue) { @@ -962,8 +985,8 @@ class SpaInterface { /// @brief Clear the call back function. void clearUpdateCallback(); - /// @brief Unified array of RWProperty pointers for each migrated pump, used for - /// both reading state and sending commands. Grows as pumps are migrated. + /// @brief Unified array of RWProperty pointers for eachpump, used for + /// both reading state and sending commands. using PumpStatus = RWProperty SpaInterface::*; static constexpr PumpStatus pumpStatuses[] = { &SpaInterface::RB_TP_Pump1, diff --git a/src/main.cpp b/src/main.cpp index 67c2e65..80fe175 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -272,16 +272,14 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "pump" + String(pumpNumber); ADConf.valueTemplate = "{{ value_json.pumps.pump" + String(pumpNumber) + " }}"; if (pumpInstallState.endsWith("4")) { - selectedPumpOptions = si.autoPumpOptions.data(); - arrSize = si.autoPumpOptions.size(); - } else { - selectedPumpOptions = nullptr; - arrSize = 0; + + (si.*(SpaInterface::pumpStatuses[pumpNumber-1])).setLabelMap({{"Manual",3},{"Auto",4}}); + } if (getPumpSpeedType(pumpInstallState) == "1") { - generateFanAdJSON(output, ADConf, spa, discoveryTopic, 0, 0, selectedPumpOptions, arrSize); + generateFanAdJSON(output, ADConf, spa, discoveryTopic, 0, 0, (si.*(SpaInterface::pumpStatuses[pumpNumber-1]))); } else { - generateFanAdJSON(output, ADConf, spa, discoveryTopic, getPumpSpeedMin(pumpInstallState), getPumpSpeedMax(pumpInstallState), selectedPumpOptions, arrSize); + generateFanAdJSON(output, ADConf, spa, discoveryTopic, getPumpSpeedMin(pumpInstallState), getPumpSpeedMax(pumpInstallState), (si.*(SpaInterface::pumpStatuses[pumpNumber-1]))); } mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); } @@ -408,13 +406,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "blower"; ADConf.deviceClass = ""; ADConf.entityCategory = ""; - // blowerModes bridges the type mismatch between Outlet_Blower_Map (ROProperty::LabelValue[], - // which includes "Off") and generateFanAdJSON which expects const String* of preset mode names only. - // "Off" is a state rather than a preset mode so is intentionally excluded here. - // TODO: future improvement — update generateFanAdJSON to accept the label map directly, skipping - // entries that represent the off state, to eliminate this bridging array. - static const String blowerModes[] = {"Variable", "Ramp"}; - generateFanAdJSON(output, ADConf, spa, discoveryTopic, 1, 5, blowerModes, 2); + generateFanAdJSON(output, ADConf, spa, discoveryTopic, 1, 5, si.Outlet_Blower); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Spa Mode"; From 9b78f316f02f288484d9a4bdfd90c2d9f71e0749 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 5 Apr 2026 07:57:05 +1000 Subject: [PATCH 35/44] Add Heat Element Current measure --- SpaNET Debug Files/register-map.md | 325 +++++++++++++++++++++++++++++ lib/SpaInterface/SpaInterface.h | 3 +- lib/SpaUtils/SpaUtils.cpp | 1 + src/main.cpp | 11 + 4 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 SpaNET Debug Files/register-map.md diff --git a/SpaNET Debug Files/register-map.md b/SpaNET Debug Files/register-map.md new file mode 100644 index 0000000..ec64479 --- /dev/null +++ b/SpaNET Debug Files/register-map.md @@ -0,0 +1,325 @@ +# SpaNET SpaLINK Register Field Map + +Fields are addressed as `RN+offset` where N is the register name and offset is the 1-based position within the CSV response line for that register. + +**Write command notation:** `W##` = write command (prefixed with value), `S##` = state command (prefixed with value). Toggle commands (no value) are noted explicitly. + +--- + +## R2 — Electrical / System Status + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| R2+1 | `MainsCurrent` | Mains current draw ×10 (A). e.g. 77 = 7.7 A | — | +| R2+2 | `MainsVoltage` | Mains supply voltage (V) | — | +| R2+3 | `CaseTemperature` | Controller case temperature ×10 (°C) | — | +| R2+4 | `PortCurrent` | Port current draw ×10 (A) | — | +| R2+5 | `SpaDayOfWeek` | Day of week on spa RTC. 0=Mon…6=Sun | `S06:<0-6>` | +| R2+6 | *(SpaTime — hour)* | Hour component of spa RTC | `S04:` | +| R2+7 | *(SpaTime — minute)* | Minute component of spa RTC | `S05:` | +| R2+8 | *(SpaTime — second)* | Second component of spa RTC | — | +| R2+9 | *(SpaTime — day)* | Day-of-month component of spa RTC | `S03:` | +| R2+10 | *(SpaTime — month)* | Month component of spa RTC | `S02:` | +| R2+11 | *(SpaTime — year)* | Year component of spa RTC (2-digit) | `S01:` | +| R2+12 | `HeaterTemperature` | Heating element temperature ×10 (°C) | — | +| R2+13 | `PoolTemperature` | Pool/secondary sensor temperature ×10 (°C). Often unreliable (999.9); use `WTMP` instead | — | +| R2+14 | `WaterPresent` | Water presence detected. 1=Yes, 0=No | — | +| R2+15 | *(TempOffset)* | Temperature offset calibration. Not mapped in code | — | +| R2+16 | `AwakeMinutesRemaining` | Minutes before spa returns to sleep/standby | — | +| R2+17 | `FiltPumpRunTimeTotal` | Total cumulative filtration pump run time (minutes) | — | +| R2+18 | `FiltPumpReqMins` | Remaining filtration minutes required this cycle | — | +| R2+19 | `LoadTimeOut` | Load management timeout counter (seconds) | — | +| R2+20 | `HourMeter` | Total controller run time ×10 (hours). e.g. 279654 = 27965.4 h | — | +| R2+21 | `Relay1` | Relay 1 cumulative run time or state | — | +| R2+22 | `Relay2` | Relay 2 cumulative run time or state | — | +| R2+23 | `Relay3` | Relay 3 cumulative run time or state | — | +| R2+24 | `Relay4` | Relay 4 cumulative run time or state | — | +| R2+25 | `Relay5` | Relay 5 cumulative run time or state | — | +| R2+26 | `Relay6` | Relay 6 cumulative run time or state | — | +| R2+27 | `Relay7` | Relay 7 cumulative run time or state | — | +| R2+28 | `Relay8` | Relay 8 cumulative run time or state | — | +| R2+29 | `Relay9` | Relay 9 cumulative run time or state | — | + +--- + +## R3 — Configuration / Identity + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| R3+1 | `CLMT` | Current limit (A). Max load allowed on supply. Range 10–60 (OEM C.LMT setting) | — | +| R3+2 | `PHSE` | Mains phase configuration. 1=Single, 2=Dual, 3=Three phase | — | +| R3+3 | `LLM1` | Phase 1 load limit — max simultaneous loads on phase 1. Range 1–5 (OEM x.LLM) | — | +| R3+4 | `LLM2` | Phase 2 load limit. Range 1–5 | — | +| R3+5 | `LLM3` | Phase 3 load limit. Range 1–5 | — | +| R3+6 | `SVER` | Firmware version string (e.g. `SW V6 19 11 12`) | — | +| R3+7 | `Model` | Controller model string (e.g. `SV3`) | — | +| R3+8 | `SerialNo1` | Serial number part 1 | — | +| R3+9 | `SerialNo2` | Serial number part 2 | — | +| R3+10 | `D1` | Dipswitch 1: Circulation pump fitted (1=Fitted) | — | +| R3+11 | `D2` | Dipswitch 2: Pump 1 type (1=Two Speed; if 0, Pump 2 assumed fitted) | — | +| R3+12 | `D3` | Dipswitch 3: SV3 = Pump 3 fitted; SV2/SV4 = Pump 3 two-speed | — | +| R3+13 | `D4` | Dipswitch 4: SV2/SV4 = Pump 4 fitted; SV3 = Not used | — | +| R3+14 | `D5` | Dipswitch 5: Phase input (1=2/3 Phase, 0=Single). Enables D6 when set | — | +| R3+15 | `D6` | Dipswitch 6: Multi-phase type when D5=1 (1=Three Phase, 0=Two Phase) | — | +| R3+16 | `Pump` | Pump configuration string | — | +| R3+17 | `LS` | Load shedding level | — | +| R3+18 | `HV` | High voltage input active (bool) | — | +| R3+19 | `SnpMR` | Snooze mode remaining time (MR). Per-second countdown; 0 when not in snooze | — | +| R3+20 | `Status` | Operational status string (e.g. `Filtering`, `In use`, `Heating`) | — | +| R3+21 | `PrimeCount` | Priming cycle count | — | +| R3+22 | `EC` | Variable heat element current draw x10 (A) | — | +| R3+23 | `HAMB` | Heater ambient air temperature ×10 (°C) | — | +| R3+24 | `HCON` | Heater conductivity/connection value | — | +| R3+25 | *(HV_2)* | High voltage 2 input state. Encoding unconfirmed; not mapped in code | — | + +--- + +## R4 — Power / Timers / Mode + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| R4+1 | `Mode` | Spa operating mode. 0=NORM, 1=ECON, 2=AWAY, 3=WEEK | `W66:<0-3>` | +| R4+2 | `Ser1_Timer` | Service interval 1 countdown (hours remaining) | — | +| R4+3 | `Ser2_Timer` | Service interval 2 countdown (hours remaining) | — | +| R4+4 | `Ser3_Timer` | Service interval 3 countdown (hours remaining) | — | +| R4+5 | `HeatMode` | Heating source mode. 0=Element, 1=Heat pump, 2=Both, 3=Cool | — | +| R4+6 | `PumpIdleTimer` | Minutes since last pump activity | — | +| R4+7 | `PumpRunTimer` | Continuous pump run time (seconds, increments each second) | — | +| R4+8 | `AdtPoolHys` | Adaptive pool temperature hysteresis ×10 (°C) | — | +| R4+9 | `AdtHeaterHys` | Adaptive heater temperature hysteresis ×10 (°C) | — | +| R4+10 | `Power` | Instantaneous power consumption ×10 (W). e.g. 35 = 3.5 kW | — | +| R4+11 | `Power_kWh` | Cumulative energy consumption ×100 (kWh) | — | +| R4+12 | `Power_Today` | Energy consumed today ×10 (Wh) | — | +| R4+13 | `Power_Yesterday` | Energy consumed yesterday ×10 (Wh) | — | +| R4+14 | `ThermalCutOut` | Thermal cut-out trip count | — | +| R4+15 | `Test_D1` | Diagnostic output D1 state | — | +| R4+16 | `Test_D2` | Diagnostic output D2 state | — | +| R4+17 | `Test_D3` | Diagnostic output D3 state | — | +| R4+18 | `ElementHeatSourceOffset` | Element heat source temperature offset ×10 (°C) | — | +| R4+19 | `Frequency` | Detected mains frequency (Hz) | — | +| R4+20 | `HPHeatSourceOffset_Heat` | Heat pump heat-mode source temperature offset ×10 (°C) | — | +| R4+21 | `HPHeatSourceOffset_Cool` | Heat pump cool-mode source temperature offset ×10 (°C) | — | +| R4+22 | `HeatSourceOffTime` | Heat source off-time (minutes) | — | +| R4+23 | `Vari_Mode` | Variable speed pump mode. 0=Auto, 1=Manual | — | +| R4+24 | `Vari_Speed` | Variable speed pump current speed setting | — | +| R4+25 | `Vari_Percent` | Variable speed pump output percentage (%) | — | + +--- + +## R5 — Touchpad / Pump / Water States + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| R5+1 | *(TouchPad2)* | Touchpad 2 type code. Encoding unknown; not mapped in code | — | +| R5+2 | *(TouchPad1)* | Touchpad 1 type code. Encoding unknown; not mapped in code | — | +| R5+3–4 | *(unknown)* | Unmapped. Possibly pump button states in touchpad encoding | — | +| R5+5 | *(RB_TP_Blower)* | Blower/air injector state. Encoding unknown; not populated in code | — | +| R5+6–9 | *(unknown)* | Unmapped fields | — | +| R5+10 | `RB_TP_Sleep` | True when spa is sleeping due to a sleep timer | — | +| R5+11 | `RB_TP_Ozone` | True when ozone/UV sanitiser is running | — | +| R5+12 | `RB_TP_Heater` | True when heating or cooling is actively running | — | +| R5+13 | `RB_TP_Auto` | True when auto mode is active | — | +| R5+14 | `RB_TP_Light` | Light state. 0=Off, 1=On | `W14` (toggle — no value argument) | +| R5+15 | `WTMP` | Actual (measured) water temperature ×10 (°C). e.g. 376 = 37.6°C | — | +| R5+16 | `CleanCycle` | True when a sanitise/clean cycle is in progress | — | +| R5+17 | *(unknown)* | Per-second countdown timer. Behaviour consistent with a touchpad activity timeout (~30 s); resets on keypad interaction | — | +| R5+18 | `RB_TP_Pump1` | Pump 1 operating state. 0=Off, 1=Speed 1, 2=Speed 2, 4=Auto | `S22:<0-4>` | +| R5+19 | `RB_TP_Pump2` | Pump 2 operating state. 0=Off, 1=On, 4=Auto | `S23:<0-4>` | +| R5+20 | `RB_TP_Pump3` | Pump 3 operating state. 0=Off, 1=On, 4=Auto | `S24:<0-4>` | +| R5+21 | `RB_TP_Pump4` | Pump 4 operating state. 0=Off, 1=On, 4=Auto | `S25:<0-4>` | +| R5+22 | `RB_TP_Pump5` | Pump 5 operating state. 0=Off, 1=On, 4=Auto | `S26:<0-4>` | + +--- + +## R6 — Settings / Lighting / Scheduling + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| R6+1 | `VARIValue` | Variable pump/blower speed level. Range 1–5 | `S13:<1-5>` | +| R6+2 | `LBRTValue` | Light brightness level. Range 1–5 | `S08:<1-5>` | +| R6+3 | `CurrClr` | Light colour index (hue angle mapped to controller colour value). Range 0–31 | `S10:<0-31>` | +| R6+4 | `ColorMode` | Light effect mode. 0=White, 1=Color, 2=Fade, 3=Step, 4=Party | `S07:<0-4>` | +| R6+5 | `LSPDValue` | Light effect speed. Range 1–5 | `S09:<1-5>` | +| R6+6 | `FiltHrs` | Filtration run time per block (hours). Range 1–24 | `W60:<1-24>` | +| R6+7 | `FiltBlockHrs` | Filtration cycle block duration (hours). Valid: 1,2,3,4,6,8,12,24 | `W90:` | +| R6+8 | `STMP` | Water temperature set point ×10 (°C). Range 50–410 (5.0–41.0°C) | `W40:` | +| R6+9 | `L_24HOURS` | 24-hour continuous operation flag. 0=Off, 1=On | — | +| R6+10 | `PSAV_LVL` | Power save level. 0=Off, 1=Low, 2=High | — | +| R6+11 | `PSAV_BGN` | Power save start time encoded as h×256+m | — | +| R6+12 | `PSAV_END` | Power save end time encoded as h×256+m | — | +| R6+13 | `L_1SNZ_DAY` | Sleep timer 1 day bitmap. 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays | `W67:` | +| R6+14 | `L_2SNZ_DAY` | Sleep timer 2 day bitmap. Same encoding as L_1SNZ_DAY | `W70:` | +| R6+15 | `L_1SNZ_BGN` | Sleep timer 1 start time encoded as h×256+m | `W68:` | +| R6+16 | `L_2SNZ_BGN` | Sleep timer 2 start time encoded as h×256+m | `W71:` | +| R6+17 | `L_1SNZ_END` | Sleep timer 1 finish time encoded as h×256+m | `W69:` | +| R6+18 | `L_2SNZ_END` | Sleep timer 2 finish time encoded as h×256+m | `W72:` | +| R6+19 | `DefaultScrn` | Default touchpad screen index shown on wake | — | +| R6+20 | `TOUT` | Pump and blower auto time-out duration (minutes). Range 10–60 | — | +| R6+21 | `VPMP` | Variable-speed pump fitted (bool) | — | +| R6+22 | `HIFI` | HiFi audio output enabled (bool) | — | +| R6+23 | `BRND` | OEM brand identifier | — | +| R6+24 | `PRME` | Priming mode setting *(V3 firmware only)* | — | +| R6+25 | `ELMT` | Heating element type/configuration *(V3 firmware only)* | — | +| R6+26 | `TYPE` | System type identifier *(V3 firmware only)* | — | +| R6+27 | `GAS` | Gas heating installed. 0=No, 1=Yes *(V3 firmware only)* | — | + +--- + +## R7 — Calibration / Sanitise / Advanced Settings + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| R7+1 | `WCLNTime` | Auto sanitise cycle start time encoded as h×256+m. e.g. 3072 = 12:00 | — | +| R7+2 | `OzoneOff` | Ozone/sanitiser manually disabled (bool). *Note: R7+2 and R7+3 may be swapped in some firmware* | — | +| R7+3 | `TemperatureUnits` | Temperature display units. 0=°C, 1=°F. *Note: may be swapped with R7+2* | — | +| R7+4 | `Ozone24` | Ozone/sanitiser continuous 24h mode (bool) | — | +| R7+5 | `CJET` | Circulation jet boost active (bool) | — | +| R7+6 | `Circ24` | Circulation pump continuous 24h mode (bool) | — | +| R7+7 | `VELE` | Variable-power element operation enabled (bool) | — | +| R7+8 | *(StartDD)* | Install date — day. Not mapped in code | — | +| R7+9 | *(StartMM)* | Install date — month. Not mapped in code | — | +| R7+10 | *(StartYY)* | Install date — year. Not mapped in code | — | +| R7+11 | `V_Max` | Maximum mains voltage recorded this session (V) | — | +| R7+12 | `V_Min` | Minimum mains voltage recorded this session (V) | — | +| R7+13 | `V_Max_24` | Maximum mains voltage in last 24 hours (V) | — | +| R7+14 | `V_Min_24` | Minimum mains voltage in last 24 hours (V) | — | +| R7+15 | `CurrentZero` | Current sensor zero-point calibration offset | — | +| R7+16 | `CurrentAdjust` | Current sensor gain calibration ×10 | — | +| R7+17 | `VoltageAdjust` | Voltage sensor calibration ×10 | — | +| R7+18 | *(unknown)* | Unlabelled field. Observed value 3 | — | +| R7+19 | `Ser1` | Service interval 1 period (hours) | — | +| R7+20 | `Ser2` | Service interval 2 period (hours) | — | +| R7+21 | `Ser3` | Service interval 3 period (hours) | — | +| R7+22 | `VMAX` | Maximum permitted supply voltage (V) | — | +| R7+23 | `AHYS` | Adaptive hysteresis setting ×10 (°C) | — | +| R7+24 | `HUSE` | Heat pump suspended during spa use when false (H.USE OEM setting) | — | +| R7+25 | `HELE` | Auxiliary booster element. false=Off, true=On | `W98:<0-1>` | +| R7+26 | `HPMP` | Heat pump operating mode. 0=Auto, 1=Heat, 2=Cool, 3=Off | `W99:<0-3>` | +| R7+27 | `PMIN` | Minimum power level for load management ×10 (kW) | — | +| R7+28 | `PFLT` | Filtration pump power draw ×10 (kW) | — | +| R7+29 | `PHTR` | Heater element power draw ×10 (kW) | — | +| R7+30 | `PMAX` | Maximum total power for load management ×10 (kW) | — | + +--- + +## R9 / RA / RB — Fault Logs (F1=most recent, F2, F3) + +Each fault register follows the same layout. R9=F1, RA=F2, RB=F3. + +| Offset | Property (F1 / F2 / F3) | Description | +|--------|-------------------------|-------------| +| +1 | *(fault label)* | Fault identifier string (`F1`, `F2`, `F3`) | +| +2 | `F1_HR` / `F2_HR` / `F3_HR` | Hour meter reading at time of fault ×10 (hours) | +| +3 | `F1_Time` / `F2_Time` / `F3_Time` | Time of fault encoded as h×256+m | +| +4 | `F1_ER` / `F2_ER` / `F3_ER` | Error/fault code | +| +5 | `F1_I` / `F2_I` / `F3_I` | Current reading ×10 (A) at time of fault | +| +6 | `F1_V` / `F2_V` / `F3_V` | Voltage reading (V) at time of fault | +| +7 | `F1_PT` / `F2_PT` / `F3_PT` | Pool temperature ×10 (°C) at time of fault | +| +8 | `F1_HT` / `F2_HT` / `F3_HT` | Heater temperature ×10 (°C) at time of fault | +| +9 | `F1_CT` / `F2_CT` / `F3_CT` | Case temperature ×10 (°C) at time of fault | +| +10 | `F1_PU` / `F2_PU` / `F3_PU` | Pump state at time of fault | +| +11 | `F1_VE` / `F2_VE` / `F3_VE` | Variable element state at time of fault (bool) | +| +12 | `F1_ST` / `F2_ST` / `F3_ST` | System status code at time of fault | + +--- + +## RC — Relay / Outlet States + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| RC+1 | *(Outlet_Heater)* | Heating element relay output state. Encoding not confirmed; not mapped in code | — | +| RC+2 | *(Outlet_Circ)* | Circulation pump relay output state. Not mapped in code | — | +| RC+3 | *(Outlet_Sanitise)* | Sanitiser relay output state. Not mapped in code | — | +| RC+4 | *(Outlet_Pump1)* | Pump 1 relay output state. Not mapped in code | — | +| RC+5 | *(Outlet_Pump2)* | Pump 2 relay output state. Not mapped in code | — | +| RC+6–9 | *(unknown)* | Unmapped relay fields | — | +| RC+10 | `Outlet_Blower` | Air blower mode. 0=Variable, 1=Ramp, 2=Off | `S28:<0-2>` | + +--- + +## RE — Heat Pump + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| RE+1 | `HP_Present` | Heat pump unit installed. 0=No, 1=Yes | — | +| RE+2–9 | *(HP switches)* | HP flow switch, high/low pressure switches, cut-outs, D1–D3 states. Not mapped in code | — | +| RE+10 | `HP_Ambient` | Heat pump ambient air temperature ×10 (°C) | — | +| RE+11 | `HP_Condensor` | Heat pump condenser temperature ×10 (°C) | — | +| RE+12 | `HP_Compressor_State` | Compressor running (bool) | — | +| RE+13 | `HP_Fan_State` | Fan running (bool) | — | +| RE+14 | `HP_4W_Valve` | 4-way reversing valve active — cool mode (bool) | — | +| RE+15 | `HP_Heater_State` | Auxiliary heater element active (bool) | — | +| RE+16 | `HP_State` | Heat pump operating state code | — | +| RE+17 | `HP_Mode` | Heat pump mode. 0=Auto, 1=Heat, 2=Cool, 3=Off | — | +| RE+18 | `HP_Defrost_Timer` | Defrost cycle timer (minutes) | — | +| RE+19 | `HP_Comp_Run_Timer` | Compressor cumulative run time (minutes) | — | +| RE+20 | `HP_Low_Temp_Timer` | Low ambient temperature protection timer (minutes) | — | +| RE+21 | `HP_Heat_Accum_Timer` | Heat accumulation timer (minutes) | — | +| RE+22 | `HP_Sequence_Timer` | Start sequence timer (seconds) | — | +| RE+23 | `HP_Warning` | Warning/fault code. 0 = no warning | — | +| RE+24 | `FrezTmr` | Freeze protection activation timer (minutes) | — | +| RE+25 | `DBGN` | Defrost cycle start temperature ×10 (°C) | — | +| RE+26 | `DEND` | Defrost cycle end temperature ×10 (°C) | — | +| RE+27 | `DCMP` | Defrost compressor run time limit (minutes) | — | +| RE+28 | `DMAX` | Defrost maximum duration (minutes) | — | +| RE+29 | `DELE` | Defrost element activation delay (minutes) | — | +| RE+30 | `DPMP` | Defrost pump operating mode | — | + +--- + +## RG — Pump Installation / Lock (V3 firmware only) + +| Offset | Property | Description | Write Command | +|--------|----------|-------------|---------------| +| RG+1 | `Pump1OkToRun` | Pump 1 safe to start (bool) | — | +| RG+2 | `Pump2OkToRun` | Pump 2 safe to start (bool) | — | +| RG+3 | `Pump3OkToRun` | Pump 3 safe to start (bool) | — | +| RG+4 | `Pump4OkToRun` | Pump 4 safe to start (bool) | — | +| RG+5 | `Pump5OkToRun` | Pump 5 safe to start (bool) | — | +| RG+6 | *(unknown)* | Unmapped | — | +| RG+7 | `Pump1InstallState` | Pump 1 install config string. Format: `I-S-PPP` (I=installed, S=speed type, PPP=valid states e.g. `014`=Off/On/Auto) | — | +| RG+8 | `Pump2InstallState` | Pump 2 install config string | — | +| RG+9 | `Pump3InstallState` | Pump 3 install config string | — | +| RG+10 | `Pump4InstallState` | Pump 4 install config string | — | +| RG+11 | `Pump5InstallState` | Pump 5 install config string | — | +| RG+12 | `LockMode` | Keypad lock mode. 0=Unlocked, 1=Partially Locked, 2=Locked | `S21:<0-2>` | +| RG+13–14 | *(unknown)* | Unmapped. Observed as 0 | — | +| RG+15 | *(unknown)* | Unmapped. Observed value ~3727 | — | + +--- + +## Write Command Reference + +| Command | Property | Notes | +|---------|----------|-------| +| `S01:` | SpaTime — year | 2-digit year | +| `S02:` | SpaTime — month | | +| `S03:` | SpaTime — day | | +| `S04:` | SpaTime — hour | | +| `S05:` | SpaTime — minute | | +| `S06:<0-6>` | SpaDayOfWeek | 0=Mon…6=Sun | +| `S07:<0-4>` | ColorMode | 0=White, 1=Color, 2=Fade, 3=Step, 4=Party | +| `S08:<1-5>` | LBRTValue | Light brightness | +| `S09:<1-5>` | LSPDValue | Light effect speed | +| `S10:<0-31>` | CurrClr | Light colour index | +| `S13:<1-5>` | VARIValue | Variable pump/blower speed | +| `S21:<0-2>` | LockMode | 0=Unlocked, 1=Partial, 2=Locked | +| `S22:<0-4>` | RB_TP_Pump1 | 0=Off, 1=Spd1, 2=Spd2, 4=Auto | +| `S23:<0-4>` | RB_TP_Pump2 | | +| `S24:<0-4>` | RB_TP_Pump3 | | +| `S25:<0-4>` | RB_TP_Pump4 | | +| `S26:<0-4>` | RB_TP_Pump5 | | +| `S28:<0-2>` | Outlet_Blower | 0=Variable, 1=Ramp, 2=Off | +| `W14` | RB_TP_Light | **Toggle** — no value argument | +| `W40:` | STMP | Set point ×10. e.g. `W40:380` = 38.0°C | +| `W60:<1-24>` | FiltHrs | Filtration hours per block | +| `W66:<0-3>` | Mode | 0=NORM, 1=ECON, 2=AWAY, 3=WEEK | +| `W67:` | L_1SNZ_DAY | Day bitmap: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays | +| `W68:` | L_1SNZ_BGN | Sleep timer 1 start: h×256+m | +| `W69:` | L_1SNZ_END | Sleep timer 1 end: h×256+m | +| `W70:` | L_2SNZ_DAY | Day bitmap (same as W67) | +| `W71:` | L_2SNZ_BGN | Sleep timer 2 start: h×256+m | +| `W72:` | L_2SNZ_END | Sleep timer 2 end: h×256+m | +| `W90:` | FiltBlockHrs | Valid: 1,2,3,4,6,8,12,24 | +| `W98:<0-1>` | HELE | Aux booster element | +| `W99:<0-3>` | HPMP | 0=Auto, 1=Heat, 2=Cool, 3=Off | diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index b59cd98..b19aa6d 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -329,6 +329,7 @@ class SpaInterface { _callback(_value); } } + // Reverse-lookup by label string; throws std::invalid_argument if the label // is not found in the map or if no map is configured. void updateFromLabel(const char* label) { @@ -612,7 +613,7 @@ class SpaInterface { ROProperty Status; /// @brief Priming cycle count. ROProperty PrimeCount; - /// @brief Error/fault code. 0 = no fault. + /// @brief Variable heat element current draw x10 (A). e.g. 77 = 7.7A. ROProperty EC; /// @brief Heater ambient air temperature x10 (°C). ROProperty HAMB; diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index b29ce0e..f1d704c 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -141,6 +141,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["power"]["current"]= si.MainsCurrent.get() / 10.0; // convert value to A json["power"]["power"] = si.Power.get() / 10.0; // convert value to W json["power"]["totalenergy"]= si.Power_kWh.get() / 100.0; // convert value to kWh. + json["power"]["heatElementCurrent"] = si.EC.get() / 10.0; // convert value to A json["status"]["heatingActive"] = si.RB_TP_Heater.get()? "ON": "OFF"; json["status"]["ozoneActive"] = si.RB_TP_Ozone.get()? "ON": "OFF"; diff --git a/src/main.cpp b/src/main.cpp index 80fe175..b6bacf9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -102,6 +102,7 @@ void startWiFiManager(){ config.writeConfig(); } + WiFi.disconnect(true); // Force teardown of all socket connections ESP.restart(); // Restart the ESP to apply the new settings } @@ -122,6 +123,7 @@ void checkButton(){ void startWifiManagerCallback() { debugD("Starting Wi-Fi Manager..."); startWiFiManager(); + WiFi.disconnect(true); // Force teardown of all socket connections ESP.restart(); //do we need to reboot here?? } @@ -213,6 +215,14 @@ void mqttHaAutoDiscovery() { generateSensorAdJSON(output, ADConf, spa, discoveryTopic, "measurement", "A"); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Heat Element Current"; + ADConf.valueTemplate = "{{ value_json.power.heatElementCurrent }}"; + ADConf.propertyId = "EC"; + ADConf.deviceClass = "current"; + ADConf.entityCategory = "diagnostic"; + generateSensorAdJSON(output, ADConf, spa, discoveryTopic, "measurement", "A"); + mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Power"; ADConf.valueTemplate = "{{ value_json.power.power }}"; ADConf.propertyId = "Power"; @@ -799,6 +809,7 @@ void loop() { if (spaCallbackProperty == "reboot") { debugI("Rebooting ESP after %d ms", spaCallbackValue.toInt()); delay(spaCallbackValue.toInt()); // Wait for the specified time before rebooting + WiFi.disconnect(true); // Force teardown of all socket connections ESP.restart(); } else { debugD("Setting Spa Properties..."); From 85b078b49fa092291a60f784359a90d42b363040 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 5 Apr 2026 10:04:32 +1000 Subject: [PATCH 36/44] Extend timeouts on SS command to catch long running commands --- lib/SpaInterface/SpaInterface.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 5ff8931..915bb78 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -81,9 +81,9 @@ void SpaInterface::_processDebugCommand() { if (_instance == nullptr) return; String cmd = Debug.getLastCommand(); - if (!cmd.startsWith("ss ")) return; + if (!cmd.startsWith("ss ") && !cmd.startsWith("SS ")) return; - String payload = cmd.substring(4); + String payload = cmd.substring(3); debugI("TX: %s", payload.c_str()); _instance->flushSerialReadBuffer(); @@ -93,14 +93,17 @@ void SpaInterface::_processDebugCommand() { _instance->port.printf("%s\n", payload.c_str()); _instance->port.flush(); - // Drain until 100ms of silence (max 2s absolute) + // Wait up to 2s for first byte, then collect until 500ms gap String response = ""; - unsigned long lastByte = millis(); - unsigned long deadline = millis() + 2000; - while (millis() - lastByte < 100 && millis() < deadline) { - while (_instance->port.available()) { - response += (char)_instance->port.read(); - lastByte = millis(); + unsigned long start = millis(); + while (!_instance->port.available() && millis() - start < 2000) {} + if (_instance->port.available()) { + unsigned long lastByte = millis(); + while (millis() - lastByte < 500) { + while (_instance->port.available()) { + response += (char)_instance->port.read(); + lastByte = millis(); + } } } From fff3095a783615587a0531e82a15ce9899f4eab2 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sun, 5 Apr 2026 10:05:10 +1000 Subject: [PATCH 37/44] Add W01 to W07 commands to Register Map --- SpaNET Debug Files/register-map.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/SpaNET Debug Files/register-map.md b/SpaNET Debug Files/register-map.md index ec64479..8c1a3dc 100644 --- a/SpaNET Debug Files/register-map.md +++ b/SpaNET Debug Files/register-map.md @@ -310,6 +310,13 @@ Each fault register follows the same layout. R9=F1, RA=F2, RB=F3. | `S25:<0-4>` | RB_TP_Pump4 | | | `S26:<0-4>` | RB_TP_Pump5 | | | `S28:<0-2>` | Outlet_Blower | 0=Variable, 1=Ramp, 2=Off | +| `W01` | Reset energy and voltage statistics mensures || +| `W02` | Unknown W02 valid, W02:1 invalid || +| `W03` | Set zero current level. Spa should be drawing no power when this is done. || +| `W04` | Decrease current calibration gain by 1 (0.1A)|| +| `W05` | Increase current calibration gain by 1 (0.1A)|| +| `W06` | VoltageAdjust | Increase voltage calibration gain by 1 (0.1v) | +| `W07` | VoltageAdjust | Decrease voltage calibration gain by 1 (0.1v)| | `W14` | RB_TP_Light | **Toggle** — no value argument | | `W40:` | STMP | Set point ×10. e.g. `W40:380` = 38.0°C | | `W60:<1-24>` | FiltHrs | Filtration hours per block | From e20888a7316ace6bf55c897148ec8152dbc4271d Mon Sep 17 00:00:00 2001 From: wayne-love Date: Wed, 8 Apr 2026 12:55:52 +1000 Subject: [PATCH 38/44] Update register map with discovered functions --- .../register-map.md => register-map.md | 174 +++++++++++++----- 1 file changed, 133 insertions(+), 41 deletions(-) rename SpaNET Debug Files/register-map.md => register-map.md (62%) diff --git a/SpaNET Debug Files/register-map.md b/register-map.md similarity index 62% rename from SpaNET Debug Files/register-map.md rename to register-map.md index 8c1a3dc..71a16a4 100644 --- a/SpaNET Debug Files/register-map.md +++ b/register-map.md @@ -28,17 +28,17 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R2+16 | `AwakeMinutesRemaining` | Minutes before spa returns to sleep/standby | — | | R2+17 | `FiltPumpRunTimeTotal` | Total cumulative filtration pump run time (minutes) | — | | R2+18 | `FiltPumpReqMins` | Remaining filtration minutes required this cycle | — | -| R2+19 | `LoadTimeOut` | Load management timeout counter (seconds) | — | -| R2+20 | `HourMeter` | Total controller run time ×10 (hours). e.g. 279654 = 27965.4 h | — | -| R2+21 | `Relay1` | Relay 1 cumulative run time or state | — | -| R2+22 | `Relay2` | Relay 2 cumulative run time or state | — | -| R2+23 | `Relay3` | Relay 3 cumulative run time or state | — | -| R2+24 | `Relay4` | Relay 4 cumulative run time or state | — | -| R2+25 | `Relay5` | Relay 5 cumulative run time or state | — | -| R2+26 | `Relay6` | Relay 6 cumulative run time or state | — | -| R2+27 | `Relay7` | Relay 7 cumulative run time or state | — | -| R2+28 | `Relay8` | Relay 8 cumulative run time or state | — | -| R2+29 | `Relay9` | Relay 9 cumulative run time or state | — | +| R2+19 | `LoadTimeOut` | In-use sessions timer (seconds) | — | +| R2+20 | `HourMeter` | Total controller run time ×10 (hours). e.g. 279654 = 27965.4 h | `W23:` | +| R2+21 | `Relay1` | Relay 1 cumulative activation count | `W24:` | +| R2+22 | `Relay2` | Relay 2 cumulative activation count | `W25:` | +| R2+23 | `Relay3` | Relay 3 cumulative activation count | `W26:` | +| R2+24 | `Relay4` | Relay 4 cumulative activation count | `W27:` | +| R2+25 | `Relay5` | Relay 5 cumulative activation count | `W28:` | +| R2+26 | `Relay6` | Relay 6 cumulative activation count | `W29:` | +| R2+27 | `Relay7` | Relay 7 cumulative activation count | `W30:` | +| R2+28 | `Relay8` | Relay 8 cumulative activation count, controls Heating Element | `W31:` | +| R2+29 | `Relay9` | Relay 9 cumulative activation count | `W32:` | --- @@ -63,14 +63,14 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R3+15 | `D6` | Dipswitch 6: Multi-phase type when D5=1 (1=Three Phase, 0=Two Phase) | — | | R3+16 | `Pump` | Pump configuration string | — | | R3+17 | `LS` | Load shedding level | — | -| R3+18 | `HV` | High voltage input active (bool) | — | +| R3+18 | *HV* | Unknown, no observed changes. 0 = Off | — | | R3+19 | `SnpMR` | Snooze mode remaining time (MR). Per-second countdown; 0 when not in snooze | — | | R3+20 | `Status` | Operational status string (e.g. `Filtering`, `In use`, `Heating`) | — | | R3+21 | `PrimeCount` | Priming cycle count | — | | R3+22 | `EC` | Variable heat element current draw x10 (A) | — | -| R3+23 | `HAMB` | Heater ambient air temperature ×10 (°C) | — | -| R3+24 | `HCON` | Heater conductivity/connection value | — | -| R3+25 | *(HV_2)* | High voltage 2 input state. Encoding unconfirmed; not mapped in code | — | +| R3+23 | `HAMB` | Heat pump ambient air temperature ×10 (°C) | — | +| R3+24 | `HCON` | Heat pump condensor temperature x10 (°C) | — | +| R3+25 | *HV_2* | 0 when heat element inactive (Off), 90 when active | — | --- @@ -83,8 +83,8 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R4+3 | `Ser2_Timer` | Service interval 2 countdown (hours remaining) | — | | R4+4 | `Ser3_Timer` | Service interval 3 countdown (hours remaining) | — | | R4+5 | `HeatMode` | Heating source mode. 0=Element, 1=Heat pump, 2=Both, 3=Cool | — | -| R4+6 | `PumpIdleTimer` | Minutes since last pump activity | — | -| R4+7 | `PumpRunTimer` | Continuous pump run time (seconds, increments each second) | — | +| R4+6 | `PumpIdleTimer` | In sleep mode: seconds elapsed in 600 s duty cycle (R4+6 + R4+7 = 600). In use mode: continuous session elapsed timer (counts beyond 600) | — | +| R4+7 | `PumpRunTimer` | In sleep mode: seconds remaining in 600 s duty cycle (counts down to 0). In use mode: clamps at 0 | — | | R4+8 | `AdtPoolHys` | Adaptive pool temperature hysteresis ×10 (°C) | — | | R4+9 | `AdtHeaterHys` | Adaptive heater temperature hysteresis ×10 (°C) | — | | R4+10 | `Power` | Instantaneous power consumption ×10 (W). e.g. 35 = 3.5 kW | — | @@ -94,15 +94,18 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R4+14 | `ThermalCutOut` | Thermal cut-out trip count | — | | R4+15 | `Test_D1` | Diagnostic output D1 state | — | | R4+16 | `Test_D2` | Diagnostic output D2 state | — | -| R4+17 | `Test_D3` | Diagnostic output D3 state | — | +| R4+17 | `Test_D3` | Diagnostic output D3 state. Observed to fluctuate (0 / 262144) with spa state transitions — not directly written by any known S/W command | — | | R4+18 | `ElementHeatSourceOffset` | Element heat source temperature offset ×10 (°C) | — | -| R4+19 | `Frequency` | Detected mains frequency (Hz) | — | +| R4+19 | `Frequency` | Unknown | — | | R4+20 | `HPHeatSourceOffset_Heat` | Heat pump heat-mode source temperature offset ×10 (°C) | — | | R4+21 | `HPHeatSourceOffset_Cool` | Heat pump cool-mode source temperature offset ×10 (°C) | — | | R4+22 | `HeatSourceOffTime` | Heat source off-time (minutes) | — | -| R4+23 | `Vari_Mode` | Variable speed pump mode. 0=Auto, 1=Manual | — | -| R4+24 | `Vari_Speed` | Variable speed pump current speed setting | — | -| R4+25 | `Vari_Percent` | Variable speed pump output percentage (%) | — | +| R4+23 | `Vari_Mode` | Variable speed pump commanded speed target (%). Written by `S15:`. Values observed: 1, 4, 100 | `S15:` | +| R4+24 | `Vari_Speed` | Variable speed pump live speed reading | — | +| R4+25 | `Vari_Percent` | Variable speed pump live output percentage (%) | — | +| R4+26 | *(unknown)* | | — | +| R4+27 | *(unknown)* | | — | +| R4+28 | *(unknown)* | Mirrors R4+23; also written by `S15:` | `S15:` | --- @@ -119,10 +122,10 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R5+11 | `RB_TP_Ozone` | True when ozone/UV sanitiser is running | — | | R5+12 | `RB_TP_Heater` | True when heating or cooling is actively running | — | | R5+13 | `RB_TP_Auto` | True when auto mode is active | — | -| R5+14 | `RB_TP_Light` | Light state. 0=Off, 1=On | `W14` (toggle — no value argument) | +| R5+14 | `RB_TP_Light` | Light state. 0=Off, 1=On | `W14` (toggle) / `S30` | | R5+15 | `WTMP` | Actual (measured) water temperature ×10 (°C). e.g. 376 = 37.6°C | — | | R5+16 | `CleanCycle` | True when a sanitise/clean cycle is in progress | — | -| R5+17 | *(unknown)* | Per-second countdown timer. Behaviour consistent with a touchpad activity timeout (~30 s); resets on keypad interaction | — | +| R5+17 | *(unss rfknown)* | Per-second countdown timer. Behaviour consistent with a touchpad activity timeout (~30 s); resets on keypad interaction | — | | R5+18 | `RB_TP_Pump1` | Pump 1 operating state. 0=Off, 1=Speed 1, 2=Speed 2, 4=Auto | `S22:<0-4>` | | R5+19 | `RB_TP_Pump2` | Pump 2 operating state. 0=Off, 1=On, 4=Auto | `S23:<0-4>` | | R5+20 | `RB_TP_Pump3` | Pump 3 operating state. 0=Off, 1=On, 4=Auto | `S24:<0-4>` | @@ -175,7 +178,7 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R7+4 | `Ozone24` | Ozone/sanitiser continuous 24h mode (bool) | — | | R7+5 | `CJET` | Circulation jet boost active (bool) | — | | R7+6 | `Circ24` | Circulation pump continuous 24h mode (bool) | — | -| R7+7 | `VELE` | Variable-power element operation enabled (bool) | — | +| R7+7 | `VELE` | Variable-power element mode. 0=Off, 1=Step, 2=Variable | `W91:<0-2>` | | R7+8 | *(StartDD)* | Install date — day. Not mapped in code | — | | R7+9 | *(StartMM)* | Install date — month. Not mapped in code | — | | R7+10 | *(StartYY)* | Install date — year. Not mapped in code | — | @@ -192,7 +195,7 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R7+21 | `Ser3` | Service interval 3 period (hours) | — | | R7+22 | `VMAX` | Maximum permitted supply voltage (V) | — | | R7+23 | `AHYS` | Adaptive hysteresis setting ×10 (°C) | — | -| R7+24 | `HUSE` | Heat pump suspended during spa use when false (H.USE OEM setting) | — | +| R7+24 | `HUSE` | Heat pump available when spa is in use. 0=Not available, 1=Available (H.USE OEM setting) | `W97:<0-1>` | | R7+25 | `HELE` | Auxiliary booster element. false=Off, true=On | `W98:<0-1>` | | R7+26 | `HPMP` | Heat pump operating mode. 0=Auto, 1=Heat, 2=Cool, 3=Off | `W99:<0-3>` | | R7+27 | `PMIN` | Minimum power level for load management ×10 (kW) | — | @@ -227,13 +230,15 @@ Each fault register follows the same layout. R9=F1, RA=F2, RB=F3. | Offset | Property | Description | Write Command | |--------|----------|-------------|---------------| -| RC+1 | *(Outlet_Heater)* | Heating element relay output state. Encoding not confirmed; not mapped in code | — | +| RC+1 | *(Outlet_Heater)* | Heating element relay output state. Not mapped in code | — | | RC+2 | *(Outlet_Circ)* | Circulation pump relay output state. Not mapped in code | — | | RC+3 | *(Outlet_Sanitise)* | Sanitiser relay output state. Not mapped in code | — | | RC+4 | *(Outlet_Pump1)* | Pump 1 relay output state. Not mapped in code | — | | RC+5 | *(Outlet_Pump2)* | Pump 2 relay output state. Not mapped in code | — | | RC+6–9 | *(unknown)* | Unmapped relay fields | — | -| RC+10 | `Outlet_Blower` | Air blower mode. 0=Variable, 1=Ramp, 2=Off | `S28:<0-2>` | +| RC+10 | `Outlet_Blower` | Air blower mode. 0=Variable, 1=Ramp, 2=Off. Confirmed by direct observation. | `S12:<0-2>` / `S28:<0-2>` | +| RC+11 | *(Demo mode?)* | Suspected demo mode flag. Set to 1 by bare `S32`; second call does not clear it. When active, temperature readings appear offset by ~20°C — consistent with simulated/demo values | `S32` | +| RC+12 | *(unknown)* | Written by `S33:<0-1>`. 0=Off, 1=On. Returns value + `S33` echo. Bare `S33` invalid | `S33:<0-1>` | --- @@ -302,24 +307,86 @@ Each fault register follows the same layout. R9=F1, RA=F2, RB=F3. | `S08:<1-5>` | LBRTValue | Light brightness | | `S09:<1-5>` | LSPDValue | Light effect speed | | `S10:<0-31>` | CurrClr | Light colour index | +| `S11` | *(unknown)* | S11 valid (returns `S11`). S11:1 invalid. No observed register changes | +| `S12:<0-2>` | Outlet_Blower | Blower control. 0=Variable, 1=Ramp, 2=Off. Returns value + `S12` | | `S13:<1-5>` | VARIValue | Variable pump/blower speed | +| `S14` || Toggle Blower | +| `S15:` | Vari_Mode | Variable speed pump commanded speed target. Writes value to R4+23 and R4+28. Returns value + `S15` echo. | +| `S16` | Outlet_Blower | Toggle blower on/off. S16:n invalid. | +| `S17` | *(unknown)* | Valid bare toggle (returns `S17`). No observable register changes — function unknown | +| `S18` | *(unknown)* | Valid bare toggle (returns `S18`). No observable register changes — function unknown | +| `S19` | ⚠️ **DANGEROUS** | **Corrupts the serial connection. Requires spa power cycle to restore communication. DO NOT SEND.** | +| `S20:` | *(unknown)* | Valid with parameter (returns value + `S20`). `S20` invalid. No observable register changes. | | `S21:<0-2>` | LockMode | 0=Unlocked, 1=Partial, 2=Locked | | `S22:<0-4>` | RB_TP_Pump1 | 0=Off, 1=Spd1, 2=Spd2, 4=Auto | -| `S23:<0-4>` | RB_TP_Pump2 | | -| `S24:<0-4>` | RB_TP_Pump3 | | -| `S25:<0-4>` | RB_TP_Pump4 | | -| `S26:<0-4>` | RB_TP_Pump5 | | -| `S28:<0-2>` | Outlet_Blower | 0=Variable, 1=Ramp, 2=Off | -| `W01` | Reset energy and voltage statistics mensures || -| `W02` | Unknown W02 valid, W02:1 invalid || -| `W03` | Set zero current level. Spa should be drawing no power when this is done. || -| `W04` | Decrease current calibration gain by 1 (0.1A)|| -| `W05` | Increase current calibration gain by 1 (0.1A)|| -| `W06` | VoltageAdjust | Increase voltage calibration gain by 1 (0.1v) | +| `S23:<0-4>` | RB_TP_Pump2 | 0=Off, 1=Spd1, 2=Spd2, 4=Auto | +| `S24:<0-4>` | RB_TP_Pump3 | 0=Off, 1=Spd1, 2=Spd2, 4=Auto | +| `S25:<0-4>` | RB_TP_Pump4 | 0=Off, 1=Spd1, 2=Spd2, 4=Auto | +| `S26:<0-4>` | RB_TP_Pump5 | 0=Off, 1=Spd1, 2=Spd2, 4=Auto | +| `S27:` | *(unknown)* | Valid with parameter. Returns value only (no command echo). Observed: resets R2+19, S27:1 woke spa from Sleeping to In use | +| `S28:<0-2>` | Outlet_Blower | Blower control. 0=Variable, 1=Ramp, 2=Off | +| `S29` | *(unknown)* | Valid bare toggle (returns `S29`). S29:n invalid. No observable register changes | +| `S30` | RB_TP_Light | Bare only. Observed: R5+14 (RB_TP_Light) 0→1. Not a toggle — exact behaviour unknown | +| `S31:` | ⚠️ **DANGEROUS** | **Triggers "Busy, config INFR" — corrupts serial link. Requires spa power cycle to restore communication. DO NOT SEND.** | +| `S32` | *(Demo mode?)* | Valid bare only (S32:n invalid). Sets RC+11=1; suspected to activate demo mode. Second call does not clear it — no known off command | +| `S33:<0-1>` | *(unknown)* | Valid with parameter. Writes value to RC+12. Returns value + `S33`. | +| `W01` | Reset energy and voltage statistics mesures || +| `W02` || Unknown W02 valid, W02:1 invalid | +| `W03` | CurrentZero | Zeros the Current meter by adjusting CurrentZero. Spa should be drawing no power when this is done. | +| `W04` | CurrentAdjust | Decrease current calibration gain by 1 (0.1A)| +| `W05` | CurrentAdjust | Increase current calibration gain by 1 (0.1A)| +| `W06` | VoltageAdjust | Increase voltage calibration gain by 1 (0.1v)| | `W07` | VoltageAdjust | Decrease voltage calibration gain by 1 (0.1v)| -| `W14` | RB_TP_Light | **Toggle** — no value argument | +| `W08` || Keypad Up button press. Returns "W8" | +| `W09` || Keypad OK button press. Returns "W9" | +| `W10` || Keypad Down button press | +| `W11` || Keypad Invert display button press | +| `W12` | CleanCycle | Keypad Start / Stop clean cycle. Returns W12.| +| `W13` | RB_TP_Blower | Keypad Toggle Blower. Returns W13. | +| `W14` | RB_TP_Light | Keypad Toggle Lights. Returns W15. | +| `W15` || Keypad Unknown, suspect keypress | +| `W16` || Keypad Unknown, suspect keypress | +| `W17` || Keypad Pump 2 toggle | +| `W18` || Keypad Pump 3 toggle | +| `W19` || Keypad Pump 4 toggle | +| `W20` || Keypad Unknown, suspect keypress | +| `W23:` | HourMeter | Sets the hour meter runtime. Returns "" | +| `W24:` | Relay1 | Sets Relay 1 count. Returns "" | +| `W25:` | Relay2 | Sets Relay 2 count. Returns "" | +| `W26:` | Relay3 | Sets Relay 3 count. Returns "" | +| `W27:` | Relay4 | Sets Relay 4 count. Returns "" | +| `W28:` | Relay5 | Sets Relay 5 count. Returns "" | +| `W29:` | Relay6 | Sets Relay 6 count. Returns "" | +| `W30:` | Relay7 | Sets Relay 7 count. Returns "" | +| `W31:` | Relay8 | Sets Relay 8 count. Returns "" | +| `W32:` | Relay9 | Sets Relay 9 count. Returns "" | +| `W33:` | CurrentZero | Sets the current offset value. Returns "" | +| `W34:` | CurrentAdjust | Sets the current adjust value. Returns "" | +| `W35:` | VoltageAdjust | Sets the voltage adjust value. Returns "" | +| `W36:
` | StartDD | Set install day. Returns "dd" | +| `W37:` | StartMM | Set install month. Returns "mm" | +| `W39:` | StartYY | Set install year. Returns "yyyy" | +| `W39` || Set the install date to today(?) has been seen to zero the install date | | `W40:` | STMP | Set point ×10. e.g. `W40:380` = 38.0°C | -| `W60:<1-24>` | FiltHrs | Filtration hours per block | +| `W41:` | AdtPoolHys | Set AdtPoolHys ×10. e.g. `W41:380` = 38.0°C | +| `W42:` | AdtHeaterHys | Set AdtHeaterHys ×10. e.g. `W42:380` = 38.0°C | +| `W43 to W49` || Invalid. No response to bare or parametric forms | +| `W50` | V_Max/V_Min | Reset all voltage min/max stats (R7+11–14) to current voltage. Returns `W50` | +| `W51` | HourMeter | Reset `HourMeter` (R2+20) to 0. Spa writes a fault log entry as a record of the reset. Returns `W51` | +| `W52` | Relay counters | Reset all relay activation counters (R2+21–29) to 0. Returns `W52` | +| `W53` | Power_kWh | Reset cumulative energy `Power_kWh` (R4+11) to 0. Returns `W53` | +| `W54` | Power_Today/Yesterday | Reset daily energy counters `Power_Today` (R4+12) and `Power_Yesterday` (R4+13) to 0. Returns `W54` | +| `W55` || Valid toggle (returns `W55`). No observed register changes; suspected `Ser1_Timer` (R4+2) reset — invisible when already 0 | +| `W56` || Valid toggle (returns `W56`). No observed register changes; suspected `Ser2_Timer` (R4+3) reset — invisible when already 0 | +| `W57` || Valid toggle (returns `W57`). No observed register changes; suspected `Ser3_Timer` (R4+4) reset — invisible when already 0 | +| `W58` || Valid toggle (returns `W58`). No observed register changes; W58:1 invalid — toggle only | +| `W59` || Invalid. No response to bare, `:1`, or `:100` forms | +| `W60:<1-24>` | FiltHrs | Filtration run time per block (hours). Range 1–24 | +| `W61:<0-1>` | L_24HOURS | 24-hour continuous operation. 0=Off, 1=On | +| `W62:` | DefaultScrn | Default touchpad screen index shown on wake | +| `W63:<0-2>` | PSAV_LVL | Power save level. 0=Off, 1=Low, 2=High | +| `W64:` | PSAV_BGN | Power save start time encoded as h×256+m. e.g. `W64:3584` = 14:00 | +| `W65:` | PSAV_END | Power save end time encoded as h×256+m. | | `W66:<0-3>` | Mode | 0=NORM, 1=ECON, 2=AWAY, 3=WEEK | | `W67:` | L_1SNZ_DAY | Day bitmap: 128=Off, 127=Everyday, 96=Weekends, 31=Weekdays | | `W68:` | L_1SNZ_BGN | Sleep timer 1 start: h×256+m | @@ -327,6 +394,31 @@ Each fault register follows the same layout. R9=F1, RA=F2, RB=F3. | `W70:` | L_2SNZ_DAY | Day bitmap (same as W67) | | `W71:` | L_2SNZ_BGN | Sleep timer 2 start: h×256+m | | `W72:` | L_2SNZ_END | Sleep timer 2 end: h×256+m | +| `W73:` | WCLNTime | Auto sanitise cycle start time encoded as h×256+m. e.g. `W73:2304` = 09:00 | +| `W74:<10-60>` | TOUT | Pump and blower auto time-out duration (minutes). Range 10–60 | +| `W75` || Invalid. No response to bare or parametric forms | +| `W76` || Invalid. No response to bare or parametric forms | +| `W77` || Invalid. No response to bare or parametric forms | +| `W78` || Invalid. No response to bare or parametric forms | +| `W79` || Invalid. No response to bare or parametric forms | +| `W80:<0-1>` | OzoneOff | Ozone/sanitiser manually disabled. 0=Enabled, 1=Disabled. Bare form invalid | +| `W81:<0-1>` | TemperatureUnits | Temperature display units. 0=°C, 1=°F. Bare form invalid | +| `W82:` | Ozone24 | Ozone/sanitiser 24h continuous mode. Sets the Ozone daily run time to hours | +| `W83:<0-1>` | CJET | Circulation jet boost. 0=Off, 1=Active | +| `W84:` | *(unknown)* | Valid, accepts any value, returns value. No observed register changes on this spa. Possible `Circ24` (R7+6) | +| `W85:` | CLMT | Supply current limit (A). Range 10–60. **Caution: low values prevent all loads from running** | +| `W86:` | LS | Load shedding level | +| `W87:` | LLM1 | Phase 1 load limit. Range 1–5. 255 = not configured | +| `W88:` | LLM2 | Phase 2 load limit. Range 1–5 | +| `W89:` | LLM3 | Phase 3 load limit. Range 1–5 *(predicted — not yet tested)* | | `W90:` | FiltBlockHrs | Valid: 1,2,3,4,6,8,12,24 | +| `W91:<0-2>` | VELE | Variable-power element mode. 0=Off, 1=Step, 2=Variable | +| `W92:` | Ser1 | Service interval 1 period (hours) | +| `W93:` | Ser2 | Service interval 2 period (hours) | +| `W94:` | Ser3 | Service interval 3 period (hours) | +| `W95:` | VMAX | Maximum heating element current (A). e.g. `W95:23` = 23A | +| `W96:` | AHYS | Adaptive hysteresis ×10 (°C). e.g. `W96:200` = 20.0°C | +| `W97:<0-1>` | HUSE | Heat pump available during spa use. 0=Not available, 1=Available | | `W98:<0-1>` | HELE | Aux booster element | | `W99:<0-3>` | HPMP | 0=Auto, 1=Heat, 2=Cool, 3=Off | + From c321a0ea9dff42995b9aabcbeb2dc7d2b2255410 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Wed, 8 Apr 2026 15:31:42 +1000 Subject: [PATCH 39/44] Add "Keyboard Button Press" button to allow spofing of button presses to wake up spa --- lib/HAAutoDiscovery/HAAutoDiscovery.cpp | 10 ++++++++++ lib/HAAutoDiscovery/HAAutoDiscovery.h | 1 + lib/SpaInterface/SpaInterface.cpp | 17 +++++++++++++++-- lib/SpaInterface/SpaInterface.h | 15 +++++++++++++-- src/main.cpp | 14 ++++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.cpp b/lib/HAAutoDiscovery/HAAutoDiscovery.cpp index 90bcc53..2ed1fff 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.cpp +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.cpp @@ -79,6 +79,16 @@ void generateSwitchAdJSON(String& output, const AutoDiscoveryInformationTemplate serializeJson(json, output); } +void generateButtonAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic) { + JsonDocument json; + generateCommonAdJSON(json, config, spa, discoveryTopic, "button"); + json.remove("state_topic"); + json.remove("value_template"); + json["command_topic"] = spa.commandTopic + "/" + config.propertyId; + json["payload_press"] = "PRESS"; + serializeJson(json, output); +} + void generateClimateAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic) { JsonDocument json; generateCommonAdJSON(json, config, spa, discoveryTopic, "climate"); diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.h b/lib/HAAutoDiscovery/HAAutoDiscovery.h index 84d88bf..a3ebace 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.h +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.h @@ -40,6 +40,7 @@ void generateSensorAdJSON(String& output, const AutoDiscoveryInformationTemplate void generateBinarySensorAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic); void generateTextAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, String regex=""); void generateSwitchAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic); +void generateButtonAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic); template void generateSelectAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, const std::array& options) { diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 915bb78..3fadabe 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -235,9 +235,22 @@ bool SpaInterface::setHELE(bool mode){ } +bool SpaInterface::sendKey(SpaKey key) { + String cmd; + String expected; + switch (key) { + case SpaKey::Up: cmd = "W08"; expected = "W8"; break; + case SpaKey::Ok: cmd = "W09"; expected = "W9"; break; + case SpaKey::Down: cmd = "W10"; expected = "W10"; break; + case SpaKey::Invert: cmd = "W11"; expected = "W11"; break; + default: return false; + } + return sendCommandCheckResult(cmd, expected); +} + /// @brief Set the water temperature set point * 10 (380 = 38.0) -/// @param temp -/// @return +/// @param temp +/// @return bool SpaInterface::setSTMP(int temp){ debugD("setSTMP - %i", temp); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index b19aa6d..bb85061 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -75,7 +75,6 @@ class SpaInterface { /// @param cmd - cmd to be executed. void sendCommand(String cmd); - /// @brief Sends a command to the SpanNet controller and returns the result string /// @param cmd - cmd to be executed /// @return String - result string @@ -93,7 +92,6 @@ class SpaInterface { void flushSerialReadBuffer() { flushSerialReadBuffer(false); }; String flushSerialReadBuffer(bool returnData); - /// @brief Singleton pointer used by the static RemoteDebug callback. static SpaInterface* _instance; @@ -514,6 +512,19 @@ class SpaInterface { /// @param SpaPollFrequency void setSpaPollFrequency(int updateFrequency); + /// @brief Keypad keys that can be simulated via sendKey(). + enum class SpaKey { + Up, ///< W08 — Keypad Up + Ok, ///< W09 — Keypad OK / Confirm + Down, ///< W10 — Keypad Down + Invert ///< W11 — Keypad Invert display + }; + + /// @brief Simulate a keypad button press on the spa controller. + /// @param key The key to press. + /// @return true if the command was acknowledged. + bool sendKey(SpaKey key); + /// @brief Complete RF command response in a single string RWProperty statusResponse{this, &SpaInterface::setStatusResponse}; diff --git a/src/main.cpp b/src/main.cpp index b6bacf9..2e84814 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -446,6 +446,14 @@ void mqttHaAutoDiscovery() { generateSelectAdJSON(output, ADConf, spa, discoveryTopic, FiltHrsSelect); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Keyboard Button Press"; + ADConf.valueTemplate = ""; + ADConf.propertyId = "keypad_up"; + ADConf.deviceClass = ""; + ADConf.entityCategory = "diagnostic"; + generateButtonAdJSON(output, ADConf, spa, discoveryTopic); + mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Lock Mode"; ADConf.valueTemplate = "{{ value_json.lockmode }}"; ADConf.propertyId = "lock_mode"; @@ -666,6 +674,12 @@ void setSpaProperty(String property, String p) { } catch (const std::exception& ex) { debugE("Failed to set LockMode from label '%s': %s", p.c_str(), ex.what()); } + } else if (property == "keypad_up") { + try { + si.sendKey(SpaInterface::SpaKey::Up); + } catch (const std::exception& ex) { + debugE("Failed to send keypad up: %s", ex.what()); + } } else { debugE("Unhandled property - %s",property.c_str()); } From 2f0992b18fd2c2ecf92085f48d84e672cef08976 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Thu, 9 Apr 2026 12:38:00 +1000 Subject: [PATCH 40/44] Fix missing updates --- lib/SpaInterface/SpaInterface.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 3fadabe..71bb349 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -318,6 +318,7 @@ bool SpaInterface::setL_1SNZ_BGN(int mode){ } if (sendCommandCheckResult(String("W68:")+mode,String(mode))) { + L_1SNZ_BGN.update(mode); return true; } return false; @@ -340,6 +341,7 @@ bool SpaInterface::setL_1SNZ_END(int mode){ } if (sendCommandCheckResult(String("W69:")+mode,String(mode))) { + L_1SNZ_END.update(mode); return true; } return false; @@ -387,6 +389,7 @@ bool SpaInterface::setL_2SNZ_BGN(int mode){ } if (sendCommandCheckResult(String("W71:")+mode,String(mode))) { + L_2SNZ_BGN.update(mode); return true; } return false; @@ -409,6 +412,7 @@ bool SpaInterface::setL_2SNZ_END(int mode){ } if (sendCommandCheckResult(String("W72:")+mode,String(mode))) { + L_2SNZ_END.update(mode); return true; } return false; From 5575f0ba208733bf99d328e337a70f6618eb9973 Mon Sep 17 00:00:00 2001 From: Skip <52146739+wayne-love@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:06:11 +1000 Subject: [PATCH 41/44] Update VMAX description from voltage to current --- register-map.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/register-map.md b/register-map.md index 71a16a4..929c25f 100644 --- a/register-map.md +++ b/register-map.md @@ -193,7 +193,7 @@ Fields are addressed as `RN+offset` where N is the register name and offset is t | R7+19 | `Ser1` | Service interval 1 period (hours) | — | | R7+20 | `Ser2` | Service interval 2 period (hours) | — | | R7+21 | `Ser3` | Service interval 3 period (hours) | — | -| R7+22 | `VMAX` | Maximum permitted supply voltage (V) | — | +| R7+22 | `VMAX` | Maximum heating element current (A) | — | | R7+23 | `AHYS` | Adaptive hysteresis setting ×10 (°C) | — | | R7+24 | `HUSE` | Heat pump available when spa is in use. 0=Not available, 1=Available (H.USE OEM setting) | `W97:<0-1>` | | R7+25 | `HELE` | Auxiliary booster element. false=Off, true=On | `W98:<0-1>` | From b9317a04c6216723b77069f4d1aa1441a86a8d97 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 20 Jun 2026 13:31:44 +1000 Subject: [PATCH 42/44] Add VMAX as RWProperty --- lib/SpaInterface/SpaInterface.cpp | 14 ++++++++++++++ lib/SpaInterface/SpaInterface.h | 7 ++++++- lib/SpaUtils/SpaUtils.cpp | 1 + src/main.cpp | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index 71bb349..d3a9a74 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -234,6 +234,20 @@ bool SpaInterface::setHELE(bool mode){ return false; } +bool SpaInterface::setVMAX(int mode){ + debugD("setVMAX - %i", mode); + if (mode == VMAX.get()) { + debugD("No VMAX change detected - current %i, new %i", VMAX.get(), mode); + return true; + } + + if (sendCommandCheckResult("W95:" + String(mode), String(mode))) { + VMAX.update(mode); + return true; + } + return false; +} + bool SpaInterface::sendKey(SpaKey key) { String cmd; diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index bb85061..bc4b6f1 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -227,6 +227,10 @@ class SpaInterface { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (0..2). bool setLockMode(int mode); + /// @brief Internal writer used by `VMAX` RWProperty. + /// @details Sends `W95:` to the controller and only updates + /// the cached property value when the command succeeds. + bool setVMAX(int mode); /// @brief Internal writer used by `SpaTime` RWProperty. /// @details Sends S01..S05 + S06 (via setSpaDayOfWeek) to the controller. bool setSpaTime(time_t t); @@ -808,7 +812,8 @@ class SpaInterface { /// @brief Service interval 3 period (hours). ROProperty Ser3; /// @brief Maximum permitted supply voltage (V). - ROProperty VMAX; + /// @details Read/write. Writes `W95:` to the controller. + RWProperty VMAX{this, &SpaInterface::setVMAX}; /// @brief Adaptive hysteresis setting x10 (°C). ROProperty AHYS; /// @brief Minimum power level for load management (kW x10). diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index f1d704c..23c957f 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -138,6 +138,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["temperatures"]["heatpumpCondensor"] = si.HP_Condensor.get(); json["power"]["voltage"] = si.MainsVoltage.get(); + json["power"]["vmax"] = si.VMAX.get(); json["power"]["current"]= si.MainsCurrent.get() / 10.0; // convert value to A json["power"]["power"] = si.Power.get() / 10.0; // convert value to W json["power"]["totalenergy"]= si.Power_kWh.get() / 100.0; // convert value to kWh. diff --git a/src/main.cpp b/src/main.cpp index 2e84814..27b8f14 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -223,6 +223,14 @@ void mqttHaAutoDiscovery() { generateSensorAdJSON(output, ADConf, spa, discoveryTopic, "measurement", "A"); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Maximum Current for Heat Element"; + ADConf.valueTemplate = "{{ value_json.power.vmax }}"; + ADConf.propertyId = "vmax"; + ADConf.deviceClass = "current"; + ADConf.entityCategory = "config"; + generateTextAdJSON(output, ADConf, spa, discoveryTopic, "[0-9]{1,2}"); + mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Power"; ADConf.valueTemplate = "{{ value_json.power.power }}"; ADConf.propertyId = "Power"; @@ -538,6 +546,12 @@ void setSpaProperty(String property, String p) { } catch (const std::exception& ex) { debugE("Failed to set HELE: %s", ex.what()); } + } else if (property == "vmax") { + try { + si.VMAX = p.toInt(); + } catch (const std::exception& ex) { + debugE("Failed to set VMAX: %s", ex.what()); + } } else if (property == "status_datetime") { tmElements_t tm; tm.Year=CalendarYrToTm(p.substring(0,4).toInt()); From 5bfebf0ee3b194c1f261518b784e50b0bdcca170 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 4 Jul 2026 11:10:34 +1000 Subject: [PATCH 43/44] Change VMax to number not text --- lib/HAAutoDiscovery/HAAutoDiscovery.cpp | 14 ++++++++++++++ lib/HAAutoDiscovery/HAAutoDiscovery.h | 1 + src/main.cpp | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.cpp b/lib/HAAutoDiscovery/HAAutoDiscovery.cpp index 2ed1fff..391dfeb 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.cpp +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.cpp @@ -70,6 +70,20 @@ void generateTextAdJSON(String& output, const AutoDiscoveryInformationTemplate& serializeJson(json, output); } +void generateNumberAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, String unitOfMeasure, int min, int max, int step) { + JsonDocument json; + generateCommonAdJSON(json, config, spa, discoveryTopic, "number"); + + json["command_topic"] = spa.commandTopic + "/" + config.propertyId; + if (!unitOfMeasure.isEmpty()) json["unit_of_measurement"] = unitOfMeasure; + json["mode"] = "box"; + json["min"] = min; + json["max"] = max; + json["step"] = step; + + serializeJson(json, output); +} + void generateSwitchAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic) { JsonDocument json; generateCommonAdJSON(json, config, spa, discoveryTopic, "switch"); diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.h b/lib/HAAutoDiscovery/HAAutoDiscovery.h index a3ebace..69034d2 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.h +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.h @@ -39,6 +39,7 @@ void generateCommonAdJSON(JsonDocument& json, const AutoDiscoveryInformationTemp void generateSensorAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, String stateClass="", String unitOfMeasure=""); void generateBinarySensorAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic); void generateTextAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, String regex=""); +void generateNumberAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, String unitOfMeasure="", int min=0, int max=100, int step=1); void generateSwitchAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic); void generateButtonAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic); diff --git a/src/main.cpp b/src/main.cpp index 27b8f14..9fdf140 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -228,7 +228,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "vmax"; ADConf.deviceClass = "current"; ADConf.entityCategory = "config"; - generateTextAdJSON(output, ADConf, spa, discoveryTopic, "[0-9]{1,2}"); + generateNumberAdJSON(output, ADConf, spa, discoveryTopic, "A", 3, 25, 1); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Power"; From bdd8f5ac8283ccc1de35a49a1b76615f6a851756 Mon Sep 17 00:00:00 2001 From: wayne-love Date: Sat, 4 Jul 2026 12:11:15 +1000 Subject: [PATCH 44/44] Add CLMT as settable & Sanatize cycle start time --- lib/SpaInterface/SpaInterface.cpp | 36 +++++++++++++++++++++++++++++++ lib/SpaInterface/SpaInterface.h | 16 ++++++++++++-- lib/SpaUtils/SpaUtils.cpp | 2 ++ src/main.cpp | 28 ++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index d3a9a74..6a33191 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -234,6 +234,42 @@ bool SpaInterface::setHELE(bool mode){ return false; } +bool SpaInterface::setCLMT(int mode){ + debugD("setCLMT - %i", mode); + if (mode < 10 || mode > 60) { + throw std::out_of_range("CLMT value out of range (10..60)"); + } + + if (mode == CLMT.get()) { + debugD("No CLMT change detected - current %i, new %i", CLMT.get(), mode); + return true; + } + + if (sendCommandCheckResult("W85:" + String(mode), String(mode))) { + CLMT.update(mode); + return true; + } + return false; +} + +bool SpaInterface::setWCLNTime(int value){ + debugD("setWCLNTime - %i", value); + if (value < 0 || value > 5947) { + throw std::out_of_range("WCLNTime value out of range (0..5947)"); + } + + if (value == WCLNTime.get()) { + debugD("No WCLNTime change detected - current %i, new %i", WCLNTime.get(), value); + return true; + } + + if (sendCommandCheckResult("W73:" + String(value), String(value))) { + WCLNTime.update(value); + return true; + } + return false; +} + bool SpaInterface::setVMAX(int mode){ debugD("setVMAX - %i", mode); if (mode == VMAX.get()) { diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index bc4b6f1..a3db354 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -227,6 +227,16 @@ class SpaInterface { /// the cached property value when the command succeeds. /// Throws std::out_of_range for invalid values (0..2). bool setLockMode(int mode); + /// @brief Internal writer used by `WCLNTime` RWProperty. + /// @details Sends `W73:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (0..5947). + bool setWCLNTime(int value); + /// @brief Internal writer used by `CLMT` RWProperty. + /// @details Sends `W85:` to the controller and only updates + /// the cached property value when the command succeeds. + /// Throws std::out_of_range for invalid values (10..60). + bool setCLMT(int mode); /// @brief Internal writer used by `VMAX` RWProperty. /// @details Sends `W95:` to the controller and only updates /// the cached property value when the command succeeds. @@ -589,7 +599,8 @@ class SpaInterface { ROProperty WaterPresent; // R3 /// @brief Current limit setting (A). Range 10–60A; should match the circuit breaker rating feeding the spa (C.LMT OEM setting). - ROProperty CLMT; + /// @details Read/write. Writes `W85:` to the controller. + RWProperty CLMT{this, &SpaInterface::setCLMT}; /// @brief Mains phase configuration (1=Single Phase, 2=Dual Phase, 3=Three Phase). ROProperty PHSE; /// @brief Phase 1 load limit — maximum number of loads (pumps/blower) allowed to run simultaneously on phase 1. Range 1–5 (x.LLM OEM setting). @@ -790,7 +801,8 @@ class SpaInterface { ROProperty HIFI; // R7 /// @brief Auto sanitise cycle start time encoded as h*256+m. Range 0–5947. e.g. 2304 = 9:00. Set via W73. - ROProperty WCLNTime; + /// @details Read/write. Writes `W73:` to the controller. + RWProperty WCLNTime{this, &SpaInterface::setWCLNTime}; /// @brief Maximum mains voltage recorded this session (V). ROProperty V_Max; /// @brief Minimum mains voltage recorded this session (V). diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index 23c957f..e0b5639 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -139,6 +139,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["power"]["voltage"] = si.MainsVoltage.get(); json["power"]["vmax"] = si.VMAX.get(); + json["power"]["clmt"] = si.CLMT.get(); json["power"]["current"]= si.MainsCurrent.get() / 10.0; // convert value to A json["power"]["power"] = si.Power.get() / 10.0; // convert value to W json["power"]["totalenergy"]= si.Power_kWh.get() / 100.0; // convert value to kWh. @@ -164,6 +165,7 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["filtration"]["blockDuration"] = si.FiltBlockHrs.get(); json["filtration"]["hours"] = si.FiltHrs.get(); + json["filtration"]["wclnTime"] = convertToTime(si.WCLNTime.get()); json["lockmode"] = si.LockMode.getLabel(); diff --git a/src/main.cpp b/src/main.cpp index 9fdf140..95fc45f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -231,6 +231,22 @@ void mqttHaAutoDiscovery() { generateNumberAdJSON(output, ADConf, spa, discoveryTopic, "A", 3, 25, 1); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Current Limit"; + ADConf.valueTemplate = "{{ value_json.power.clmt }}"; + ADConf.propertyId = "clmt"; + ADConf.deviceClass = "current"; + ADConf.entityCategory = "config"; + generateNumberAdJSON(output, ADConf, spa, discoveryTopic, "A", 10, 60, 1); + mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + + ADConf.displayName = "Sanitise Start Time"; + ADConf.valueTemplate = "{{ value_json.filtration.wclnTime }}"; + ADConf.propertyId = "wclnTime"; + ADConf.deviceClass = ""; + ADConf.entityCategory = "config"; + generateTextAdJSON(output, ADConf, spa, discoveryTopic, "[0-2][0-9]:[0-9]{2}"); + mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); + ADConf.displayName = "Power"; ADConf.valueTemplate = "{{ value_json.power.power }}"; ADConf.propertyId = "Power"; @@ -552,6 +568,18 @@ void setSpaProperty(String property, String p) { } catch (const std::exception& ex) { debugE("Failed to set VMAX: %s", ex.what()); } + } else if (property == "clmt") { + try { + si.CLMT = p.toInt(); + } catch (const std::exception& ex) { + debugE("Failed to set CLMT: %s", ex.what()); + } + } else if (property == "wclnTime") { + try { + si.WCLNTime = convertToInteger(p); + } catch (const std::exception& ex) { + debugE("Failed to set WCLNTime: %s", ex.what()); + } } else if (property == "status_datetime") { tmElements_t tm; tm.Year=CalendarYrToTm(p.substring(0,4).toInt());