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 diff --git a/.gitignore b/.gitignore index 8293886..f7c9bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ .vscode/ipch .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..391dfeb 100644 --- a/lib/HAAutoDiscovery/HAAutoDiscovery.cpp +++ b/lib/HAAutoDiscovery/HAAutoDiscovery.cpp @@ -70,50 +70,36 @@ void generateTextAdJSON(String& output, const AutoDiscoveryInformationTemplate& serializeJson(json, output); } -void generateSwitchAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic) { +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, "switch"); + 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 generateFanAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, int min, int max, const String* modes, const size_t modesSize) { +void generateSwitchAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic) { JsonDocument json; - generateCommonAdJSON(json, config, spa, discoveryTopic, "fan"); + generateCommonAdJSON(json, config, spa, discoveryTopic, "switch"); - // 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]); - } + json["command_topic"] = spa.commandTopic + "/" + config.propertyId; - if (config.propertyId.startsWith("pump")) { - json["icon"] = "mdi:pump"; - } + 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); } diff --git a/lib/HAAutoDiscovery/HAAutoDiscovery.h b/lib/HAAutoDiscovery/HAAutoDiscovery.h index ed66339..69034d2 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. @@ -38,7 +39,9 @@ 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); template void generateSelectAdJSON(String& output, const AutoDiscoveryInformationTemplate& config, const SpaADInformationTemplate& spa, String &discoveryTopic, const std::array& options) { @@ -52,7 +55,63 @@ 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 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); +} + +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) { @@ -94,6 +153,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 0c88bcf..12623d0 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -26,11 +26,12 @@ void SpaInterface::begin() { 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() {} - void SpaInterface::setSpaPollFrequency(int updateFrequency) { _updateFrequency = updateFrequency; } @@ -93,121 +94,242 @@ bool SpaInterface::sendCommandCheckResult(String cmd, String expected){ return outcome; } +void SpaInterface::_processDebugCommand() { + if (_instance == nullptr) return; + + String cmd = Debug.getLastCommand(); + if (!cmd.startsWith("ss ") && !cmd.startsWith("SS ")) return; + + String payload = cmd.substring(3); + 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(); + + // Wait up to 2s for first byte, then collect until 500ms gap + String response = ""; + 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(); + } + } + } + + 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 == 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; } +bool SpaInterface::setStatusResponse(String s) { + return true; +} + 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; } -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; } +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()) { + 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; + 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); - if (temp==getSTMP()) { - debugD("No STMP change detected - current %i, new %i", getSTMP(), temp); - return true; // No change needed + if (temp < 50 || temp > 410) { + throw std::out_of_range("STMP value out of range (50..410)"); } - // check the temperate is within the valid range - if (temp < 50 || temp > 410) { - debugE("STMP out of range - %i", temp); - return false; + 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 } + if (temp % 2 != 0) { temp++; } @@ -215,7 +337,7 @@ bool SpaInterface::setSTMP(int temp){ String stemp = String(temp); if (sendCommandCheckResult("W40:" + stemp, stemp)) { - update_STMP(stemp); + STMP.update(temp); return true; } return false; @@ -223,13 +345,24 @@ bool SpaInterface::setSTMP(int temp){ 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); + 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_1SNZ_DAY value out of range"); + } + + 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; @@ -237,13 +370,22 @@ 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 < 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; } if (sendCommandCheckResult(String("W68:")+mode,String(mode))) { - update_L_1SNZ_BGN(String(mode)); + L_1SNZ_BGN.update(mode); return true; } return false; @@ -251,13 +393,22 @@ 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 < 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; } if (sendCommandCheckResult(String("W69:")+mode,String(mode))) { - update_L_1SNZ_END(String(mode)); + L_1SNZ_END.update(mode); return true; } return false; @@ -265,13 +416,24 @@ 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); + 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; } if (sendCommandCheckResult(String("W70:")+mode,String(mode))) { - update_L_2SNZ_DAY(String(mode)); + L_2SNZ_DAY.update(mode); return true; } return false; @@ -279,13 +441,22 @@ 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 < 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; } if (sendCommandCheckResult(String("W71:")+mode,String(mode))) { - update_L_2SNZ_BGN(String(mode)); + L_2SNZ_BGN.update(mode); return true; } return false; @@ -293,79 +464,80 @@ 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 < 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; } if (sendCommandCheckResult(String("W72:")+mode,String(mode))) { - update_L_2SNZ_END(String(mode)); + L_2SNZ_END.update(mode); return true; } return false; } 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 < 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; } 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 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; } String smode = String(mode); if (sendCommandCheckResult("S07:"+smode,smode)) { - update_ColorMode(smode); + ColorMode.update(mode); return true; } return false; } -bool SpaInterface::setColorMode(String mode){ - debugD("setColorMode - %s", mode.c_str()); - for (uint x=0; x 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; @@ -373,38 +545,37 @@ 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()) { - 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; @@ -412,14 +583,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; @@ -431,7 +605,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); @@ -442,35 +616,38 @@ 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){ - 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; @@ -478,69 +655,72 @@ 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; } 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; x24) { - 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; @@ -548,18 +728,15 @@ bool SpaInterface::setFiltHrs(String duration){ 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; @@ -583,11 +760,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 @@ -696,7 +873,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); @@ -744,6 +921,12 @@ void SpaInterface::updateStatus() { void SpaInterface::loop(){ + if (!_debugInitialised) { + Debug.setHelpProjectsCmds("ss - Send raw command to spa serial and print response"); + Debug.setCallBackProjectCmds(&SpaInterface::_processDebugCommand); + _debugInitialised = true; + } + if ( _lastWaitMessage + 1000 < millis()) { debugV("Waiting..."); _lastWaitMessage = millis(); @@ -772,206 +955,215 @@ void SpaInterface::clearUpdateCallback() { void SpaInterface::updateMeasures() { #pragma region R2 - update_MainsCurrent(statusResponseRaw[R2+1]); - update_MainsVoltage(statusResponseRaw[R2+2]); - update_CaseTemperature(statusResponseRaw[R2+3]); - update_PortCurrent(statusResponseRaw[R2+4]); - update_SpaDayOfWeek(statusResponseRaw[R2+5]); - 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]); - 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]); + MainsCurrent.update(statusResponseRaw[R2+1].toInt()); + 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; + 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)); + } + 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 - update_Mode(statusResponseRaw[R4+1]); - 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]); + try { Mode.updateFromLabel(statusResponseRaw[R4+1].c_str()); } catch (const std::exception& ex) { debugE("Mode update failed: %s", ex.what()); } + 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]); - 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_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()); + 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()); + 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]); - update_LBRTValue(statusResponseRaw[R6 + 2]); - update_CurrClr(statusResponseRaw[R6 + 3]); - update_ColorMode(statusResponseRaw[R6 + 4]); - update_LSPDValue(statusResponseRaw[R6 + 5]); - update_FiltHrs(statusResponseRaw[R6 + 6]); - update_FiltBlockHrs(statusResponseRaw[R6 + 7]); - update_STMP(statusResponseRaw[R6 + 8]); - 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]); - update_L_2SNZ_DAY(statusResponseRaw[R6 + 14]); - 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]); - 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]); + VARIValue.update(statusResponseRaw[R6 + 1].toInt()); + LBRTValue.update(statusResponseRaw[R6 + 2].toInt()); + CurrClr.update(statusResponseRaw[R6 + 3].toInt()); + ColorMode.update(statusResponseRaw[R6 + 4].toInt()); + LSPDValue.update(statusResponseRaw[R6 + 5].toInt()); + FiltHrs.update(statusResponseRaw[R6 + 6].toInt()); + FiltBlockHrs.update(statusResponseRaw[R6 + 7].toInt()); + STMP.update(statusResponseRaw[R6 + 8].toInt()); + 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()); + 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]); - update_HELE(statusResponseRaw[R7 + 25]); - update_HPMP(statusResponseRaw[R7 + 26]); - update_PMIN(statusResponseRaw[R7 + 27]); - update_PFLT(statusResponseRaw[R7 + 28]); - update_PHTR(statusResponseRaw[R7 + 29]); - update_PMAX(statusResponseRaw[R7 + 30]); + 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()); + 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[]); @@ -981,10 +1173,10 @@ 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]); + HP_Present.update(statusResponseRaw[RE + 1].toInt()); //HP_FlowSwitch.updateValue(statusResponseRaw[]); //HP_HighSwitch.updateValue(statusResponseRaw[]); //HP_LowSwitch.updateValue(statusResponseRaw[]); @@ -993,27 +1185,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[]); @@ -1024,17 +1216,17 @@ 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]); - update_LockMode(statusResponseRaw[RG + 12]); + 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 1ba0a3f..647f8b8 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -4,16 +4,20 @@ #include #include #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. #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 { +class SpaInterface { private: - /// @brief How often to pole the spa for updates in seconds. int _updateFrequency = 60; @@ -66,14 +70,11 @@ class SpaInterface : public SpaProperties { void updateMeasures(); - - /// @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. 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 @@ -91,6 +92,13 @@ class SpaInterface : public SpaProperties { void flushSerialReadBuffer() { flushSerialReadBuffer(false); }; 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; @@ -101,211 +109,942 @@ class SpaInterface : public SpaProperties { /// @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; u_long _lastWaitMessage = millis(); - - - public: - /// @brief Init SpaInterface. - SpaInterface(); - - /// @brief Initialize serial communication with the spa controller. - /// - /// This method MUST be called from setup() after the Arduino framework - /// is initialized. On ESP32-C6 (ESPA_V2), serial initialization in - /// global constructors causes crashes, so this deferred initialization - /// pattern is required. It is safe to use on all platforms. - /// - /// @note Called automatically by legacy code, but explicit call in - /// setup() is now required for ESP32-C6 compatibility. - void begin(); - - ~SpaInterface(); - - /// @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 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 - bool isInitialised(); - - /// @brief Set the function to be called when properties have been updated. - /// @param f - void setUpdateCallback(void (*f)()); - - /// @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 Internal writer used by `HPMP` RWProperty. + /// @details Sends `W99:` 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 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. + /// 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 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 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 /// @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 - 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 + bool setL_1SNZ_BGN(int mode); + bool setL_1SNZ_END(int mode); 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 - 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 - bool setLBRTValue(int mode); - - /// @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 - 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 + /// @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 Set the operating mode for pump 2 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful + /// @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 Set the operating mode for pump 3 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful + /// @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 Set the operating mode for pump 4 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful + /// @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 Set the operating mode for pump 5 - /// @param mode 0 = off, 1 = on, 4 = auto (if supported) - /// @return True if successful + /// @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); - + /// @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); + /// @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); + /// @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); + /// @brief Internal writer used by `Mode` RWProperty. + /// @details Sends `W66:` 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); + /// @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); + /// @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); + /// @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); + /// @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); + /// @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. + 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); + /// @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); - /// @brief Set aux element operating mode - /// @param mode 0 = off, 1 = on - /// @return True if successful - bool setHELE(int mode); + public: + /// @brief Init SpaInterface. + SpaInterface(); - /// @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 Initialize serial communication with the spa controller. + /// + /// This method MUST be called from setup() after the Arduino framework + /// is initialized. On ESP32-C6 (ESPA_V2), serial initialization in + /// global constructors causes crashes, so this deferred initialization + /// pattern is required. It is safe to use on all platforms. + /// + /// @note Called automatically by legacy code, but explicit call in + /// setup() is now required for ESP32-C6 compatibility. + void begin(); - /// @brief Sets the clock on the spa - /// @param t Time - /// @return True if successful - bool setSpaTime(time_t t); + ~SpaInterface(); - /// @brief Controls the air blower - /// @param mode 0 = Varible, 1 = Ramp, 2 = Off - /// @return True if successful - bool setOutlet_Blower(int mode); + // Read-only value holder synced from the spa; external code can only read. + 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; array size is deduced. + template + ROProperty(const LabelValue (&map)[N]) + : _map(map), _mapSize(N) {} + + 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) { + return fallback; + } + for (size_t i = 0; i < _mapSize; i++) { + if (_map[i].value == _value) { + return _map[i].label; + } + } + 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; + } + return _map[index].label; + } + + protected: + T _value{}; + bool _hasValue = false; + 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) { + 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. + 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 + "'").c_str()); + } + + 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 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 label/value map for setLabel/getLabel. + template + RWProperty(SpaInterface* owner, WriteFunction writer, + const LabelValue (&map)[N]) + : ROProperty(map), + _owner(owner), + _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; + } + + if (!_owner || !_writer) { + throw std::invalid_argument("RWProperty has no owner/writer"); + } + + if (!(_owner->*_writer)(newValue)) { + throw std::runtime_error("RWProperty write failed"); + } + + this->update(newValue); + } + + RWProperty& operator=(T newValue) { + set(newValue); + 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; + WriteFunction _writer = nullptr; + }; - /// @brief Set the speed of the air blower - /// @param mode 1 = low, 5 = high - /// @return True if successful - bool setVARIValue(int mode); + 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}, + }; + static constexpr ROProperty::LabelValue ColorMode_Map[] = { + {"White", 0}, + {"Color", 1}, + {"Fade", 2}, + {"Step", 3}, + {"Party", 4}, + }; + static constexpr ROProperty::LabelValue LSPDValue_Map[] = { + {"1", 1}, + {"2", 2}, + {"3", 3}, + {"4", 4}, + {"5", 5}, + }; + 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}, + }; + /// @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}, + {"Weekends", 96}, + {"Weekdays", 31}, + {"Monday", 16}, + {"Tuesday", 8}, + {"Wednesday", 4}, + {"Thuesday", 2}, + {"Friday", 1}, + {"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}, + }; - /// @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); + public: + /// @brief configure how often the spa is polled in seconds. + /// @param SpaPollFrequency + void setSpaPollFrequency(int updateFrequency); - /// @brief Set filtration block duration (1,2,3,4,6,8,12,24 hours) - /// @param duration - /// @return - bool setFiltBlockHrs(String duration); + /// @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 Set filtration hours (1 to 24 hours) - /// @param duration - bool setFiltHrs(String duration); + /// @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); - bool setLockMode(int mode); -}; + /// @brief Complete RF command response in a single string + 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 + /// @brief Controller case temperature x10 (°C). e.g. 250 = 25.0°C. + 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 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; + /// @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 x10 (hours). e.g. 33207 = 3320.7 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). Range 10–60A; should match the circuit breaker rating feeding the spa (C.LMT OEM setting). + /// @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). + ROProperty LLM1; + /// @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 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; + /// @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. SW1: Circulation pump fitted (ON=Fitted, OFF=Not Fitted). + ROProperty D1; + /// @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. 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. 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. SW5: Phase input selection (ON=2/3 Phase, OFF=Single Phase). Enables SW6 when ON. + ROProperty D5; + /// @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; + ROProperty LS; + 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 Variable heat element current draw x10 (A). e.g. 77 = 7.7A. + ROProperty EC; + /// @brief Heater ambient air temperature x10 (°C). + ROProperty HAMB; + /// @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). + 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. Note: encoding unknown; this property is never populated from the RF response. + ROProperty RB_TP_Blower; + /// @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). + 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 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 Pump and blower auto time-out duration (minutes). Range 10–60. Set via W74. + 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 Auto sanitise cycle start time encoded as h*256+m. Range 0–5947. e.g. 2304 = 9:00. Set via W73. + /// @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). + 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). + /// @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). + 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 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; + /// @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; + // 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; + /// @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 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(); -// Define the function pointer type for getPumpInstallState functions -typedef String (SpaInterface::*GetPumpStateInstallFunction)(); + /// @brief Have we sucessfuly read the registers from the SpaNet controller. + /// @return + bool isInitialised(); -// 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 -}; + /// @brief Set the function to be called when properties have been updated. + /// @param f + void setUpdateCallback(void (*f)()); -// Define the function pointer type for getPumpState functions -typedef int (SpaInterface::*GetPumpStateFunction)(); + /// @brief Clear the call back function. + void clearUpdateCallback(); -// 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 + /// @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, + &SpaInterface::RB_TP_Pump2, + &SpaInterface::RB_TP_Pump3, + &SpaInterface::RB_TP_Pump4, + &SpaInterface::RB_TP_Pump5, + }; + + using PumpInstallStatePtr = ROProperty SpaInterface::*; + static constexpr PumpInstallStatePtr pumpInstallStateFunctions[] = { + &SpaInterface::Pump1InstallState, + &SpaInterface::Pump2InstallState, + &SpaInterface::Pump3InstallState, + &SpaInterface::Pump4InstallState, + &SpaInterface::Pump5InstallState, + }; }; -// 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 deleted file mode 100644 index c86b287..0000000 --- a/lib/SpaInterface/SpaProperties.cpp +++ /dev/null @@ -1,877 +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_MainsCurrent(const String& s){ - return updateIntProperty(MainsCurrent, s); -} - -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; - 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); -} - -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_Mode(const String& s){ - return updateStringProperty(Mode, 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_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); -} - -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); -} - -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_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); -} - -boolean SpaProperties::update_ColorMode(const String& s){ - return updateIntProperty(ColorMode, s); -} - -boolean SpaProperties::update_LSPDValue(const String& s){ - return updateIntProperty(LSPDValue, s); -} - -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_STMP(const String& s){ - return updateIntProperty(STMP, 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_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); -} - -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); -} - -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_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); -} - -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_Outlet_Blower(const String& s){ - return updateIntProperty(Outlet_Blower, 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); -} - -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 deleted file mode 100644 index 9f381b9..0000000 --- a/lib/SpaInterface/SpaProperties.h +++ /dev/null @@ -1,1589 +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 current draw (A) - Property MainsCurrent; - /// @brief Mains voltage (V) - Property MainsVoltage; - /// @brief Internal case temperature ('C) - Property CaseTemperature; - /// @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) - 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 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 - 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; - /// @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 - /// - /// 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 Blower variable speed - /// - /// min 1, max 5 - Property VARIValue; - /// @brief Lights brightness - /// - /// min 1, max 5 - Property LBRTValue; - /// @brief Light colour - /// - /// 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 - Property LSPDValue; - /// @brief Filter run time (in hours) per block - Property FiltSetHrs; - /// @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 - /// - /// 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 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 - 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) - 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 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 Heatpump mode - /// - /// 0 = Auto, 1 = Heat, 2 = Cool, 3 = disabled - Property HPMP; - /// @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; - /// @brief Blower status - /// - /// 0 = variable mode, 1 = ramp mode, 2 = off - Property Outlet_Blower; -#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; - /// @brief Lock mode - /// - /// 0 = keypad unlocked, 1 = partial lock, 2 = full lock - Property LockMode; - -#pragma endregion - - -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&); - 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&); - 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_Mode(const String&); - 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_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&); - 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_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&); - 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&); - 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&); - 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_HELE(const String&); - boolean update_HPMP(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); - boolean update_Outlet_Blower(const 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&); - boolean update_LockMode(const String&); -#pragma endregion - - -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); } - - /// @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 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(); } - 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(); } - 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); } - - 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); } - - 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); } - - 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(); } - 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); } - - 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 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}; - - 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"}; - - int getFiltHrs() { return FiltSetHrs.getValue(); } - void setFiltHrsCallback(void (*callback)(int)) { FiltSetHrs.setCallback(callback); } - // 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 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); } - - 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_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}; - - 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); } - - 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); } - - 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); } - - 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 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); } - - 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); } - - 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 8ab7a37..f98936c 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 @@ -78,7 +78,8 @@ bool getPumpModesJson(SpaInterface &si, int pumpNumber, JsonObject pumps) { } } - int pumpState = (si.*(pumpStateFunctions[pumpNumber - 1]))(); + 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"; else pumps[pumpKey]["mode"] = "Manual"; @@ -129,40 +130,44 @@ 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"]["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["power"]["voltage"] = si.getMainsVoltage(); - json["power"]["current"]= si.getMainsCurrent() / 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["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"]["controller"] = si.getModel(); - String firmware = si.getSVER().substring(3); + json["temperatures"]["setPoint"] = si.STMP.get() / 10.0; + 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.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. + 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"; + json["status"]["state"] = si.Status.get(); + json["status"]["spaMode"] = si.Mode.getLabel(); + 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"; json["eSpa"]["model"] = xstr(PIOENV); json["eSpa"]["update"]["installed_version"] = xstr(BUILD_INFO); - json["heatpump"]["mode"] = si.HPMPStrings[si.getHPMP()]; - json["heatpump"]["auxheat"] = si.getHELE()==0? "OFF" : "ON"; + json["heatpump"]["mode"] = si.HPMP.getLabel(); + json["heatpump"]["auxheat"] = si.HELE ? "ON" : "OFF"; - json["filtration"]["blockDuration"] = si.getFiltBlockHrs(); - json["filtration"]["hours"] = si.getFiltHrs(); + json["filtration"]["blockDuration"] = si.FiltBlockHrs.get(); + json["filtration"]["hours"] = si.FiltHrs.get(); + json["filtration"]["wclnTime"] = convertToTime(si.WCLNTime.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 @@ -172,58 +177,43 @@ 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.spaDayOfWeekStrings[si.getSpaDayOfWeek()]; - - 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()); - - 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()); - } - member++; - } - 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["status"]["dayOfWeek"]=si.SpaDayOfWeek.getLabel(); + + 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.VARIValue.get()); - json["lights"]["speed"] = si.getLSPDValue(); - json["lights"]["state"] = si.getRB_TP_Light()? "ON": "OFF"; - json["lights"]["effect"] = si.colorModeStrings[si.getColorMode()]; - json["lights"]["brightness"] = si.getLBRTValue(); + 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.LSPDValue.get(); + json["lights"]["state"] = si.RB_TP_Light.get()? "ON": "OFF"; + json["lights"]["effect"] = si.ColorMode.getLabel(); + 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.getColorMode() == 0) { + 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; - } - } + int hue = atoi(si.CurrClr.getLabel("0")); json["lights"]["color"]["h"] = hue; json["lights"]["color"]["s"] = 100; } 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/platformio.ini b/platformio.ini index 904dfe6..b00dfed 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,10 +13,11 @@ default_envs = esp32dev, espa-v1, espa-v2 ; data_dir = {$PROJECT_DIR}/data [env:spa-base] -platform = espressif32 +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 board_build.filesystem = spiffs lib_deps = https://github.com/ktos/RemoteDebug.git@^3.0.7 diff --git a/register-map.md b/register-map.md new file mode 100644 index 0000000..929c25f --- /dev/null +++ b/register-map.md @@ -0,0 +1,424 @@ +# 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` | 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:` | + +--- + +## 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* | 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` | 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 | — | + +--- + +## 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` | 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 | — | +| 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. 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` | 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 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:` | + +--- + +## 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) / `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 | *(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>` | +| 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 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 | — | +| 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 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>` | +| 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. 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. 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>` | + +--- + +## 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 | +| `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 | 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)| +| `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 | +| `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 | +| `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 | +| `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 | + diff --git a/src/main.cpp b/src/main.cpp index 2835140..455cf42 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -103,6 +104,7 @@ void startWiFiManager(){ config.writeConfig(); } + WiFi.disconnect(true); // Force teardown of all socket connections ESP.restart(); // Restart the ESP to apply the new settings } @@ -142,6 +144,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?? } @@ -233,6 +236,38 @@ 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 = "Maximum Current for Heat Element"; + ADConf.valueTemplate = "{{ value_json.power.vmax }}"; + ADConf.propertyId = "vmax"; + ADConf.deviceClass = "current"; + ADConf.entityCategory = "config"; + 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"; @@ -286,28 +321,26 @@ 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); 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); } } - if (si.getHP_Present()) { + if (si.HP_Present.get()) { ADConf.displayName = "Heatpump Ambient Temperature"; ADConf.valueTemplate = "{{ value_json.temperatures.heatpumpAmbient }}"; ADConf.propertyId = "HPAmbTemp"; @@ -329,7 +362,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"; @@ -346,7 +379,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"; @@ -354,7 +387,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"; @@ -362,7 +395,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"; @@ -370,7 +403,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); /* @@ -428,7 +461,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "blower"; ADConf.deviceClass = ""; ADConf.entityCategory = ""; - generateFanAdJSON(output, ADConf, spa, discoveryTopic, 1, 5, si.blowerStrings.data(), si.blowerStrings.size()); + generateFanAdJSON(output, ADConf, spa, discoveryTopic, 1, 5, si.Outlet_Blower); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); ADConf.displayName = "Spa Mode"; @@ -436,7 +469,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"; @@ -444,7 +477,7 @@ void mqttHaAutoDiscovery() { ADConf.propertyId = "filtration_blockDuration"; ADConf.deviceClass = ""; ADConf.entityCategory = "config"; - generateSelectAdJSON(output, ADConf, spa, discoveryTopic, si.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 @@ -458,12 +491,20 @@ 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"; 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); } @@ -490,9 +531,17 @@ 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); + 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'; @@ -501,18 +550,57 @@ 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 < array_count(SpaInterface::pumpStatuses)) + 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 < 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 + } 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); + 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 + 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); + try { + si.HELE.set(p != "OFF"); + } 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 == "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()); @@ -521,67 +609,139 @@ 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") { - 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); + 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") { - 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()); + 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) { 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") { - 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); + 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.VARIValue.set(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); - } 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") - si.setL_1SNZ_DAY(si.sleepBitmap[member]); - else if (property == "sleepTimers_2_state") - si.setL_2SNZ_DAY(si.sleepBitmap[member]); - break; - } - member++; + 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()); + } 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") { - 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); + 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); + 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); + 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) { - 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 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()); @@ -741,6 +901,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..."); @@ -790,7 +951,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("/"); @@ -866,4 +1027,4 @@ void loop() { } mqttClient.loop(); -} \ No newline at end of file +}