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:
parent
7c28262bee
commit
f7cf82be6d
10 changed files with 337 additions and 29 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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']}"
|
||||
|
||||
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
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
9
homeassistant/components/omnilogic/services.yaml
Normal file
9
homeassistant/components/omnilogic/services.yaml
Normal 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
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
264
homeassistant/components/omnilogic/switch.py
Normal file
264
homeassistant/components/omnilogic/switch.py
Normal 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": [],
|
||||
}
|
||||
],
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"ph_offset": "pH offset (positive or negative)",
|
||||
"polling_interval": "Polling interval (in seconds)"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue