diff --git a/pyomnilogic_local/api.py b/pyomnilogic_local/api.py index ac0e18b..4988d27 100755 --- a/pyomnilogic_local/api.py +++ b/pyomnilogic_local/api.py @@ -10,7 +10,13 @@ from .models.telemetry import Telemetry from .models.util import to_pydantic from .protocol import OmniLogicProtocol -from .types import ColorLogicBrightness, ColorLogicShow, ColorLogicSpeed, MessageType +from .types import ( + ColorLogicBrightness, + ColorLogicShow, + ColorLogicSpeed, + HeaterMode, + MessageType, +) _LOGGER = logging.getLogger(__name__) @@ -165,6 +171,34 @@ async def async_set_solar_heater(self, pool_id: int, equipment_id: int, temperat return await self.async_send_message(MessageType.SET_SOLAR_SET_POINT_COMMAND, req_body, False) + async def async_set_heater_mode(self, pool_id: int, equipment_id: int, mode: HeaterMode) -> None: + """async_set_heater_enable handles sending a SetHeaterEnable XML API call to the Hayward Omni pool controller + + Args: + pool_id (int): The Pool/BodyOfWater ID that you want to address + equipment_id (int): Which equipment_id within that Pool to address + enabled (bool, optional): Turn the heater on (True) or off (False) + + Returns: + _type_: _description_ + """ + body_element = ET.Element("Request", {"xmlns": "http://nextgen.hayward.com/api"}) + + name_element = ET.SubElement(body_element, "Name") + name_element.text = "SetUIHeaterModeCmd" + + parameters_element = ET.SubElement(body_element, "Parameters") + parameter = ET.SubElement(parameters_element, "Parameter", name="poolId", dataType="int") + parameter.text = str(pool_id) + parameter = ET.SubElement(parameters_element, "Parameter", name="HeaterID", dataType="int", alias="EquipmentID") + parameter.text = str(equipment_id) + parameter = ET.SubElement(parameters_element, "Parameter", name="Mode", dataType="int", alias="Data") + parameter.text = str(mode.value) + + req_body = ET.tostring(body_element, xml_declaration=True, encoding="unicode") + + return await self.async_send_message(MessageType.SET_HEATER_MODE_COMMAND, req_body, False) + async def async_set_heater_enable(self, pool_id: int, equipment_id: int, enabled: int | bool) -> None: """async_set_heater_enable handles sending a SetHeaterEnable XML API call to the Hayward Omni pool controller diff --git a/pyomnilogic_local/cli.py b/pyomnilogic_local/cli.py index 83e28d6..0a4ed95 100755 --- a/pyomnilogic_local/cli.py +++ b/pyomnilogic_local/cli.py @@ -72,6 +72,11 @@ async def async_main() -> None: # Adjust solar heater set point # await omni.async_set_solar_heater(POOL_ID, HEATER_EQUIPMENT_ID, 90, "F") + # Set the heater to heat/cool/auto + # await omni.async_set_heater_mode(POOL_ID, HEATER_EQUIPMENT_ID, HeaterMode.HEAT) + # await omni.async_set_heater_mode(POOL_ID, HEATER_EQUIPMENT_ID, HeaterMode.COOL) + # await omni.async_set_heater_mode(POOL_ID, HEATER_EQUIPMENT_ID, HeaterMode.AUTO) + # Turn a variable speed pump on to 50% # await omni.async_set_filter_speed(POOL_ID, PUMP_EQUIPMENT_ID, 50) # Turn the pump off diff --git a/pyomnilogic_local/models/mspconfig.py b/pyomnilogic_local/models/mspconfig.py index b197360..3818042 100644 --- a/pyomnilogic_local/models/mspconfig.py +++ b/pyomnilogic_local/models/mspconfig.py @@ -111,7 +111,7 @@ class MSPHeaterEquip(OmniBase): enabled: Literal["yes", "no"] = Field(alias="Enabled") min_filter_speed: int = Field(alias="Min-Speed-For-Operation") sensor_id: int = Field(alias="Sensor-System-Id") - supports_cooling: Literal["yes", "no"] = Field(alias="SupportsCooling") + supports_cooling: Literal["yes", "no"] | None = Field(alias="SupportsCooling") # This is the entry for the VirtualHeater, it does not use OmniBase because it has no name attribute @@ -121,7 +121,7 @@ class MSPVirtualHeater(OmniBase): omni_type: OmniType = OmniType.VIRT_HEATER enabled: Literal["yes", "no"] = Field(alias="Enabled") set_point: int = Field(alias="Current-Set-Point") - solar_set_point: int = Field(alias="SolarSetPoint") + solar_set_point: int | None = Field(alias="SolarSetPoint") max_temp: int = Field(alias="Max-Settable-Water-Temp") min_temp: int = Field(alias="Min-Settable-Water-Temp") heater_equipment: list[MSPHeaterEquip] | None diff --git a/pyomnilogic_local/models/telemetry.py b/pyomnilogic_local/models/telemetry.py index 8276123..2e9264a 100644 --- a/pyomnilogic_local/models/telemetry.py +++ b/pyomnilogic_local/models/telemetry.py @@ -15,6 +15,7 @@ FilterState, FilterValvePosition, FilterWhyOn, + HeaterMode, HeaterState, OmniType, PumpState, @@ -138,7 +139,7 @@ class TelemetryVirtualHeater(BaseModel): current_set_point: int = Field(alias="@Current-Set-Point") enabled: bool = Field(alias="@enable") solar_set_point: int = Field(alias="@SolarSetPoint") - mode: int = Field(alias="@Mode") + mode: HeaterMode = Field(alias="@Mode") silent_mode: int = Field(alias="@SilentMode") why_on: int = Field(alias="@whyHeaterIsOn") @@ -190,7 +191,13 @@ def xml_postprocessor(path: Any, key: Any, value: Any) -> tuple[Any, Any]: ... def xml_postprocessor(path: Any, key: Any, value: SupportsInt | Any) -> tuple[Any, SupportsInt | Any]: - """Post process XML to attempt to convert values to int.""" + """Post process XML to attempt to convert values to int. + + Pydantic can coerce values natively, but the Omni API returns values as strings of numbers (I.E. "2", "5", etc) and we need them + coerced into int enums. Pydantic only seems to be able to handle one coercion, so it could coerce an int into an Enum, but it + cannot coerce a string into an int and then into the Enum. We help it out a little bit here by pre-emptively coercing any + string ints into an int, then pydantic handles the int to enum coercion if necessary. + """ newvalue: SupportsInt | Any try: diff --git a/pyomnilogic_local/types.py b/pyomnilogic_local/types.py index f42b604..c31566a 100644 --- a/pyomnilogic_local/types.py +++ b/pyomnilogic_local/types.py @@ -11,6 +11,7 @@ class MessageType(Enum): SET_HEATER_COMMAND = 11 REQUEST_LOG_CONFIG = 31 SET_SOLAR_SET_POINT_COMMAND = 40 + SET_HEATER_MODE_COMMAND = 42 SET_HEATER_ENABLED = 147 SET_EQUIPMENT = 164 CREATE_SCHEDULE = 230 @@ -224,6 +225,12 @@ class HeaterType(str, PrettyEnum): SMART = "HTR_SMART" +class HeaterMode(PrettyEnum): + HEAT = 0 + COOL = 1 + AUTO = 2 + + class PumpState(PrettyEnum): OFF = 0 ON = 1 @@ -277,6 +284,7 @@ class SensorType(str, PrettyEnum): SOLAR_TEMP = "SENSOR_SOLAR_TEMP" WATER_TEMP = "SENSOR_WATER_TEMP" FLOW = "SENSOR_FLOW" + EXT_INPUT = "SENSOR_EXT_INPUT" class SensorUnits(str, PrettyEnum): @@ -286,6 +294,7 @@ class SensorUnits(str, PrettyEnum): GRAMS_PER_LITER = "UNITS_GRAMS_PER_LITER" MILLIVOLTS = "UNITS_MILLIVOLTS" NO_UNITS = "UNITS_NO_UNITS" + ACTIVE_INACTIVE = "UNITS_ACTIVE_INACTIVE" class ValveActuatorState(PrettyEnum):