From 778a50cde304109c410c901422cfb6d18fb23264 Mon Sep 17 00:00:00 2001 From: Jean-Luc Vaillant Date: Thu, 31 Dec 2020 13:25:24 -0800 Subject: [PATCH] cleanup and optimizations also added binary sensors for heaters and sensors for IntelliChem --- .gitignore | 1 + custom_components/intellicenter/__init__.py | 120 +++-- .../intellicenter/binary_sensor.py | 92 +++- custom_components/intellicenter/light.py | 34 +- .../intellicenter/pyintellicenter/__init__.py | 111 +++++ .../pyintellicenter/attributes.py | 411 +++++++++++------- .../pyintellicenter/controller.py | 37 +- .../intellicenter/pyintellicenter/model.py | 31 +- custom_components/intellicenter/sensor.py | 117 ++++- custom_components/intellicenter/switch.py | 37 +- .../intellicenter/water_heater.py | 126 ++++-- 11 files changed, 839 insertions(+), 278 deletions(-) diff --git a/.gitignore b/.gitignore index b401725..ea5c811 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .DS_Store *.code-workspace +.vscode diff --git a/custom_components/intellicenter/__init__.py b/custom_components/intellicenter/__init__.py index bbce969..1134933 100644 --- a/custom_components/intellicenter/__init__.py +++ b/custom_components/intellicenter/__init__.py @@ -22,7 +22,41 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -from .pyintellicenter import ConnectionHandler, ModelController, PoolModel, PoolObject +from .pyintellicenter import ( + ACT_ATTR, + BODY_ATTR, + BODY_TYPE, + CHEM_TYPE, + CIRCGRP_TYPE, + CIRCUIT_ATTR, + CIRCUIT_TYPE, + FEATR_ATTR, + GPM_ATTR, + HEATER_ATTR, + HEATER_TYPE, + HTMODE_ATTR, + LISTORD_ATTR, + LOTMP_ATTR, + LSTTMP_ATTR, + MODE_ATTR, + PUMP_TYPE, + PWR_ATTR, + RPM_ATTR, + SCHED_TYPE, + SENSE_TYPE, + SNAME_ATTR, + SOURCE_ATTR, + STATUS_ATTR, + SUBTYP_ATTR, + SYSTEM_TYPE, + USE_ATTR, + VACFLO_ATTR, + VOL_ATTR, + ConnectionHandler, + ModelController, + PoolModel, + PoolObject, +) _LOGGER = logging.getLogger(__name__) @@ -35,6 +69,8 @@ WATER_HEATER_DOMAIN, ] +# ------------------------------------------------------------------------------------- + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Pentair IntelliCenter Integration.""" @@ -44,28 +80,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IntelliCenter integration from a config entry.""" - # we don't need some of the system objects - def ignoreFunc(object): - """Return False for the objects we want to ignore.""" - - return ( - object.objtype - in [ - "PANEL", - "MODULE", - "PERMIT", - "SYSTIM", - ] - or object.subtype in ["LEGACY"] - ) - attributes_map = { - "BODY": {"SNAME", "HEATER", "HTMODE", "LOTMP", "LSTTMP", "STATUS"}, - "CIRCUIT": {"SNAME", "STATUS", "USE", "SUBTYPE", "FEATR"}, - "CIRCGRP": {"CIRCUIT"}, - "HEATER": {"SNAME", "BODY"}, - "PUMP": {"SNAME", "STATUS", "PWR", "RPM", "GPM"}, - "SENSE": {"SNAME", "SOURCE"}, + BODY_TYPE: { + SNAME_ATTR, + HEATER_ATTR, + HTMODE_ATTR, + LOTMP_ATTR, + LSTTMP_ATTR, + STATUS_ATTR, + VOL_ATTR, + }, + CIRCUIT_TYPE: {SNAME_ATTR, STATUS_ATTR, USE_ATTR, SUBTYP_ATTR, FEATR_ATTR}, + CIRCGRP_TYPE: {CIRCUIT_ATTR}, + CHEM_TYPE: {}, + HEATER_TYPE: {SNAME_ATTR, BODY_ATTR, LISTORD_ATTR}, + PUMP_TYPE: {SNAME_ATTR, STATUS_ATTR, PWR_ATTR, RPM_ATTR, GPM_ATTR}, + SENSE_TYPE: {SNAME_ATTR, SOURCE_ATTR}, + SCHED_TYPE: {SNAME_ATTR, ACT_ATTR, VACFLO_ATTR}, + SYSTEM_TYPE: {MODE_ATTR, VACFLO_ATTR}, } model = PoolModel(attributes_map) @@ -167,6 +199,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +# ------------------------------------------------------------------------------------- + + class PoolEntity(Entity): """Representation of an Pool entity linked to an pool object.""" @@ -175,17 +210,20 @@ def __init__( entry: ConfigEntry, controller: ModelController, poolObject: PoolObject, - attribute_key="STATUS", - name_suffix="", + attribute_key=STATUS_ATTR, + name=None, + enabled_by_default=True, + extraStateAttributes=set(), ): """Initialize a Pool entity.""" self._entry_id = entry.entry_id self._controller = controller self._poolObject = poolObject self._available = True - self._extraStateAttributes = [] - self._name_suffix = name_suffix + self._extraStateAttributes = extraStateAttributes + self._name = name self._attribute_key = attribute_key + self._enabled_by_default = enabled_by_default _LOGGER.debug(f"mapping {poolObject}") @@ -209,6 +247,11 @@ async def async_will_remove_from_hass(self) -> None: """Entity is removed from Home Assistant.""" _LOGGER.debug(f"removing entity: {self.unique_id}") + @property + def entity_registry_enabled_default(self): + """Return True if the entity is enabled by default.""" + return self._enabled_by_default + @property def available(self): """Return True is the entity is available.""" @@ -217,16 +260,21 @@ def available(self): @property def name(self): """Return the name of the entity.""" - name = self._poolObject.sname - if self._name_suffix: - name += " " + self._name_suffix - return name + + if self._name is None: + # default is to return the name of the underlying pool object + return self._poolObject.sname + elif self._name.startswith("+"): + # name is a suffix + return self._poolObject.sname + self._name[1:] + else: + return self._name @property def unique_id(self): """Return a unique ID.""" my_id = self._entry_id + self._poolObject.objnam - if self._attribute_key != "STATUS": + if self._attribute_key != STATUS_ATTR: my_id += self._attribute_key return my_id @@ -279,14 +327,18 @@ def requestChanges(self, changes: dict) -> None: self._poolObject.objnam, changes, waitForResponse=False ) + def isUpdated(self, updates: Dict[str, Dict[str, str]]) -> bool: + """Return true if the entity is updated by the updates from Intellicenter.""" + + return self._attribute_key in updates.get(self._poolObject.objnam, {}) + @callback def _update_callback(self, updates: Dict[str, Dict[str, str]]): """Update the entity if its underlying pool object has changed.""" - if self._attribute_key in updates.get(self._poolObject.objnam, {}): + if self.isUpdated(updates): self._available = True - my_updates = updates.get(self._poolObject.objnam) - _LOGGER.debug(f"updating {self} from {my_updates}") + _LOGGER.debug(f"updating {self} from {updates}") self.async_write_ha_state() @callback diff --git a/custom_components/intellicenter/binary_sensor.py b/custom_components/intellicenter/binary_sensor.py index ccbf91d..8dc0734 100644 --- a/custom_components/intellicenter/binary_sensor.py +++ b/custom_components/intellicenter/binary_sensor.py @@ -1,14 +1,22 @@ """Pentair Intellicenter binary sensors.""" import logging +from typing import Dict from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType +from custom_components.intellicenter.pyintellicenter.attributes import ( + BODY_ATTR, + CIRCUIT_TYPE, + HEATER_TYPE, +) +from custom_components.intellicenter.water_heater import HEATER_ATTR, HTMODE_ATTR + from . import PoolEntity from .const import DOMAIN -from .pyintellicenter import ModelController, PoolObject +from .pyintellicenter import STATUS_ATTR, ModelController, PoolObject _LOGGER = logging.getLogger(__name__) @@ -24,17 +32,91 @@ async def async_setup_entry( object: PoolObject for object in controller.model.objectList: - if ( - object.objtype == "CIRCUIT" and object.subtype == "FRZ" - ) or object.objtype == "PUMP": + if object.objtype == CIRCUIT_TYPE and object.subtype == "FRZ": sensors.append(PoolBinarySensor(entry, controller, object)) + elif object.objtype == HEATER_TYPE: + sensors.append( + HeaterBinarySensor( + entry, + controller, + object, + ) + ) + elif object.objtype == "SCHED": + sensors.append( + PoolBinarySensor( + entry, + controller, + object, + attribute_key="ACT", + name="+ (schedule)", + enabled_by_default=False, + extraStateAttributes={"VACFLO"}, + ) + ) + elif object.objtype == "PUMP": + sensors.append(PoolBinarySensor(entry, controller, object, valueForON="10")) async_add_entities(sensors) +# ------------------------------------------------------------------------------------- + + class PoolBinarySensor(PoolEntity, BinarySensorEntity): """Representation of a Pentair Binary Sensor.""" + def __init__( + self, + entry: ConfigEntry, + controller: ModelController, + poolObject: PoolObject, + valueForON="ON", + **kwargs, + ): + """Initialize.""" + super().__init__(entry, controller, poolObject, **kwargs) + self._valueForON = valueForON + @property def is_on(self): """Return true if sensor is on.""" - return self._poolObject.status == self._poolObject.onStatus + return self._poolObject[self._attribute_key] == self._valueForON + + +# ------------------------------------------------------------------------------------- + + +class HeaterBinarySensor(PoolEntity, BinarySensorEntity): + """Representation of a Heater binary sensor.""" + + def __init__( + self, + entry: ConfigEntry, + controller: ModelController, + poolObject: PoolObject, + **kwargs, + ): + """Initialize.""" + super().__init__(entry, controller, poolObject, **kwargs) + self._bodies = set(poolObject[BODY_ATTR].split(" ")) + + @property + def is_on(self) -> bool: + """Return true if sensor is on.""" + for bodyObjnam in self._bodies: + body = self._controller.model[bodyObjnam] + if ( + body[STATUS_ATTR] == "ON" + and body[HEATER_ATTR] == self._poolObject.objnam + and body[HTMODE_ATTR] != "0" + ): + return True + return False + + def isUpdated(self, updates: Dict[str, Dict[str, str]]) -> bool: + """Return true if the entity is updated by the updates from Intellicenter.""" + + for objnam in self._bodies & updates.keys(): + if {STATUS_ATTR, HEATER_ATTR, HTMODE_ATTR} & updates[objnam].keys(): + return True + return False diff --git a/custom_components/intellicenter/light.py b/custom_components/intellicenter/light.py index 18c9e00..95bde7e 100644 --- a/custom_components/intellicenter/light.py +++ b/custom_components/intellicenter/light.py @@ -6,12 +6,18 @@ from homeassistant.components.light import ATTR_EFFECT, SUPPORT_EFFECT, LightEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from . import PoolEntity from .const import DOMAIN -from .pyintellicenter import ModelController, PoolObject +from .pyintellicenter import ( + ACT_ATTR, + CIRCUIT_ATTR, + STATUS_ATTR, + USE_ATTR, + ModelController, + PoolObject, +) _LOGGER = logging.getLogger(__name__) @@ -55,7 +61,7 @@ async def async_setup_entry( supportColorEffects = reduce( lambda x, y: x and y, map( - lambda obj: controller.model[obj["CIRCUIT"]].supportColorEffects, + lambda obj: controller.model[obj[CIRCUIT_ATTR]].supportColorEffects, controller.model.getChildren(object), ), True, @@ -85,7 +91,7 @@ def __init__( """Initialize.""" super().__init__(entry, controller, poolObject) # USE appears to contain extra info like color... - self._extraStateAttributes = ["USE"] + self._extraStateAttributes = [USE_ATTR] self._features = 0 @@ -110,7 +116,7 @@ def effect_list(self) -> list: @property def effect(self) -> str: """Return the current effect.""" - return self._lightEffects.get(self._poolObject["USE"]) + return self._lightEffects.get(self._poolObject[USE_ATTR]) @property def is_on(self) -> bool: @@ -119,26 +125,24 @@ def is_on(self) -> bool: def turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - self.requestChanges({"STATUS": "OFF"}) + self.requestChanges({STATUS_ATTR: "OFF"}) def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - changes = {"STATUS": self._poolObject.onStatus} + changes = {STATUS_ATTR: self._poolObject.onStatus} if ATTR_EFFECT in kwargs: effect = kwargs[ATTR_EFFECT] new_use = self._reversedLightEffects.get(effect) if new_use: - changes["ACT"] = new_use + changes[ACT_ATTR] = new_use self.requestChanges(changes) - @callback - def _update_callback(self, updates: Dict[str, PoolObject]): - """Update the entity if its underlying pool object has changed.""" + def isUpdated(self, updates: Dict[str, Dict[str, str]]) -> bool: + """Return true if the entity is updated by the updates from Intellicenter.""" - if self._poolObject.objnam in updates: - self._available = True - _LOGGER.debug(f"updating {self} from {self._poolObject}") - self.async_write_ha_state() + myUpdates = updates.get(self._poolObject.objnam, {}) + + return myUpdates and {STATUS_ATTR, USE_ATTR} & myUpdates.keys() diff --git a/custom_components/intellicenter/pyintellicenter/__init__.py b/custom_components/intellicenter/pyintellicenter/__init__.py index 1cbd1b2..da31d23 100644 --- a/custom_components/intellicenter/pyintellicenter/__init__.py +++ b/custom_components/intellicenter/pyintellicenter/__init__.py @@ -1,5 +1,61 @@ """pyintellicenter module.""" +from .attributes import ( + ACT_ATTR, + BODY_ATTR, + BODY_TYPE, + CHEM_TYPE, + CIRCGRP_TYPE, + CIRCUIT_ATTR, + CIRCUIT_TYPE, + COMUART_ATTR, + DLY_ATTR, + ENABLE_ATTR, + FEATR_ATTR, + GPM_ATTR, + HEATER_ATTR, + HEATER_TYPE, + HNAME_ATTR, + HTMODE_ATTR, + LISTORD_ATTR, + LOTMP_ATTR, + LSTTMP_ATTR, + MODE_ATTR, + NORMAL_ATTR, + NULL_OBJNAM, + OBJTYP_ATTR, + ORPTNK_ATTR, + ORPVAL_ATTR, + PARENT_ATTR, + PHTNK_ATTR, + PHVAL_ATTR, + PMPCIRC_TYPE, + PROPNAME_ATTR, + PUMP_TYPE, + PWR_ATTR, + QUALTY_ATTR, + READY_ATTR, + REMBTN_TYPE, + REMOTE_TYPE, + RPM_ATTR, + SALT_ATTR, + SCHED_TYPE, + SELECT_ATTR, + SENSE_TYPE, + SHOMNU_ATTR, + SNAME_ATTR, + SOURCE_ATTR, + STATIC_ATTR, + STATUS_ATTR, + SUBTYP_ATTR, + SYSTEM_TYPE, + TIME_ATTR, + TIMOUT_ATTR, + USE_ATTR, + VACFLO_ATTR, + VER_ATTR, + VOL_ATTR, +) from .controller import ( BaseController, CommandError, @@ -17,4 +73,59 @@ SystemInfo, PoolModel, PoolObject, + BODY_TYPE, + CHEM_TYPE, + CIRCUIT_TYPE, + CIRCGRP_TYPE, + DLY_ATTR, + ENABLE_ATTR, + HEATER_TYPE, + PMPCIRC_TYPE, + PUMP_TYPE, + REMBTN_TYPE, + REMOTE_TYPE, + SCHED_TYPE, + SENSE_TYPE, + SYSTEM_TYPE, + NULL_OBJNAM, + ACT_ATTR, + BODY_ATTR, + CIRCUIT_ATTR, + COMUART_ATTR, + FEATR_ATTR, + GPM_ATTR, + HEATER_ATTR, + HNAME_ATTR, + HTMODE_ATTR, + LISTORD_ATTR, + LOTMP_ATTR, + LSTTMP_ATTR, + MODE_ATTR, + NORMAL_ATTR, + OBJTYP_ATTR, + ORPTNK_ATTR, + ORPVAL_ATTR, + PARENT_ATTR, + PHTNK_ATTR, + PHVAL_ATTR, + PROPNAME_ATTR, + PWR_ATTR, + QUALTY_ATTR, + READY_ATTR, + RPM_ATTR, + SALT_ATTR, + SELECT_ATTR, + SHOMNU_ATTR, + SNAME_ATTR, + SOURCE_ATTR, + SUBTYP_ATTR, + STATIC_ATTR, + STATUS_ATTR, + TIME_ATTR, + TIMOUT_ATTR, + USE_ATTR, + VACFLO_ATTR, + VER_ATTR, + VOL_ATTR, + RPM_ATTR, ] diff --git a/custom_components/intellicenter/pyintellicenter/attributes.py b/custom_components/intellicenter/pyintellicenter/attributes.py index a812e43..50ea115 100644 --- a/custom_components/intellicenter/pyintellicenter/attributes.py +++ b/custom_components/intellicenter/pyintellicenter/attributes.py @@ -1,5 +1,62 @@ """Definition of all the attributes per OBJTYP.""" +NULL_OBJNAM = "00000" + +BODY_TYPE = "BODY" +CHEM_TYPE = "CHEM" +CIRCUIT_TYPE = "CIRCUIT" +CIRCGRP_TYPE = "CIRCGRP" +HEATER_TYPE = "HEATER" +PMPCIRC_TYPE = "PMPCIRC" +PUMP_TYPE = "PUMP" +REMBTN_TYPE = "REMBTN" +REMOTE_TYPE = "REMOTE" +SCHED_TYPE = "SCHED" +SENSE_TYPE = "SENSE" +SYSTEM_TYPE = "SYSTEM" + +ACT_ATTR = "ACT" +BODY_ATTR = "BODY" +CIRCUIT_ATTR = "CIRCUIT" +COMUART_ATTR = "COMUART" +DLY_ATTR = "DLY" +ENABLE_ATTR = "ENABLE" +FEATR_ATTR = "FEATR" +GPM_ATTR = "GPM" +HEATER_ATTR = "HEATER" +HNAME_ATTR = "HNAME" +HTMODE_ATTR = "HTMODE" +LISTORD_ATTR = "LISTORD" +LOTMP_ATTR = "LOTMP" +LSTTMP_ATTR = "LSTTMP" +MODE_ATTR = "MODE" +NORMAL_ATTR = "NORMAL" +OBJTYP_ATTR = "OBJTYP" +ORPTNK_ATTR = "ORPTNK" +ORPVAL_ATTR = "ORPVAL" +PARENT_ATTR = "PARENT" +PHVAL_ATTR = "PHVAL" +PHTNK_ATTR = "PHTNK" +PROPNAME_ATTR = "PROPNAME" +PWR_ATTR = "PWR" +QUALTY_ATTR = "QUALTY" +READY_ATTR = "READY" +RPM_ATTR = "RPM" +SALT_ATTR = "SALT" +SELECT_ATTR = "SELECT" +SHOMNU_ATTR = "SHOMNU" +SNAME_ATTR = "SNAME" +SOURCE_ATTR = "SOURCE" +STATIC_ATTR = "STATIC" +STATUS_ATTR = "STATUS" +SUBTYP_ATTR = "SUBTYP" +TIME_ATTR = "TIME" +TIMOUT_ATTR = "TIMOUT" +USE_ATTR = "USE" +VACFLO_ATTR = "VACFLO" +VER_ATTR = "VER" +VOL_ATTR = "VOL" + USER_PRIVILEGES = { "p": "Pool Access", "P": "Pool temperature", @@ -30,155 +87,202 @@ "ACT2", # (int) ??? "ACT3", # (int) ??? "ACT4", # (int) ??? - "HEATER", # (objnam) + "FILTER", # (objnam) Circuit object that filter this body + HEATER_ATTR, # (objnam) "HITMP", # (int) maximum temperature to set - "HNAME", # equals to OBJNAM - "HTMODE", # (int) 1 if currently heating, 0 if not + HNAME_ATTR, # equals to OBJNAM + HTMODE_ATTR, # (int) >0 if currently heating, 0 if not "HTSRC", # (objnam) the heating source (or '00000') - "LISTORD", # (int) used to order in UI - "LOTMP", # (int) desired temperature - "LSTTMP", # (int) last recorded temperature + LISTORD_ATTR, # (int) used to order in UI + LOTMP_ATTR, # (int) desired temperature + LSTTMP_ATTR, # (int) last recorded temperature "MANHT", # Manual heating ??? "MANUAL", # (int) ??? - "PARENT", # (objnam) parent object + PARENT_ATTR, # (objnam) parent object "PRIM", # (int) ??? - "READY", # (ON/OFF) ??? + READY_ATTR, # (ON/OFF) ??? "SEC", # (int) ??? "SETPT", # (int) set point (same as 'LOTMP' AFAIK) "SHARE", # (objnam) sharing with that other body? - "SNAME", # (str) friendly name - "STATIC", # (ON/OFF) 'OFF' - "STATUS", # (ON/OFF) 'ON' is body is "active" - "SUBTYP", # 'POOL' or 'SPA' - "VOL", # (int) Volume in Gallons + SNAME_ATTR, # (str) friendly name + "SRCTYP", # ??? only seeing "GENERIC" + STATIC_ATTR, # (ON/OFF) 'OFF' + STATUS_ATTR, # (ON/OFF) 'ON' is body is "active" + SUBTYP_ATTR, # 'POOL' or 'SPA' + VOL_ATTR, # (int) Volume in Gallons } -CIRCGRP_ATTRIBUTES = {"ACT", "CIRCUIT", "DLY", "LISTORD", "PARENT", "READY", "STATIC"} + +CHEM_ATTRIBUTES = { + "ALK", # (int) IntelliChem: Alkalinity setting + BODY_ATTR, # (objnam) BODY being managed + "CALC", # (int) IntelliChem: Calcium Harness setting + "CHLOR", # (ON/OFF) IntelliChem: ?? + COMUART_ATTR, # (int) X25 related ? + "CYACID", # (int) IntelliChem: Cyanuric Acid setting + LISTORD_ATTR, # (int) used to order in UI + "ORPHI", # (ON/OFF) IntelliChem: ORP Level too high? + "ORPLO", # (ON/OFF) IntelliChem: ORP Level too low? + "ORPSET", # (int) IntelliChem ORP level setting + ORPTNK_ATTR, # (int) IntelliChem: ORP Tank Level + ORPVAL_ATTR, # (int) IntelliChem: ORP Level + "PHHI", # (ON/OFF) IntelliChem: Ph Level too low? + "PHLO", # (ON/OFF) IntelliChem: Ph Level too low? + "PHSET", # (float) IntelliChem Ph level setting + PHTNK_ATTR, # (int) IntelliChem: Ph Tank Level + PHVAL_ATTR, # (float) IntelliChem: Ph Level + "PRIM", # (int) Intellichor: output setting in % + QUALTY_ATTR, # (float) Intellichem: Water Quality (Saturation Index) + SALT_ATTR, # (int) Salt level + "SEC", # (int) IntelliChlor ?? + "SHARE", # (objnam) ?? + "SINDEX", # (int) ?? + SNAME_ATTR, # friendly name + SUBTYP_ATTR, # 'ICHLOR' for IntelliChlor, 'ICHEM' for IntelliChem + "SUPER", # (ON/OFF) IntelliChlor: turn on Boost mode (aka Super Chlorinate) + TIMOUT_ATTR, # (int) IntelliChlor: in seconds ?? +} +CIRCGRP_ATTRIBUTES = { + ACT_ATTR, + CIRCUIT_ATTR, + DLY_ATTR, + LISTORD_ATTR, + PARENT_ATTR, + READY_ATTR, + STATIC_ATTR, +} CIRCUIT_ATTRIBUTES = { - "ACT", # to be set for changing USE attribute - "BODY", + ACT_ATTR, # to be set for changing USE attribute + BODY_ATTR, "CHILD", "COVER", "DNTSTP", # (ON/OFF) "Don't Stop", disable egg timer - "FEATR", # (ON/OFF) Featured + FEATR_ATTR, # (ON/OFF) Featured "FREEZE", # (ON/OFF) Freeze Protection - "HNAME", # equals to OBJNAM + HNAME_ATTR, # equals to OBJNAM "LIMIT", - "LISTORD", # (int) used to order in UI + LISTORD_ATTR, # (int) used to order in UI "OBJLIST", - "PARENT", # OBJNAM of the parent object - "READY", # (ON/OFF) ?? - "SELECT", # ??? + PARENT_ATTR, # OBJNAM of the parent object + READY_ATTR, # (ON/OFF) ?? + SELECT_ATTR, # ??? "SET", # (ON/OFF) for light groups only - "SHOMNU", # (str) permissions - "SNAME", # (str) friendly name - "STATIC", # (ON/OFF) ?? - "STATUS", # (ON/OFF) 'ON' if circuit is active - "SUBTYP", # subtype can be '? + SHOMNU_ATTR, # (str) permissions + SNAME_ATTR, # (str) friendly name + STATIC_ATTR, # (ON/OFF) ?? + STATUS_ATTR, # (ON/OFF) 'ON' if circuit is active + SUBTYP_ATTR, # subtype can be '? "SWIM", # (ON/OFF) for light groups only "SYNC", # (ON/OFF) for light groups only - "TIME", # (int) Egg Timer, number of minutes + TIME_ATTR, # (int) Egg Timer, number of minutes "USAGE", - "USE", # for lights with light effects, indicate the 'color' + USE_ATTR, # for lights with light effects, indicate the 'color' } # represents External Equipment like covers EXTINSTR_ATRIBUTES = { - "BODY", # (objnam) which body it covers - "HNAME", # equals to OBJNAM - "LISTORD", # (int) used to order in UI - "NORMAL", # (ON/OFF) 'ON' for Cover State Normally On - "PARENT", # (objnam) - "READY", # (ON/OFF) ??? - "SNAME", # (str) friendly name - "STATIC", # (ON/OFF) 'OFF' - "STATUS", # (ON/OFF) 'ON' if cover enabled - "SUBTYP", # only seen 'COVER' + BODY_ATTR, # (objnam) which body it covers + HNAME_ATTR, # equals to OBJNAM + LISTORD_ATTR, # (int) used to order in UI + NORMAL_ATTR, # (ON/OFF) 'ON' for Cover State Normally On + PARENT_ATTR, # (objnam) + READY_ATTR, # (ON/OFF) ??? + SNAME_ATTR, # (str) friendly name + STATIC_ATTR, # (ON/OFF) 'OFF' + STATUS_ATTR, # (ON/OFF) 'ON' if cover enabled + SUBTYP_ATTR, # only seen 'COVER' } # no idea what this represents -FEATR_ATTRIBUTES = {"HNAME", "LISTORD", "READY", "SNAME", "SOURCE", "STATIC"} +FEATR_ATTRIBUTES = { + HNAME_ATTR, + LISTORD_ATTR, + READY_ATTR, + SNAME_ATTR, + SOURCE_ATTR, + STATIC_ATTR, +} HEATER_ATTRIBUTES = { - "BODY", # the objnam of the body the pump serves or a list (separated by a space) + BODY_ATTR, # the objnam of the body the pump serves or a list (separated by a space) "BOOST", # (int) ?? - "COMUART", # X25 related? + COMUART_ATTR, # X25 related? "COOL", # (ON/OFF) - "DLY", # (int) ?? - "HNAME", # equals to OBJNAM - "HTMODE", # (int) ?? - "LISTORD", # (int) used to order in UI - "PARENT", # (objnam) parent (module) for this heater - "READY", # (ON/OFF) - "SHOMNU", # (str) permissions - "SNAME", # (str) friendly name + DLY_ATTR, # (int) ?? + HNAME_ATTR, # equals to OBJNAM + HTMODE_ATTR, # (int) ?? + LISTORD_ATTR, # (int) used to order in UI + PARENT_ATTR, # (objnam) parent (module) for this heater + READY_ATTR, # (ON/OFF) + SHOMNU_ATTR, # (str) permissions + SNAME_ATTR, # (str) friendly name "START", # (int) ?? - "STATIC", # (ON/OFF) 'OFF' - "STATUS", # (ON/OFF) only seen 'ON' + STATIC_ATTR, # (ON/OFF) 'OFF' + STATUS_ATTR, # (ON/OFF) only seen 'ON' "STOP", # (int) ?? - "SUBTYP", # type of heater 'GENERIC','SOLAR','ULTRA','HEATER' - "TIME", # (int) ?? + SUBTYP_ATTR, # type of heater 'GENERIC','SOLAR','ULTRA','HEATER' + TIME_ATTR, # (int) ?? } MODULE_ATTRUBUTES = { "CIRCUITS", # [ objects ] the objects that the module controls - "PARENT", # (objnam) the parent (PANEL) of the module + PARENT_ATTR, # (objnam) the parent (PANEL) of the module "PORT", # (int) no idea - "SNAME", # friendly name - "STATIC", # (ON/OFF) 'ON' - "SUBTYP", # type of the module (like 'I5P' or 'I8PS') - "VER", # (str) the version of the firmware for this module + SNAME_ATTR, # friendly name + STATIC_ATTR, # (ON/OFF) 'ON' + SUBTYP_ATTR, # type of the module (like 'I5P' or 'I8PS') + VER_ATTR, # (str) the version of the firmware for this module } PANEL_ATTRIBUTES = { - "HNAME", # equals to OBJNAM - "LISTORD", # (int) used to order in UI + HNAME_ATTR, # equals to OBJNAM + LISTORD_ATTR, # (int) used to order in UI "OBJLIST", # [ (objnam) ] the elements managed by the panel "PANID", # ??? only seen 'SHARE' - "SNAME", # friendly name - "STATIC", # only seen 'ON' - "SUBTYP", # only seen 'OCP' + SNAME_ATTR, # friendly name + STATIC_ATTR, # only seen 'ON' + SUBTYP_ATTR, # only seen 'OCP' } # represent a USER for the system PERMIT_ATTRIBUTES = { - "ENABLE", # (ON/OFF) ON if user is enabled + ENABLE_ATTR, # (ON/OFF) ON if user is enabled "PASSWRD", # 4 digit code or '' - "SHOMNU", # privileges associated with this user - "SNAME", # friendly name - "STATIC", # (ON/OFF) only seen ON - "SUBTYP", # ADV for administrator, BASIC for guest - "TIMOUT", # (int) in minutes, timeout for user session + SHOMNU_ATTR, # privileges associated with this user + SNAME_ATTR, # friendly name + STATIC_ATTR, # (ON/OFF) only seen ON + SUBTYP_ATTR, # ADV for administrator, BASIC for guest + TIMOUT_ATTR, # (int) in minutes, timeout for user session } # represents a PUMP setting PMPCIRC_ATTRIBUTES = { - "BODY", # not sure, I've only see '00000' - "CIRCUIT", # (objnam) the circuit this setting is for - "GPM", # (int): the flow setting for the pump if select is GPM - "LISTORD", # (int) used to order in UI - "PARENT", # (objnam) the pump the setting belongs to + BODY_ATTR, # not sure, I've only see '00000' + CIRCUIT_ATTR, # (objnam) the circuit this setting is for + GPM_ATTR, # (int): the flow setting for the pump if select is GPM + LISTORD_ATTR, # (int) used to order in UI + PARENT_ATTR, # (objnam) the pump the setting belongs to "SPEED", # (int): the speed setting for the pump if select is RPM - "SELECT", # 'RPM' or 'GPM' + SELECT_ATTR, # 'RPM' or 'GPM' } # no idea what this object type represents # only seem to be one instance of it PRESS_ATTRIBUTES = { - "SHOMNU", # (ON/OFF) ??? - "SNAME", # seems equal to objnam - "STATIC", # (ON/OFF) only seen ON + SHOMNU_ATTR, # (ON/OFF) ??? + SNAME_ATTR, # seems equal to objnam + STATIC_ATTR, # (ON/OFF) only seen ON } # represents a PUMP PUMP_ATTRIBUTES = { - "BODY", # the objnam of the body the pump serves or a list (separated by a space) - "CIRCUIT", # (int) ??? only seen 1 - "COMUART", # X25 related? - "HNAME", # same as objnam - "GPM", # (int) when applicable, real time Gallon Per Minute - "LISTORD", # (int) used to order in UI + BODY_ATTR, # the objnam of the body the pump serves or a list (separated by a space) + CIRCUIT_ATTR, # (int) ??? only seen 1 + COMUART_ATTR, # X25 related? + HNAME_ATTR, # same as objnam + GPM_ATTR, # (int) when applicable, real time Gallon Per Minute + LISTORD_ATTR, # (int) used to order in UI "MAX", # (int) maximum RPM "MAXF", # (int) maximum GPM (if applicable, 0 otherwise) "MIN", # (int) minimum RPM @@ -188,80 +292,80 @@ "PRIMFLO", # (int) Priming Speed "PRIMTIM", # (int) Priming Time in minutes "PRIOR", # (int) ??? - "PWR", # (int) when applicable, real time Power usage in Watts - "RPM", # (int) when applicable, real time Rotation Per Minute + PWR_ATTR, # (int) when applicable, real time Power usage in Watts + RPM_ATTR, # (int) when applicable, real time Rotation Per Minute "SETTMP", # (int) Step size for RPM "SETTMPNC", # (int) ??? - "SNAME", # friendly name - "STATUS", # only seen 10 for on, 4 for off - "SUBTYP", # type of pump: 'SPEED' (variable speed), 'FLOW' (variable flow), 'VSF' (both) + SNAME_ATTR, # friendly name + STATUS_ATTR, # only seen 10 for on, 4 for off + SUBTYP_ATTR, # type of pump: 'SPEED' (variable speed), 'FLOW' (variable flow), 'VSF' (both) "SYSTIM", # (int) ??? } # represents a mapping between a remote button and a circuit REMBTN_ATTRIBUTES = { - "CIRCUIT", # (objnam) the circuit triggered by the button - "LISTORD", # (int) which button on the remote (1 to 4) - "PARENT" # (objnam) the remote this button is associated with - "STATIC", # (ON/OFF) not sure, only seen 'ON' + CIRCUIT_ATTR, # (objnam) the circuit triggered by the button + LISTORD_ATTR, # (int) which button on the remote (1 to 4) + PARENT_ATTR, # (objnam) the remote this button is associated with + STATIC_ATTR, # (ON/OFF) not sure, only seen 'ON' } # represents a REMOTE REMOTE_ATTRIBUTES = { - "BODY", # (objnam) the body the remote controls - "COMUART", # X25 address? - "ENABLE", # (ON/OFF) 'ON' if the remote is set to active - "HNAME", # same as objnam - "LISTORD", # number likely used to order things in UI - "SNAME", # friendly name - "STATIC", # (ON/OFF) not sure, only seen 'OFF' - "SUBTYP", # type of the remote, I've only seen IS4 + BODY_ATTR, # (objnam) the body the remote controls + COMUART_ATTR, # X25 address? + ENABLE_ATTR, # (ON/OFF) 'ON' if the remote is set to active + HNAME_ATTR, # same as objnam + LISTORD_ATTR, # number likely used to order things in UI + SNAME_ATTR, # friendly name + STATIC_ATTR, # (ON/OFF) not sure, only seen 'OFF' + SUBTYP_ATTR, # type of the remote, I've only seen IS4 } # represents a SCHEDULE SCHED_ATTRIBUTES = { - "ACT", - "CIRCUIT", # (objnam) the circuit controlled by this schedule + ACT_ATTR, # (ON/OFF) ON is schedule is currently active + CIRCUIT_ATTR, # (objnam) the circuit controlled by this schedule "DAY", # the days this schedule run (example: 'MTWRFAU' for every day, 'AU' for weekends) "DNTSTP", # 'ON' or 'OFF" means Don't Stop. Set to ON to never end... - "HEATER", # set to a HEATER objnam is the schedule should trigger heating, '00000' for off, '00001' for Don't Change - "HNAME", # same as objnam + HEATER_ATTR, # set to a HEATER objnam is the schedule should trigger heating, '00000' for off, '00001' for Don't Change + HNAME_ATTR, # same as objnam "HITMP", # number but not sure - "LISTORD", # number likely used to order things in UI - "LOTMP", # number. when heater is set, that is the desired temperature + LISTORD_ATTR, # number likely used to order things in UI + LOTMP_ATTR, # number. when heater is set, that is the desired temperature "SINGLE", # 'ON' if the schedule is not to repeat - "SNAME", # the friendly name of the schedule + SNAME_ATTR, # the friendly name of the schedule "START", # start time mode # 'ABSTIM' means absolute and 'TIME' will be the startime # 'SRIS' means Sunrise, 'SSET' means Sunset - "STATIC", # (ON/OFF) not sure, only seen 'OFF' - "STATUS", # 'ON' if schedule is active, 'OFF' otherwise + STATIC_ATTR, # (ON/OFF) not sure, only seen 'OFF' + STATUS_ATTR, # 'ON' if schedule is active, 'OFF' otherwise "STOP", # stop time mode ('ABSTIME','SRIS' or 'SSET') - "TIME", # time the schedule starts in 'HH,MM,SS' format (24h clock) - "TIMOUT", # time the schedule stops in 'HH,MM,SS' format (24h clock) - "VACFLO", # ON/OFF not sure of the meaning... + TIME_ATTR, # time the schedule starts in 'HH,MM,SS' format (24h clock) + TIMOUT_ATTR, # time the schedule stops in 'HH,MM,SS' format (24h clock) + VACFLO_ATTR, # (ON/OFF) 'ON' if schedule only applies to Vacation Mode } # represents a SENSOR SENSE_ATTRIBUTES = { "CALIB", # (int) calibration value - "HNAME", # same as objnam - "LISTORD", # number likely used to order things in UI - "MODE", # I've only seen 'OFF' so far + HNAME_ATTR, # same as objnam + LISTORD_ATTR, # number likely used to order things in UI + MODE_ATTR, # I've only seen 'OFF' so far "NAME", # I've only seen '00000' - "PARENT", # the parent's objnam + PARENT_ATTR, # the parent's objnam "PROBE", # the uncalibrated reading of the sensor - "SNAME", # friendly name - "SOURCE", # the calibrated reading of the sensor - "STATIC", # (ON/OFF) not sure, only seen 'ON' - "STATUS", # I've only seen 'OK' so far - "SUBTYP", # 'SOLAR','POOL' (for water), 'AIR' + SNAME_ATTR, # friendly name + SOURCE_ATTR, # the calibrated reading of the sensor + STATIC_ATTR, # (ON/OFF) not sure, only seen 'ON' + STATUS_ATTR, # I've only seen 'OK' so far + SUBTYP_ATTR, # 'SOLAR','POOL' (for water), 'AIR' } # represent the (unique instance of) SYSTEM object SYSTEM_ATTRIBUTES = [ - "ACT", # ON/OFF but not sure what it does + ACT_ATTR, # ON/OFF but not sure what it does "ADDRESS", # Pool Address "AVAIL", # ON/OFF but not sure what it does "CITY", # Pool City @@ -269,28 +373,28 @@ "EMAIL", # primary email for the owner "EMAIL2", # secondary email for the owner "HEATING", # ON/OFF: Pump On During Heater Cool-Down Delay - "HNAME", # same as objnam + HNAME_ATTR, # same as objnam "LOCX", # (float) longitude "LOCY", # (float) latitude "MANHT", # ON/OFF: Manual Heat - "MODE", # unit system, 'METRIC' or 'ENGLISH' + MODE_ATTR, # unit system, 'METRIC' or 'ENGLISH' "NAME", # name of the owner "PASSWRD", # a 4 digit password or '' "PHONE", # primary phone number for the owner "PHONE2", # secondary phone number for the owner - "PROPNAME", # name of the property + PROPNAME_ATTR, # name of the property "SERVICE", # 'AUTO' for automatic - "SNAME", # a crazy looking string I assume to be unique to this system + SNAME_ATTR, # a crazy looking string I assume to be unique to this system "START", # almost looks like a date but no idea "STATE", # Pool State - "STATUS", # ON/OFF + STATUS_ATTR, # ON/OFF "STOP", # same value as START "TEMPNC", # ON/OFF "TIMZON", # (int) Time Zone (example '-8' for US Pacific) - "VACFLO", # ON/OFF, vacation mode + VACFLO_ATTR, # ON/OFF, vacation mode "VACTIM", # ON/OFF "VALVE", # ON/OFF: Pump Off During Valve Action - "VER", # (str) software version + VER_ATTR, # (str) software version "ZIP", # Pool Zip Code ] @@ -302,45 +406,46 @@ "CLK24A", # clock mode, 'AMPM' or 'HR24' "DAY", # in 'MM,DD,YY' format "DLSTIM", # ON/OFF, ON for following DST - "HNAME", # same as objnam + HNAME_ATTR, # same as objnam "LOCX", # (float) longitude "LOCY", # (float) latitude "MIN", # in 'HH,MM,SS' format (24h clock) - "SNAME", # unused really, likely equals to OBJNAM - "SOURCE", # set to URL if time is from the internet - "STATIC", # (ON/OFF) not sure, only seen 'ON' + SNAME_ATTR, # unused really, likely equals to OBJNAM + SOURCE_ATTR, # set to URL if time is from the internet + STATIC_ATTR, # (ON/OFF) not sure, only seen 'ON' "TIMZON", # (int) timezone (example '-8' for US Pacific) "ZIP", # ZipCode } VALVE_ATTRIBUTES = { "ASSIGN", # 'NONE', 'INTAKE' or 'RETURN' - "CIRCUIT", # I've only seen '00000' - "DLY", # (ON/OFF) - "HNAME", # same as objnam - "PARENT", # (objnam) parent (a module) - "SNAME", # friendly name - "STATIC", # (ON/OFF) I've only seen 'OFF' - "SUBTYP", # I've only seen 'LEGACY' + CIRCUIT_ATTR, # I've only seen '00000' + DLY_ATTR, # (ON/OFF) + HNAME_ATTR, # same as objnam + PARENT_ATTR, # (objnam) parent (a module) + SNAME_ATTR, # friendly name + STATIC_ATTR, # (ON/OFF) I've only seen 'OFF' + SUBTYP_ATTR, # I've only seen 'LEGACY' } ALL_ATTRIBUTES_BY_TYPE = { - "BODY": BODY_ATTRIBUTES, - "CIRCGRP": CIRCGRP_ATTRIBUTES, - "CIRCUIT": CIRCUIT_ATTRIBUTES, + BODY_TYPE: BODY_ATTRIBUTES, + CHEM_TYPE: CHEM_ATTRIBUTES, + CIRCGRP_TYPE: CIRCGRP_ATTRIBUTES, + CIRCUIT_TYPE: CIRCUIT_ATTRIBUTES, "EXTINSTR": EXTINSTR_ATRIBUTES, "FEATR": FEATR_ATTRIBUTES, - "HEATER": HEATER_ATTRIBUTES, + HEATER_TYPE: HEATER_ATTRIBUTES, "MODULE": MODULE_ATTRUBUTES, "PANEL": PANEL_ATTRIBUTES, - "PMPCIRC": PMPCIRC_ATTRIBUTES, + PMPCIRC_TYPE: PMPCIRC_ATTRIBUTES, "PRESS": PRESS_ATTRIBUTES, - "PUMP": PUMP_ATTRIBUTES, - "REMBTN": REMBTN_ATTRIBUTES, - "REMOTE": REMOTE_ATTRIBUTES, - "SCHED": SCHED_ATTRIBUTES, - "SENSE": SENSE_ATTRIBUTES, - "SYSTEM": SYSTEM_ATTRIBUTES, + PUMP_TYPE: PUMP_ATTRIBUTES, + REMBTN_TYPE: REMBTN_ATTRIBUTES, + REMOTE_TYPE: REMOTE_ATTRIBUTES, + SCHED_TYPE: SCHED_ATTRIBUTES, + SENSE_TYPE: SENSE_ATTRIBUTES, + SYSTEM_TYPE: SYSTEM_ATTRIBUTES, "SYSTIM": SYSTIM_ATTRIBUTES, "VALVE": VALVE_ATTRIBUTES, } diff --git a/custom_components/intellicenter/pyintellicenter/controller.py b/custom_components/intellicenter/pyintellicenter/controller.py index 67e8c76..fb76800 100644 --- a/custom_components/intellicenter/pyintellicenter/controller.py +++ b/custom_components/intellicenter/pyintellicenter/controller.py @@ -7,6 +7,16 @@ import traceback from typing import Dict, Optional +from .attributes import ( + MODE_ATTR, + OBJTYP_ATTR, + PARENT_ATTR, + PROPNAME_ATTR, + SNAME_ATTR, + SUBTYP_ATTR, + SYSTEM_TYPE, + VER_ATTR, +) from .model import PoolModel from .protocol import ICProtocol @@ -33,16 +43,18 @@ def errorCode(self): class SystemInfo: """Represents minimal information about a Pentair system.""" + ATTRIBUTES_LIST = [PROPNAME_ATTR, VER_ATTR, MODE_ATTR, SNAME_ATTR] + def __init__(self, objnam: str, params: dict): """Initialize from a dictionary.""" self._objnam = objnam - self._propName = params["PROPNAME"] - self._sw_version = params["VER"] - self._mode = params["MODE"] + self._propName = params[PROPNAME_ATTR] + self._sw_version = params[VER_ATTR] + self._mode = params[MODE_ATTR] # here we compute what is expected to be a unique_id # from the internal name of the system object h = blake2b(digest_size=8) - h.update(params["SNAME"].encode()) + h.update(params[SNAME_ATTR].encode()) self._unique_id = h.hexdigest() @property @@ -68,9 +80,9 @@ def uniqueID(self): def update(self, updates): """Update the object from a set of key/value pairs.""" _LOGGER.debug(f"updating system info with {updates}") - self._propName = updates.get("PROPNAME", self._propName) - self._sw_version = updates.get("VER", self._sw_version) - self._mode = updates.get("MODE", self._mode) + self._propName = updates.get(PROPNAME_ATTR, self._propName) + self._sw_version = updates.get(VER_ATTR, self._sw_version) + self._mode = updates.get(MODE_ATTR, self._mode) # ------------------------------------------------------------------------------------- @@ -133,9 +145,12 @@ async def start(self) -> None: msg = await self.sendCmd( "GetParamList", { - "condition": "OBJTYP=SYSTEM", + "condition": f"{OBJTYP_ATTR}={SYSTEM_TYPE}", "objectList": [ - {"objnam": "INCR", "keys": ["SNAME", "VER", "PROPNAME", "MODE"]} + { + "objnam": "INCR", + "keys": SystemInfo.ATTRIBUTES_LIST, + } ], }, ) @@ -290,7 +305,9 @@ async def start(self): await super().start() # now we retrieve all the objects type, subtype, sname and parent - allObjects = await self.getAllObjects(["OBJTYP", "SUBTYP", "SNAME", "PARENT"]) + allObjects = await self.getAllObjects( + [OBJTYP_ATTR, SUBTYP_ATTR, SNAME_ATTR, PARENT_ATTR] + ) # and process that list into our model self.model.addObjects(allObjects) diff --git a/custom_components/intellicenter/pyintellicenter/model.py b/custom_components/intellicenter/pyintellicenter/model.py index b6913c8..950d49b 100644 --- a/custom_components/intellicenter/pyintellicenter/model.py +++ b/custom_components/intellicenter/pyintellicenter/model.py @@ -3,7 +3,16 @@ import logging from typing import List -from .attributes import ALL_ATTRIBUTES_BY_TYPE +from .attributes import ( + ALL_ATTRIBUTES_BY_TYPE, + CIRCUIT_TYPE, + FEATR_ATTR, + OBJTYP_ATTR, + PARENT_ATTR, + SNAME_ATTR, + STATUS_ATTR, + SUBTYP_ATTR, +) _LOGGER = logging.getLogger(__name__) @@ -16,8 +25,8 @@ class PoolObject: def __init__(self, objnam, params): """Initialize.""" self._objnam = objnam - self._objtyp = params.pop("OBJTYP") - self._subtyp = params.pop("SUBTYP", None) + self._objtyp = params.pop(OBJTYP_ATTR) + self._subtyp = params.pop(SUBTYP_ATTR, None) self._properties = params @property @@ -28,7 +37,7 @@ def objnam(self): @property def sname(self): """Return the friendly name (SNAME).""" - return self._properties.get("SNAME") + return self._properties.get(SNAME_ATTR) @property def objtype(self): @@ -43,7 +52,7 @@ def subtype(self): @property def status(self) -> str: """Return the object status.""" - return self._properties.get("STATUS") + return self._properties.get(STATUS_ATTR) @property def offStatus(self) -> str: @@ -58,7 +67,7 @@ def onStatus(self) -> str: @property def isALight(self) -> bool: """Return True is the object is a light.""" - return self.objtype == "CIRCUIT" and self.subtype in [ + return self.objtype == CIRCUIT_TYPE and self.subtype in [ "LIGHT", "INTELLI", "GLOW", @@ -75,12 +84,12 @@ def supportColorEffects(self) -> bool: @property def isALightShow(self) -> bool: """Return True is the object is a light show.""" - return self.objtype == "CIRCUIT" and self.subtype == "LITSHO" + return self.objtype == CIRCUIT_TYPE and self.subtype == "LITSHO" @property def isFeatured(self) -> bool: """Return True is the object is Featured.""" - return self["FEATR"] == "ON" + return self[FEATR_ATTR] == "ON" def __getitem__(self, key): """Return the value for attribure 'key'.""" @@ -119,9 +128,9 @@ def update(self, updates): continue # there are a few case when we receive the type/subtype in an update - if key == "OBJTYP": + if key == OBJTYP_ATTR: self._objtyp = value - elif key == "SUBTYP": + elif key == SUBTYP_ATTR: self._subtyp = value else: self._properties[key] = value @@ -182,7 +191,7 @@ def getByType(self, type: str, subtype: str = None) -> List[PoolObject]: def getChildren(self, object: PoolObject) -> List[PoolObject]: """Return the children of a given object.""" - return list(filter(lambda v: v["PARENT"] == object.objnam, self)) + return list(filter(lambda v: v[PARENT_ATTR] == object.objnam, self)) def addObject(self, objnam, params): """Update the model with a new object.""" diff --git a/custom_components/intellicenter/sensor.py b/custom_components/intellicenter/sensor.py index 5a57d96..7faa06a 100644 --- a/custom_components/intellicenter/sensor.py +++ b/custom_components/intellicenter/sensor.py @@ -13,10 +13,31 @@ from . import PoolEntity from .const import CONST_GPM, CONST_RPM, DOMAIN -from .pyintellicenter import ModelController, PoolObject +from .pyintellicenter import ( + BODY_TYPE, + CHEM_TYPE, + GPM_ATTR, + LOTMP_ATTR, + LSTTMP_ATTR, + ORPTNK_ATTR, + ORPVAL_ATTR, + PHTNK_ATTR, + PHVAL_ATTR, + PUMP_TYPE, + PWR_ATTR, + QUALTY_ATTR, + RPM_ATTR, + SALT_ATTR, + SENSE_TYPE, + SOURCE_ATTR, + ModelController, + PoolObject, +) _LOGGER = logging.getLogger(__name__) +# ------------------------------------------------------------------------------------- + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -29,18 +50,18 @@ async def async_setup_entry( object: PoolObject for object in controller.model.objectList: - if object.objtype == "SENSE": + if object.objtype == SENSE_TYPE: sensors.append( PoolSensor( entry, controller, object, device_class=DEVICE_CLASS_TEMPERATURE, - attribute_key="SOURCE", + attribute_key=SOURCE_ATTR, ) ) - elif object.objtype == "PUMP": - if object["PWR"]: + elif object.objtype == PUMP_TYPE: + if object[PWR_ATTR]: sensors.append( PoolSensor( entry, @@ -48,12 +69,12 @@ async def async_setup_entry( object, device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=POWER_WATT, - attribute_key="PWR", - name_suffix="power", + attribute_key=PWR_ATTR, + name="+ power", rounding_factor=25, ) ) - if object["RPM"]: + if object[RPM_ATTR]: sensors.append( PoolSensor( entry, @@ -61,11 +82,11 @@ async def async_setup_entry( object, device_class=None, unit_of_measurement=CONST_RPM, - attribute_key="RPM", - name_suffix="rpm", + attribute_key=RPM_ATTR, + name="+ rpm", ) ) - if object["GPM"]: + if object[GPM_ATTR]: sensors.append( PoolSensor( entry, @@ -73,19 +94,19 @@ async def async_setup_entry( object, device_class=None, unit_of_measurement=CONST_GPM, - attribute_key="GPM", - name_suffix="gpm", + attribute_key=GPM_ATTR, + name="+ gpm", ) ) - elif object.objtype == "BODY": + elif object.objtype == BODY_TYPE: sensors.append( PoolSensor( entry, controller, object, device_class=DEVICE_CLASS_TEMPERATURE, - attribute_key="LSTTMP", - name_suffix="last temp", + attribute_key=LSTTMP_ATTR, + name="+ last temp", ) ) sensors.append( @@ -94,13 +115,73 @@ async def async_setup_entry( controller, object, device_class=DEVICE_CLASS_TEMPERATURE, - attribute_key="LOTMP", - name_suffix="desired temp", + attribute_key=LOTMP_ATTR, + name="+ desired temp", ) ) + elif object.objtype == CHEM_TYPE: + if object.subtype == "ICHLOR": + sensors.append( + PoolSensor( + entry, + controller, + object, + attribute_key=PHVAL_ATTR, + name="+ (pH)", + ) + ) + sensors.append( + PoolSensor( + entry, + controller, + object, + attribute_key=ORPVAL_ATTR, + name="+ (ORP)", + ) + ) + sensors.append( + PoolSensor( + entry, + controller, + object, + attribute_key=SALT_ATTR, + name="+ (Salt)", + ) + ) + sensors.append( + PoolSensor( + entry, + controller, + object, + attribute_key=QUALTY_ATTR, + name="+ (Water Quality)", + ) + ) + sensors.append( + PoolSensor( + entry, + controller, + object, + attribute_key=PHTNK_ATTR, + name_="+ (Ph Tank Level)", + ) + ) + # should this be a factor only if there is not IntelliChlor? + sensors.append( + PoolSensor( + entry, + controller, + object, + attribute_key=ORPTNK_ATTR, + name="+ (ORP Tank Level)", + ) + ) async_add_entities(sensors) +# ------------------------------------------------------------------------------------- + + class PoolSensor(PoolEntity): """Representation of an Pentair sensor.""" diff --git a/custom_components/intellicenter/switch.py b/custom_components/intellicenter/switch.py index 650bb01..da4c74c 100644 --- a/custom_components/intellicenter/switch.py +++ b/custom_components/intellicenter/switch.py @@ -9,10 +9,22 @@ from . import PoolEntity from .const import DOMAIN -from .pyintellicenter import ModelController, PoolObject +from .pyintellicenter import ( + BODY_TYPE, + CIRCUIT_TYPE, + HEATER_ATTR, + HTMODE_ATTR, + SYSTEM_TYPE, + VACFLO_ATTR, + VOL_ATTR, + ModelController, + PoolObject, +) _LOGGER = logging.getLogger(__name__) +# ------------------------------------------------------------------------------------- + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -24,18 +36,32 @@ async def async_setup_entry( object: PoolObject for object in controller.model.objectList: - if object.objtype == "BODY": + if object.objtype == BODY_TYPE: switches.append(PoolBody(entry, controller, object)) elif ( - object.objtype == "CIRCUIT" + object.objtype == CIRCUIT_TYPE and not (object.isALight or object.isALightShow) and object.isFeatured ): switches.append(PoolCircuit(entry, controller, object)) + elif object.objtype == SYSTEM_TYPE: + switches.append( + PoolCircuit( + entry, + controller, + object, + VACFLO_ATTR, + name="Vacation mode", + enabled_by_default=False, + ) + ) async_add_entities(switches) +# ------------------------------------------------------------------------------------- + + class PoolCircuit(PoolEntity, SwitchEntity): """Representation of an standard pool circuit.""" @@ -53,13 +79,16 @@ def turn_on(self, **kwargs: Any) -> None: self.requestChanges({self._attribute_key: self._poolObject.onStatus}) +# ------------------------------------------------------------------------------------- + + class PoolBody(PoolCircuit): """Representation of a body of water.""" def __init__(self, entry: ConfigEntry, controller, poolObject): """Initialize a Pool body from the underlying circuit.""" super().__init__(entry, controller, poolObject) - self._extraStateAttributes = ["VOL", "HEATER", "HTMODE"] + self._extraStateAttributes = [VOL_ATTR, HEATER_ATTR, HTMODE_ATTR] @property def icon(self): diff --git a/custom_components/intellicenter/water_heater.py b/custom_components/intellicenter/water_heater.py index 414ce6a..246ca98 100644 --- a/custom_components/intellicenter/water_heater.py +++ b/custom_components/intellicenter/water_heater.py @@ -1,7 +1,7 @@ """Pentair Intellicenter water heaters.""" import logging -from typing import Dict +from typing import Any, Dict, Optional from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, @@ -10,13 +10,25 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, STATE_IDLE, STATE_OFF, STATE_ON -from homeassistant.core import callback +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import HomeAssistantType from . import PoolEntity from .const import DOMAIN -from .pyintellicenter import ModelController, PoolObject +from .pyintellicenter import ( + BODY_ATTR, + HEATER_ATTR, + HEATER_TYPE, + HTMODE_ATTR, + LOTMP_ATTR, + LSTTMP_ATTR, + NULL_OBJNAM, + STATUS_ATTR, + ModelController, + PoolObject, +) +# from homeassistant.components.climate.const import CURRENT_HVAC_OFF, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE _LOGGER = logging.getLogger(__name__) @@ -31,12 +43,12 @@ async def async_setup_entry( # body of water # first find all heaters - heaters = [object for object in controller.model if object.objtype == "HEATER"] + heaters = [object for object in controller.model if object.objtype == HEATER_TYPE] # then for each heater, find which bodies it handles body_to_heater_map = {} for heater in heaters: - bodies = heater["BODY"].split(" ") + bodies = heater[BODY_ATTR].split(" ") for body_id in bodies: body_to_heater_map[body_id] = heater.objnam @@ -50,9 +62,14 @@ async def async_setup_entry( async_add_entities(water_heaters) -class PoolWaterHeater(PoolEntity, WaterHeaterEntity): +# ------------------------------------------------------------------------------------- + + +class PoolWaterHeater(PoolEntity, WaterHeaterEntity, RestoreEntity): """Representation of a Pentair water heater.""" + LAST_HEATER_ATTR = "LAST_HEATER" + def __init__( self, entry: ConfigEntry, @@ -61,19 +78,41 @@ def __init__( heater_id, ): """Initialize.""" - super().__init__(entry, controller, poolObject) + super().__init__( + entry, + controller, + poolObject, + extraStateAttributes=[HEATER_ATTR, HTMODE_ATTR], + ) self._heater_id = heater_id + self._lastHeater = self._poolObject[HEATER_ATTR] + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + + state_attributes = super().device_state_attributes + + if self._lastHeater != NULL_OBJNAM: + state_attributes[self.LAST_HEATER_ATTR] = self._lastHeater + + return state_attributes @property - def name(self): - """Return the name of the entity.""" - name = super().name - return name + " heater" + def state(self) -> str: + """Return the current state.""" + status = self._poolObject[STATUS_ATTR] + heater = self._poolObject[HEATER_ATTR] + htmode = self._poolObject[HTMODE_ATTR] + if status == "OFF" or heater == NULL_OBJNAM: + return STATE_OFF + if heater == self._heater_id: + return STATE_ON if htmode != "0" else STATE_IDLE @property def unique_id(self): """Return a unique ID.""" - return super().unique_id + "LOTMP" + return super().unique_id + LOTMP_ATTR @property def supported_features(self): @@ -103,42 +142,73 @@ def max_temp(self): @property def current_temperature(self): """Return the current temperature.""" - return float(self._poolObject["LSTTMP"]) + return float(self._poolObject[LSTTMP_ATTR]) @property def target_temperature(self): """Return the temperature we try to reach.""" - return float(self._poolObject["LOTMP"]) + return float(self._poolObject[LOTMP_ATTR]) def set_temperature(self, **kwargs): """Set new target temperatures.""" target_temperature = kwargs.get(ATTR_TEMPERATURE) - self.requestChanges({"LOTMP": str(int(target_temperature))}) + self.requestChanges({LOTMP_ATTR: str(int(target_temperature))}) @property def current_operation(self): """Return current operation.""" - heater = self._poolObject["HEATER"] - htmode = self._poolObject["HTMODE"] + heater = self._poolObject[HEATER_ATTR] if heater == self._heater_id: - return "heating" if htmode == "1" else STATE_IDLE + return self._controller.model[self._heater_id].sname return STATE_OFF @property def operation_list(self): """Return the list of available operation modes.""" - return [STATE_ON, STATE_OFF] + return [STATE_OFF, self._controller.model[self._heater_id].sname] def set_operation_mode(self, operation_mode): """Set new target operation mode.""" - value = self._heater_id if operation_mode == STATE_ON else "00000" - self.requestChanges({"HEATER": value}) + if operation_mode == STATE_OFF: + self._turnOff() + elif operation_mode == self._controller.model[self._heater_id].sname: + self.requestChanges({HEATER_ATTR: self._heater_id}) + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + if self._lastHeater: + self.requestChanges({HEATER_ATTR: self._lastHeater}) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + self._turnOff() + + def _turnOff(self): + self._lastHeater = self._poolObject[HEATER_ATTR] + self.requestChanges({HEATER_ATTR: NULL_OBJNAM}) + + def isUpdated(self, updates: Dict[str, Dict[str, str]]) -> bool: + """Return true if the entity is updated by the updates from Intellicenter.""" + + myUpdates = updates.get(self._poolObject.objnam, {}) + + return ( + myUpdates + and {STATUS_ATTR, HEATER_ATTR, HTMODE_ATTR, LOTMP_ATTR, LSTTMP_ATTR} + & myUpdates.keys() + ) + + async def async_added_to_hass(self): + """Entity is added to Home Assistant.""" + + await super().async_added_to_hass() - @callback - def _update_callback(self, updates: Dict[str, PoolObject]): - """Update the entity if its underlying pool object has changed.""" + if self._lastHeater == NULL_OBJNAM: + # our current state is OFF so + # let's see if we find a previous value stored in out state + last_state = await self.async_get_last_state() - if self._poolObject.objnam in updates: - self._available = True - _LOGGER.debug(f"updating {self} from {self._poolObject}") - self.async_write_ha_state() + if last_state: + value = last_state.attributes.get(self.LAST_HEATER_ATTR) + if value: + self._lastHeater = value