Skip to content

Commit 20c4fa1

Browse files
committed
feat: More functionality (#12)
* docs: add comment explaing the purpose of the xml postprocessor * feat: add support for changing heater modes * fix: mark supports_cooling as not required for heater equip * fix: mark solar_set_point as optional for vheaters * fix: add SENSOR_EXT_INPUT as a Sensor Type * fix: add active_inactive as a sensor unit
1 parent 4dce628 commit 20c4fa1

File tree

5 files changed

+60
-5
lines changed

5 files changed

+60
-5
lines changed

pyomnilogic_local/api.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
from .models.telemetry import Telemetry
1111
from .models.util import to_pydantic
1212
from .protocol import OmniLogicProtocol
13-
from .types import ColorLogicBrightness, ColorLogicShow, ColorLogicSpeed, MessageType
13+
from .types import (
14+
ColorLogicBrightness,
15+
ColorLogicShow,
16+
ColorLogicSpeed,
17+
HeaterMode,
18+
MessageType,
19+
)
1420

1521
_LOGGER = logging.getLogger(__name__)
1622

@@ -165,6 +171,34 @@ async def async_set_solar_heater(self, pool_id: int, equipment_id: int, temperat
165171

166172
return await self.async_send_message(MessageType.SET_SOLAR_SET_POINT_COMMAND, req_body, False)
167173

174+
async def async_set_heater_mode(self, pool_id: int, equipment_id: int, mode: HeaterMode) -> None:
175+
"""async_set_heater_enable handles sending a SetHeaterEnable XML API call to the Hayward Omni pool controller
176+
177+
Args:
178+
pool_id (int): The Pool/BodyOfWater ID that you want to address
179+
equipment_id (int): Which equipment_id within that Pool to address
180+
enabled (bool, optional): Turn the heater on (True) or off (False)
181+
182+
Returns:
183+
_type_: _description_
184+
"""
185+
body_element = ET.Element("Request", {"xmlns": "http://nextgen.hayward.com/api"})
186+
187+
name_element = ET.SubElement(body_element, "Name")
188+
name_element.text = "SetUIHeaterModeCmd"
189+
190+
parameters_element = ET.SubElement(body_element, "Parameters")
191+
parameter = ET.SubElement(parameters_element, "Parameter", name="poolId", dataType="int")
192+
parameter.text = str(pool_id)
193+
parameter = ET.SubElement(parameters_element, "Parameter", name="HeaterID", dataType="int", alias="EquipmentID")
194+
parameter.text = str(equipment_id)
195+
parameter = ET.SubElement(parameters_element, "Parameter", name="Mode", dataType="int", alias="Data")
196+
parameter.text = str(mode.value)
197+
198+
req_body = ET.tostring(body_element, xml_declaration=True, encoding="unicode")
199+
200+
return await self.async_send_message(MessageType.SET_HEATER_MODE_COMMAND, req_body, False)
201+
168202
async def async_set_heater_enable(self, pool_id: int, equipment_id: int, enabled: int | bool) -> None:
169203
"""async_set_heater_enable handles sending a SetHeaterEnable XML API call to the Hayward Omni pool controller
170204

pyomnilogic_local/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ async def async_main() -> None:
7272
# Adjust solar heater set point
7373
# await omni.async_set_solar_heater(POOL_ID, HEATER_EQUIPMENT_ID, 90, "F")
7474

75+
# Set the heater to heat/cool/auto
76+
# await omni.async_set_heater_mode(POOL_ID, HEATER_EQUIPMENT_ID, HeaterMode.HEAT)
77+
# await omni.async_set_heater_mode(POOL_ID, HEATER_EQUIPMENT_ID, HeaterMode.COOL)
78+
# await omni.async_set_heater_mode(POOL_ID, HEATER_EQUIPMENT_ID, HeaterMode.AUTO)
79+
7580
# Turn a variable speed pump on to 50%
7681
# await omni.async_set_filter_speed(POOL_ID, PUMP_EQUIPMENT_ID, 50)
7782
# Turn the pump off

pyomnilogic_local/models/mspconfig.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class MSPHeaterEquip(OmniBase):
111111
enabled: Literal["yes", "no"] = Field(alias="Enabled")
112112
min_filter_speed: int = Field(alias="Min-Speed-For-Operation")
113113
sensor_id: int = Field(alias="Sensor-System-Id")
114-
supports_cooling: Literal["yes", "no"] = Field(alias="SupportsCooling")
114+
supports_cooling: Literal["yes", "no"] | None = Field(alias="SupportsCooling")
115115

116116

117117
# 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):
121121
omni_type: OmniType = OmniType.VIRT_HEATER
122122
enabled: Literal["yes", "no"] = Field(alias="Enabled")
123123
set_point: int = Field(alias="Current-Set-Point")
124-
solar_set_point: int = Field(alias="SolarSetPoint")
124+
solar_set_point: int | None = Field(alias="SolarSetPoint")
125125
max_temp: int = Field(alias="Max-Settable-Water-Temp")
126126
min_temp: int = Field(alias="Min-Settable-Water-Temp")
127127
heater_equipment: list[MSPHeaterEquip] | None

pyomnilogic_local/models/telemetry.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
FilterState,
1616
FilterValvePosition,
1717
FilterWhyOn,
18+
HeaterMode,
1819
HeaterState,
1920
OmniType,
2021
PumpState,
@@ -138,7 +139,7 @@ class TelemetryVirtualHeater(BaseModel):
138139
current_set_point: int = Field(alias="@Current-Set-Point")
139140
enabled: bool = Field(alias="@enable")
140141
solar_set_point: int = Field(alias="@SolarSetPoint")
141-
mode: int = Field(alias="@Mode")
142+
mode: HeaterMode = Field(alias="@Mode")
142143
silent_mode: int = Field(alias="@SilentMode")
143144
why_on: int = Field(alias="@whyHeaterIsOn")
144145

@@ -190,7 +191,13 @@ def xml_postprocessor(path: Any, key: Any, value: Any) -> tuple[Any, Any]:
190191
...
191192

192193
def xml_postprocessor(path: Any, key: Any, value: SupportsInt | Any) -> tuple[Any, SupportsInt | Any]:
193-
"""Post process XML to attempt to convert values to int."""
194+
"""Post process XML to attempt to convert values to int.
195+
196+
Pydantic can coerce values natively, but the Omni API returns values as strings of numbers (I.E. "2", "5", etc) and we need them
197+
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
198+
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
199+
string ints into an int, then pydantic handles the int to enum coercion if necessary.
200+
"""
194201
newvalue: SupportsInt | Any
195202

196203
try:

pyomnilogic_local/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class MessageType(Enum):
1111
SET_HEATER_COMMAND = 11
1212
REQUEST_LOG_CONFIG = 31
1313
SET_SOLAR_SET_POINT_COMMAND = 40
14+
SET_HEATER_MODE_COMMAND = 42
1415
SET_HEATER_ENABLED = 147
1516
SET_EQUIPMENT = 164
1617
CREATE_SCHEDULE = 230
@@ -224,6 +225,12 @@ class HeaterType(str, PrettyEnum):
224225
SMART = "HTR_SMART"
225226

226227

228+
class HeaterMode(PrettyEnum):
229+
HEAT = 0
230+
COOL = 1
231+
AUTO = 2
232+
233+
227234
class PumpState(PrettyEnum):
228235
OFF = 0
229236
ON = 1
@@ -277,6 +284,7 @@ class SensorType(str, PrettyEnum):
277284
SOLAR_TEMP = "SENSOR_SOLAR_TEMP"
278285
WATER_TEMP = "SENSOR_WATER_TEMP"
279286
FLOW = "SENSOR_FLOW"
287+
EXT_INPUT = "SENSOR_EXT_INPUT"
280288

281289

282290
class SensorUnits(str, PrettyEnum):
@@ -286,6 +294,7 @@ class SensorUnits(str, PrettyEnum):
286294
GRAMS_PER_LITER = "UNITS_GRAMS_PER_LITER"
287295
MILLIVOLTS = "UNITS_MILLIVOLTS"
288296
NO_UNITS = "UNITS_NO_UNITS"
297+
ACTIVE_INACTIVE = "UNITS_ACTIVE_INACTIVE"
289298

290299

291300
class ValveActuatorState(PrettyEnum):

0 commit comments

Comments
 (0)