Add Omnilogic Switch platform (#42116)

* Bump omnilogic dependency to 0.4.4 to fix Lights key error.

* Bumped dependency to 0.4.5.

* Fixed pump type issue for unique pool structure.

* Create full platform bundle for final testing and PR to Home Assistant dev.

* Removed logger instances not required.

* Fixed lint issues.

* Fixed pylint issues.

* Fix pylint issues. Fix issue with pH sensor offset.

* Stripped light, water_heater platform for PR submit.

* Correct pH and ORP sensor report to unknown with offset if pump is off.

* Moving guard condition check to helper function.

* Update to asyncio.sleep to wait for switch status delay in Hayward API status.

* Removed sleep, added state delay to handle slow Hayward API state update response.

* Fix flake8 issue.

* Fix flake8 issue.

* Fix isort issue.

* Addressed PR Comments.

* Addressed PR comments. Corrected Unit of Measure for sensor where pump speed is not variable.

* Fix pylint issue.

* Address pylint issue.

* Update homeassistant/components/omnilogic/switch.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
djtimca 2021-04-29 10:46:04 -04:00 committed by GitHub
parent 7c28262bee
commit f7cf82be6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 337 additions and 29 deletions

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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)),
}
),
)

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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)"
}
}
}

View file

@ -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": [],
}
],
}

View file

@ -21,6 +21,7 @@
"step": {
"init": {
"data": {
"ph_offset": "pH offset (positive or negative)",
"polling_interval": "Polling interval (in seconds)"
}
}