diff --git a/.coveragerc b/.coveragerc index 6ad158066df..d106ca332f0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -704,6 +704,7 @@ omit = homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/common.py homeassistant/components/omnilogic/sensor.py + homeassistant/components/omnilogic/switch.py homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/api.py homeassistant/components/ondilo_ico/const.py diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index f50efb7eafb..8d2071dee7c 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -10,11 +10,17 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from .common import OmniLogicUpdateCoordinator -from .const import CONF_SCAN_INTERVAL, COORDINATOR, DOMAIN, OMNI_API +from .const import ( + CONF_SCAN_INTERVAL, + COORDINATOR, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + OMNI_API, +) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = ["sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -24,9 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] - polling_interval = 6 - if CONF_SCAN_INTERVAL in conf: - polling_interval = conf[CONF_SCAN_INTERVAL] + polling_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) session = aiohttp_client.async_get_clientsession(hass) @@ -46,6 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass=hass, api=api, name="Omnilogic", + config_entry=entry, polling_interval=polling_interval, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 6f7ee6e5eb5..645c24ac676 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -3,8 +3,9 @@ from datetime import timedelta import logging -from omnilogic import OmniLogicException +from omnilogic import OmniLogic, OmniLogicException +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( @@ -30,12 +31,14 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, - api: str, + api: OmniLogic, name: str, + config_entry: ConfigEntry, polling_interval: int, ): """Initialize the global Omnilogic data updater.""" self.api = api + self.config_entry = config_entry super().__init__( hass=hass, @@ -103,9 +106,13 @@ class OmniLogicEntity(CoordinatorEntity): if bow_id is not None: unique_id = f"{unique_id}_{coordinator.data[bow_id]['systemId']}" - entity_friendly_name = ( - f"{entity_friendly_name}{coordinator.data[bow_id]['Name']} " - ) + + if kind != "Heaters": + entity_friendly_name = ( + f"{entity_friendly_name}{coordinator.data[bow_id]['Name']} " + ) + else: + entity_friendly_name = f"{entity_friendly_name}{coordinator.data[bow_id]['Operation']['VirtualHeater']['Name']} " unique_id = f"{unique_id}_{coordinator.data[item_id]['systemId']}_{kind}" @@ -155,3 +162,17 @@ class OmniLogicEntity(CoordinatorEntity): ATTR_MANUFACTURER: "Hayward", ATTR_MODEL: "OmniLogic", } + + +def check_guard(state_key, item, entity_setting): + """Validate that this entity passes the defined guard conditions defined at setup.""" + + if state_key not in item: + return True + + for guard_condition in entity_setting["guard_condition"]: + if guard_condition and all( + item.get(guard_key) == guard_value + for guard_key, guard_value in guard_condition.items() + ): + return True diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py index f8dffaeda44..3f681db0987 100644 --- a/homeassistant/components/omnilogic/config_flow.py +++ b/homeassistant/components/omnilogic/config_flow.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import CONF_SCAN_INTERVAL, DOMAIN +from .const import CONF_SCAN_INTERVAL, DEFAULT_PH_OFFSET, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -88,8 +88,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_SCAN_INTERVAL, - default=6, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), ): int, + vol.Optional( + "ph_offset", + default=self.config_entry.options.get( + "ph_offset", DEFAULT_PH_OFFSET + ), + ): vol.All(vol.Coerce(float)), } ), ) diff --git a/homeassistant/components/omnilogic/const.py b/homeassistant/components/omnilogic/const.py index a57ef2b062a..41db7be5064 100644 --- a/homeassistant/components/omnilogic/const.py +++ b/homeassistant/components/omnilogic/const.py @@ -2,6 +2,8 @@ DOMAIN = "omnilogic" CONF_SCAN_INTERVAL = "polling_interval" +DEFAULT_SCAN_INTERVAL = 6 +DEFAULT_PH_OFFSET = 0 COORDINATOR = "coordinator" OMNI_API = "omni_api" ATTR_IDENTIFIERS = "identifiers" @@ -20,7 +22,7 @@ PUMP_TYPES = { ALL_ITEM_KINDS = { "BOWS", "Filter", - "Heater", + "Heaters", "Chlorinator", "CSAD", "Lights", diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 6e3d1593fe9..24cbe82e17b 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -9,8 +9,8 @@ from homeassistant.const import ( VOLUME_LITERS, ) -from .common import OmniLogicEntity, OmniLogicUpdateCoordinator -from .const import COORDINATOR, DOMAIN, PUMP_TYPES +from .common import OmniLogicEntity, OmniLogicUpdateCoordinator, check_guard +from .const import COORDINATOR, DEFAULT_PH_OFFSET, DOMAIN, PUMP_TYPES async def async_setup_entry(hass, entry, async_add_entities): @@ -29,18 +29,7 @@ async def async_setup_entry(hass, entry, async_add_entities): for entity_setting in entity_settings: for state_key, entity_class in entity_setting["entity_classes"].items(): - if state_key not in item: - continue - - guard = False - for guard_condition in entity_setting["guard_condition"]: - if guard_condition and all( - item.get(guard_key) == guard_value - for guard_key, guard_value in guard_condition.items() - ): - guard = True - - if guard: + if check_guard(state_key, item, entity_setting): continue entity = entity_class( @@ -147,6 +136,7 @@ class OmniLogicPumpSpeedSensor(OmnilogicSensor): self._unit = PERCENTAGE state = pump_speed elif pump_type == "DUAL": + self._unit = "" if pump_speed == 0: state = "off" elif pump_speed == self.coordinator.data[self._item_id].get( @@ -204,6 +194,12 @@ class OmniLogicPHSensor(OmnilogicSensor): if ph_state == 0: ph_state = None + else: + ph_state = float(ph_state) + float( + self.coordinator.config_entry.options.get( + "ph_offset", DEFAULT_PH_OFFSET + ) + ) return ph_state @@ -238,7 +234,7 @@ class OmniLogicORPSensor(OmnilogicSensor): def state(self): """Return the state for the ORP sensor.""" - orp_state = self.coordinator.data[self._item_id][self._state_key] + orp_state = int(self.coordinator.data[self._item_id][self._state_key]) if orp_state == -1: orp_state = None diff --git a/homeassistant/components/omnilogic/services.yaml b/homeassistant/components/omnilogic/services.yaml new file mode 100644 index 00000000000..32ad2716ade --- /dev/null +++ b/homeassistant/components/omnilogic/services.yaml @@ -0,0 +1,9 @@ +set_pump_speed: + description: Set the run speed of a variable speed pump. + fields: + entity_id: + description: Target switch entity + example: switch.pool_pump + speed: + description: Speed for the VSP between min and max speed. + example: 85 diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json index c050a7945f1..9c55877b3b0 100644 --- a/homeassistant/components/omnilogic/strings.json +++ b/homeassistant/components/omnilogic/strings.json @@ -21,7 +21,8 @@ "step": { "init": { "data": { - "polling_interval": "Polling interval (in seconds)" + "polling_interval": "Polling interval (in seconds)", + "ph_offset": "pH offset (positive or negative)" } } } diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py new file mode 100644 index 00000000000..fce4fb019da --- /dev/null +++ b/homeassistant/components/omnilogic/switch.py @@ -0,0 +1,264 @@ +"""Platform for Omnilogic switch integration.""" +import time + +from omnilogic import OmniLogicException +import voluptuous as vol + +from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers import config_validation as cv, entity_platform + +from .common import OmniLogicEntity, OmniLogicUpdateCoordinator, check_guard +from .const import COORDINATOR, DOMAIN, PUMP_TYPES + +SERVICE_SET_SPEED = "set_pump_speed" +OMNILOGIC_SWITCH_OFF = 7 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the light platform.""" + + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + entities = [] + + for item_id, item in coordinator.data.items(): + id_len = len(item_id) + item_kind = item_id[-2] + entity_settings = SWITCH_TYPES.get((id_len, item_kind)) + + if not entity_settings: + continue + + for entity_setting in entity_settings: + for state_key, entity_class in entity_setting["entity_classes"].items(): + if check_guard(state_key, item, entity_setting): + continue + + entity = entity_class( + coordinator=coordinator, + state_key=state_key, + name=entity_setting["name"], + kind=entity_setting["kind"], + item_id=item_id, + icon=entity_setting["icon"], + ) + + entities.append(entity) + + async_add_entities(entities) + + # register service + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_SPEED, + {vol.Required("speed"): cv.positive_int}, + "async_set_speed", + ) + + +class OmniLogicSwitch(OmniLogicEntity, SwitchEntity): + """Define an Omnilogic Base Switch entity which will be instantiated through specific switch type entities.""" + + def __init__( + self, + coordinator: OmniLogicUpdateCoordinator, + kind: str, + name: str, + icon: str, + item_id: tuple, + state_key: str, + ): + """Initialize Entities.""" + super().__init__( + coordinator=coordinator, + kind=kind, + name=name, + item_id=item_id, + icon=icon, + ) + + self._state_key = state_key + self._state = None + self._last_action = 0 + self._state_delay = 30 + + @property + def is_on(self): + """Return the on/off state of the switch.""" + state_int = 0 + + # The Omnilogic API has a significant delay in state reporting after calling for a + # change. This state delay will ensure that HA keeps an optimistic value of state + # during this period to improve the user experience and avoid confusion. + if self._last_action < (time.time() - self._state_delay): + state_int = int(self.coordinator.data[self._item_id][self._state_key]) + + if self._state == OMNILOGIC_SWITCH_OFF: + state_int = 0 + + self._state = state_int != 0 + + return self._state + + +class OmniLogicRelayControl(OmniLogicSwitch): + """Define the OmniLogic Relay entity.""" + + async def async_turn_on(self, **kwargs): + """Turn on the relay.""" + self._state = True + self._last_action = time.time() + self.async_schedule_update_ha_state() + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + 1, + ) + + async def async_turn_off(self, **kwargs): + """Turn off the relay.""" + self._state = False + self._last_action = time.time() + self.async_schedule_update_ha_state() + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + 0, + ) + + +class OmniLogicPumpControl(OmniLogicSwitch): + """Define the OmniLogic Pump Switch Entity.""" + + def __init__( + self, + coordinator: OmniLogicUpdateCoordinator, + kind: str, + name: str, + icon: str, + item_id: tuple, + state_key: str, + ): + """Initialize entities.""" + super().__init__( + coordinator=coordinator, + kind=kind, + name=name, + icon=icon, + item_id=item_id, + state_key=state_key, + ) + + self._max_speed = int(coordinator.data[item_id]["Max-Pump-Speed"]) + self._min_speed = int(coordinator.data[item_id]["Min-Pump-Speed"]) + + if "Filter-Type" in coordinator.data[item_id]: + self._pump_type = PUMP_TYPES[coordinator.data[item_id]["Filter-Type"]] + else: + self._pump_type = PUMP_TYPES[coordinator.data[item_id]["Type"]] + + self._last_speed = None + + async def async_turn_on(self, **kwargs): + """Turn on the pump.""" + self._state = True + self._last_action = time.time() + self.async_schedule_update_ha_state() + + on_value = 100 + + if self._pump_type != "SINGLE" and self._last_speed: + on_value = self._last_speed + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + on_value, + ) + + async def async_turn_off(self, **kwargs): + """Turn off the pump.""" + self._state = False + self._last_action = time.time() + self.async_schedule_update_ha_state() + + if self._pump_type != "SINGLE": + if "filterSpeed" in self.coordinator.data[self._item_id]: + self._last_speed = self.coordinator.data[self._item_id]["filterSpeed"] + else: + self._last_speed = self.coordinator.data[self._item_id]["pumpSpeed"] + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + 0, + ) + + async def async_set_speed(self, speed): + """Set the switch speed.""" + + if self._pump_type != "SINGLE": + if self._min_speed <= speed <= self._max_speed: + success = await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + speed, + ) + + if success: + self.async_schedule_update_ha_state() + + else: + raise OmniLogicException( + "Cannot set speed. Speed is outside pump range." + ) + + else: + raise OmniLogicException("Cannot set speed on a non-variable speed pump.") + + +SWITCH_TYPES = { + (4, "Relays"): [ + { + "entity_classes": {"switchState": OmniLogicRelayControl}, + "name": "", + "kind": "relay", + "icon": None, + "guard_condition": [], + }, + ], + (6, "Relays"): [ + { + "entity_classes": {"switchState": OmniLogicRelayControl}, + "name": "", + "kind": "relay", + "icon": None, + "guard_condition": [], + } + ], + (6, "Pumps"): [ + { + "entity_classes": {"pumpState": OmniLogicPumpControl}, + "name": "", + "kind": "pump", + "icon": None, + "guard_condition": [], + } + ], + (6, "Filter"): [ + { + "entity_classes": {"filterState": OmniLogicPumpControl}, + "name": "", + "kind": "pump", + "icon": None, + "guard_condition": [], + } + ], +} diff --git a/homeassistant/components/omnilogic/translations/en.json b/homeassistant/components/omnilogic/translations/en.json index e46253a922d..809f8a0ec28 100644 --- a/homeassistant/components/omnilogic/translations/en.json +++ b/homeassistant/components/omnilogic/translations/en.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH offset (positive or negative)", "polling_interval": "Polling interval (in seconds)" } }