Migrate RainMachine to DataUpdateCoordinator (#42530)
parent
4e614e0f2c
commit
bba7c15d79
|
@ -1,12 +1,14 @@
|
|||
"""Support for RainMachine devices."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from regenmaschine import Client
|
||||
from regenmaschine.controller import Controller
|
||||
from regenmaschine.errors import RainMachineError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONF_IP_ADDRESS,
|
||||
|
@ -14,32 +16,30 @@ from homeassistant.const import (
|
|||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.service import verify_domain_control
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_ZONE_RUN_TIME,
|
||||
DATA_CLIENT,
|
||||
DATA_CONTROLLER,
|
||||
DATA_COORDINATOR,
|
||||
DATA_PROGRAMS,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
DATA_ZONES,
|
||||
DATA_ZONES_DETAILS,
|
||||
DEFAULT_ZONE_RUN,
|
||||
DOMAIN,
|
||||
PROGRAM_UPDATE_TOPIC,
|
||||
SENSOR_UPDATE_TOPIC,
|
||||
ZONE_UPDATE_TOPIC,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PROGRAM_ID = "program_id"
|
||||
CONF_SECONDS = "seconds"
|
||||
CONF_ZONE_ID = "zone_id"
|
||||
|
@ -48,8 +48,8 @@ DATA_LISTENER = "listener"
|
|||
|
||||
DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC"
|
||||
DEFAULT_ICON = "mdi:water"
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=60)
|
||||
DEFAULT_SSL = True
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
SERVICE_ALTER_PROGRAM = vol.Schema({vol.Required(CONF_PROGRAM_ID): cv.positive_int})
|
||||
|
||||
|
@ -76,30 +76,54 @@ SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_i
|
|||
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119")
|
||||
|
||||
PLATFORMS = ["binary_sensor", "sensor", "switch"]
|
||||
|
||||
async def async_setup(hass, config):
|
||||
|
||||
async def async_update_programs_and_zones(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Update program and zone DataUpdateCoordinators.
|
||||
|
||||
Program and zone updates always go together because of how linked they are:
|
||||
programs affect zones and certain combinations of zones affect programs.
|
||||
"""
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
DATA_PROGRAMS
|
||||
].async_refresh(),
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
DATA_ZONES
|
||||
].async_refresh(),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the RainMachine component."""
|
||||
hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}}
|
||||
hass.data[DOMAIN] = {DATA_CONTROLLER: {}, DATA_COORDINATOR: {}, DATA_LISTENER: {}}
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up RainMachine as config entry."""
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {}
|
||||
|
||||
entry_updates = {}
|
||||
if not config_entry.unique_id:
|
||||
if not entry.unique_id:
|
||||
# If the config entry doesn't already have a unique ID, set one:
|
||||
entry_updates["unique_id"] = config_entry.data[CONF_IP_ADDRESS]
|
||||
if CONF_ZONE_RUN_TIME in config_entry.data:
|
||||
entry_updates["unique_id"] = entry.data[CONF_IP_ADDRESS]
|
||||
if CONF_ZONE_RUN_TIME in entry.data:
|
||||
# If a zone run time exists in the config entry's data, pop it and move it to
|
||||
# options:
|
||||
data = {**config_entry.data}
|
||||
data = {**entry.data}
|
||||
entry_updates["data"] = data
|
||||
entry_updates["options"] = {
|
||||
**config_entry.options,
|
||||
**entry.options,
|
||||
CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME),
|
||||
}
|
||||
if entry_updates:
|
||||
hass.config_entries.async_update_entry(config_entry, **entry_updates)
|
||||
hass.config_entries.async_update_entry(entry, **entry_updates)
|
||||
|
||||
_verify_domain_control = verify_domain_control(hass, DOMAIN)
|
||||
|
||||
|
@ -108,97 +132,133 @@ async def async_setup_entry(hass, config_entry):
|
|||
|
||||
try:
|
||||
await client.load_local(
|
||||
config_entry.data[CONF_IP_ADDRESS],
|
||||
config_entry.data[CONF_PASSWORD],
|
||||
port=config_entry.data[CONF_PORT],
|
||||
ssl=config_entry.data.get(CONF_SSL, DEFAULT_SSL),
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
entry.data[CONF_PASSWORD],
|
||||
port=entry.data[CONF_PORT],
|
||||
ssl=entry.data.get(CONF_SSL, DEFAULT_SSL),
|
||||
)
|
||||
except RainMachineError as err:
|
||||
_LOGGER.error("An error occurred: %s", err)
|
||||
LOGGER.error("An error occurred: %s", err)
|
||||
raise ConfigEntryNotReady from err
|
||||
else:
|
||||
# regenmaschine can load multiple controllers at once, but we only grab the one
|
||||
# we loaded above:
|
||||
controller = next(iter(client.controllers.values()))
|
||||
rainmachine = RainMachine(hass, config_entry, controller)
|
||||
|
||||
# Update the data object, which at this point (prior to any sensors registering
|
||||
# "interest" in the API), will focus on grabbing the latest program and zone data:
|
||||
await rainmachine.async_update()
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = rainmachine
|
||||
# regenmaschine can load multiple controllers at once, but we only grab the one
|
||||
# we loaded above:
|
||||
controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] = next(
|
||||
iter(client.controllers.values())
|
||||
)
|
||||
|
||||
for component in ("binary_sensor", "sensor", "switch"):
|
||||
async def async_update(api_category: str) -> dict:
|
||||
"""Update the appropriate API data based on a category."""
|
||||
try:
|
||||
if api_category == DATA_PROGRAMS:
|
||||
return await controller.programs.all(include_inactive=True)
|
||||
|
||||
if api_category == DATA_PROVISION_SETTINGS:
|
||||
return await controller.provisioning.settings()
|
||||
|
||||
if api_category == DATA_RESTRICTIONS_CURRENT:
|
||||
return await controller.restrictions.current()
|
||||
|
||||
if api_category == DATA_RESTRICTIONS_UNIVERSAL:
|
||||
return await controller.restrictions.universal()
|
||||
|
||||
return await controller.zones.all(details=True, include_inactive=True)
|
||||
except RainMachineError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
controller_init_tasks = []
|
||||
for api_category in [
|
||||
DATA_PROGRAMS,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
DATA_ZONES,
|
||||
]:
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
api_category
|
||||
] = DataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=f'{controller.name} ("{api_category}")',
|
||||
update_interval=DEFAULT_UPDATE_INTERVAL,
|
||||
update_method=partial(async_update, api_category),
|
||||
)
|
||||
controller_init_tasks.append(coordinator.async_refresh())
|
||||
|
||||
await asyncio.gather(*controller_init_tasks)
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
@_verify_domain_control
|
||||
async def disable_program(call):
|
||||
async def disable_program(call: ServiceCall):
|
||||
"""Disable a program."""
|
||||
await rainmachine.controller.programs.disable(call.data[CONF_PROGRAM_ID])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.programs.disable(call.data[CONF_PROGRAM_ID])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def disable_zone(call):
|
||||
async def disable_zone(call: ServiceCall):
|
||||
"""Disable a zone."""
|
||||
await rainmachine.controller.zones.disable(call.data[CONF_ZONE_ID])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.zones.disable(call.data[CONF_ZONE_ID])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def enable_program(call):
|
||||
async def enable_program(call: ServiceCall):
|
||||
"""Enable a program."""
|
||||
await rainmachine.controller.programs.enable(call.data[CONF_PROGRAM_ID])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.programs.enable(call.data[CONF_PROGRAM_ID])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def enable_zone(call):
|
||||
async def enable_zone(call: ServiceCall):
|
||||
"""Enable a zone."""
|
||||
await rainmachine.controller.zones.enable(call.data[CONF_ZONE_ID])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.zones.enable(call.data[CONF_ZONE_ID])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def pause_watering(call):
|
||||
async def pause_watering(call: ServiceCall):
|
||||
"""Pause watering for a set number of seconds."""
|
||||
await rainmachine.controller.watering.pause_all(call.data[CONF_SECONDS])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.watering.pause_all(call.data[CONF_SECONDS])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def start_program(call):
|
||||
async def start_program(call: ServiceCall):
|
||||
"""Start a particular program."""
|
||||
await rainmachine.controller.programs.start(call.data[CONF_PROGRAM_ID])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.programs.start(call.data[CONF_PROGRAM_ID])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def start_zone(call):
|
||||
async def start_zone(call: ServiceCall):
|
||||
"""Start a particular zone for a certain amount of time."""
|
||||
await rainmachine.controller.zones.start(
|
||||
await controller.zones.start(
|
||||
call.data[CONF_ZONE_ID], call.data[CONF_ZONE_RUN_TIME]
|
||||
)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def stop_all(call):
|
||||
async def stop_all(call: ServiceCall):
|
||||
"""Stop all watering."""
|
||||
await rainmachine.controller.watering.stop_all()
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.watering.stop_all()
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def stop_program(call):
|
||||
async def stop_program(call: ServiceCall):
|
||||
"""Stop a program."""
|
||||
await rainmachine.controller.programs.stop(call.data[CONF_PROGRAM_ID])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.programs.stop(call.data[CONF_PROGRAM_ID])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def stop_zone(call):
|
||||
async def stop_zone(call: ServiceCall):
|
||||
"""Stop a zone."""
|
||||
await rainmachine.controller.zones.stop(call.data[CONF_ZONE_ID])
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.zones.stop(call.data[CONF_ZONE_ID])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
@_verify_domain_control
|
||||
async def unpause_watering(call):
|
||||
async def unpause_watering(call: ServiceCall):
|
||||
"""Unpause watering."""
|
||||
await rainmachine.controller.watering.unpause_all()
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
await controller.watering.unpause_all()
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
for service, method, schema in [
|
||||
("disable_program", disable_program, SERVICE_ALTER_PROGRAM),
|
||||
|
@ -215,194 +275,68 @@ async def async_setup_entry(hass, config_entry):
|
|||
]:
|
||||
hass.services.async_register(DOMAIN, service, method, schema=schema)
|
||||
|
||||
hass.data[DOMAIN][DATA_LISTENER] = config_entry.add_update_listener(
|
||||
async_reload_entry
|
||||
hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload an RainMachine config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id)
|
||||
cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id)
|
||||
cancel_listener()
|
||||
|
||||
return True
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload an OpenUV config entry."""
|
||||
hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
|
||||
cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
|
||||
cancel_listener()
|
||||
|
||||
tasks = [
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in ("binary_sensor", "sensor", "switch")
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_reload_entry(hass, config_entry):
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle an options update."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
class RainMachine:
|
||||
"""Define a generic RainMachine object."""
|
||||
|
||||
def __init__(self, hass, config_entry, controller):
|
||||
"""Initialize."""
|
||||
self._async_cancel_time_interval_listener = None
|
||||
self.config_entry = config_entry
|
||||
self.controller = controller
|
||||
self.data = {}
|
||||
self.device_mac = controller.mac
|
||||
self.hass = hass
|
||||
|
||||
self._api_category_count = {
|
||||
DATA_PROVISION_SETTINGS: 0,
|
||||
DATA_RESTRICTIONS_CURRENT: 0,
|
||||
DATA_RESTRICTIONS_UNIVERSAL: 0,
|
||||
}
|
||||
self._api_category_locks = {
|
||||
DATA_PROVISION_SETTINGS: asyncio.Lock(),
|
||||
DATA_RESTRICTIONS_CURRENT: asyncio.Lock(),
|
||||
DATA_RESTRICTIONS_UNIVERSAL: asyncio.Lock(),
|
||||
}
|
||||
|
||||
async def _async_update_listener_action(self, now):
|
||||
"""Define an async_track_time_interval action to update data."""
|
||||
await self.async_update()
|
||||
|
||||
@callback
|
||||
def async_deregister_sensor_api_interest(self, api_category):
|
||||
"""Decrement the number of entities with data needs from an API category."""
|
||||
# If this deregistration should leave us with no registration at all, remove the
|
||||
# time interval:
|
||||
if sum(self._api_category_count.values()) == 0:
|
||||
if self._async_cancel_time_interval_listener:
|
||||
self._async_cancel_time_interval_listener()
|
||||
self._async_cancel_time_interval_listener = None
|
||||
return
|
||||
|
||||
self._api_category_count[api_category] -= 1
|
||||
|
||||
async def async_fetch_from_api(self, api_category):
|
||||
"""Execute the appropriate coroutine to fetch particular data from the API."""
|
||||
if api_category == DATA_PROGRAMS:
|
||||
data = await self.controller.programs.all(include_inactive=True)
|
||||
elif api_category == DATA_PROVISION_SETTINGS:
|
||||
data = await self.controller.provisioning.settings()
|
||||
elif api_category == DATA_RESTRICTIONS_CURRENT:
|
||||
data = await self.controller.restrictions.current()
|
||||
elif api_category == DATA_RESTRICTIONS_UNIVERSAL:
|
||||
data = await self.controller.restrictions.universal()
|
||||
elif api_category == DATA_ZONES:
|
||||
data = await self.controller.zones.all(include_inactive=True)
|
||||
elif api_category == DATA_ZONES_DETAILS:
|
||||
# This API call needs to be separate from the DATA_ZONES one above because,
|
||||
# maddeningly, the DATA_ZONES_DETAILS API call doesn't include the current
|
||||
# state of the zone:
|
||||
data = await self.controller.zones.all(details=True, include_inactive=True)
|
||||
|
||||
self.data[api_category] = data
|
||||
|
||||
async def async_register_sensor_api_interest(self, api_category):
|
||||
"""Increment the number of entities with data needs from an API category."""
|
||||
# If this is the first registration we have, start a time interval:
|
||||
if not self._async_cancel_time_interval_listener:
|
||||
self._async_cancel_time_interval_listener = async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_update_listener_action,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self._api_category_count[api_category] += 1
|
||||
|
||||
# If a sensor registers interest in a particular API call and the data doesn't
|
||||
# exist for it yet, make the API call and grab the data:
|
||||
async with self._api_category_locks[api_category]:
|
||||
if api_category not in self.data:
|
||||
await self.async_fetch_from_api(api_category)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update all RainMachine data."""
|
||||
tasks = [self.async_update_programs_and_zones(), self.async_update_sensors()]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def async_update_sensors(self):
|
||||
"""Update sensor/binary sensor data."""
|
||||
_LOGGER.debug("Updating sensor data for RainMachine")
|
||||
|
||||
# Fetch an API category if there is at least one interested entity:
|
||||
tasks = {}
|
||||
for category, count in self._api_category_count.items():
|
||||
if count == 0:
|
||||
continue
|
||||
tasks[category] = self.async_fetch_from_api(category)
|
||||
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
for api_category, result in zip(tasks, results):
|
||||
if isinstance(result, RainMachineError):
|
||||
_LOGGER.error(
|
||||
"There was an error while updating %s: %s", api_category, result
|
||||
)
|
||||
continue
|
||||
|
||||
async_dispatcher_send(self.hass, SENSOR_UPDATE_TOPIC)
|
||||
|
||||
async def async_update_programs_and_zones(self):
|
||||
"""Update program and zone data.
|
||||
|
||||
Program and zone updates always go together because of how linked they are:
|
||||
programs affect zones and certain combinations of zones affect programs.
|
||||
|
||||
Note that this call does not take into account interested entities when making
|
||||
the API calls; we make the reasonable assumption that switches will always be
|
||||
enabled.
|
||||
"""
|
||||
_LOGGER.debug("Updating program and zone data for RainMachine")
|
||||
|
||||
tasks = {
|
||||
DATA_PROGRAMS: self.async_fetch_from_api(DATA_PROGRAMS),
|
||||
DATA_ZONES: self.async_fetch_from_api(DATA_ZONES),
|
||||
DATA_ZONES_DETAILS: self.async_fetch_from_api(DATA_ZONES_DETAILS),
|
||||
}
|
||||
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
for api_category, result in zip(tasks, results):
|
||||
if isinstance(result, RainMachineError):
|
||||
_LOGGER.error(
|
||||
"There was an error while updating %s: %s", api_category, result
|
||||
)
|
||||
|
||||
async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
|
||||
async_dispatcher_send(self.hass, ZONE_UPDATE_TOPIC)
|
||||
|
||||
|
||||
class RainMachineEntity(Entity):
|
||||
class RainMachineEntity(CoordinatorEntity):
|
||||
"""Define a generic RainMachine entity."""
|
||||
|
||||
def __init__(self, rainmachine):
|
||||
def __init__(
|
||||
self, coordinator: DataUpdateCoordinator, controller: Controller
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||
self._controller = controller
|
||||
self._device_class = None
|
||||
# The colons are removed from the device MAC simply because that value
|
||||
# (unnecessarily) makes up the existing unique ID formula and we want to avoid
|
||||
# a breaking change:
|
||||
self._unique_id = controller.mac.replace(":", "")
|
||||
self._name = None
|
||||
self.rainmachine = rainmachine
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
def device_class(self) -> str:
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> dict:
|
||||
"""Return device registry information for this entity."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.rainmachine.controller.mac)},
|
||||
"name": self.rainmachine.controller.name,
|
||||
"identifiers": {(DOMAIN, self._controller.mac)},
|
||||
"name": self._controller.name,
|
||||
"manufacturer": "RainMachine",
|
||||
"model": (
|
||||
f"Version {self.rainmachine.controller.hardware_version} "
|
||||
f"(API: {self.rainmachine.controller.api_version})"
|
||||
f"Version {self._controller.hardware_version} "
|
||||
f"(API: {self._controller.api_version})"
|
||||
),
|
||||
"sw_version": self.rainmachine.controller.software_version,
|
||||
"sw_version": self._controller.software_version,
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -415,18 +349,18 @@ class RainMachineEntity(Entity):
|
|||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _update_state(self):
|
||||
"""Update the state."""
|
||||
def _handle_coordinator_update(self):
|
||||
"""Respond to a DataUpdateCoordinator update."""
|
||||
self.update_from_latest_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
self.update_from_latest_data()
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self):
|
||||
"""Update the entity."""
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
"""This platform provides binary sensors for key RainMachine data."""
|
||||
from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
from regenmaschine.controller import Controller
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import RainMachineEntity
|
||||
from .const import (
|
||||
DATA_CLIENT,
|
||||
DATA_CONTROLLER,
|
||||
DATA_COORDINATOR,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
DOMAIN as RAINMACHINE_DOMAIN,
|
||||
SENSOR_UPDATE_TOPIC,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
TYPE_FLOW_SENSOR = "flow_sensor"
|
||||
|
@ -66,32 +72,60 @@ BINARY_SENSORS = {
|
|||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
|
||||
) -> None:
|
||||
"""Set up RainMachine binary sensors based on a config entry."""
|
||||
rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id]
|
||||
coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id]
|
||||
|
||||
@callback
|
||||
def async_get_sensor(api_category: str) -> partial:
|
||||
"""Generate the appropriate sensor object for an API category."""
|
||||
if api_category == DATA_PROVISION_SETTINGS:
|
||||
return partial(
|
||||
ProvisionSettingsBinarySensor,
|
||||
coordinators[DATA_PROVISION_SETTINGS],
|
||||
)
|
||||
|
||||
if api_category == DATA_RESTRICTIONS_CURRENT:
|
||||
return partial(
|
||||
CurrentRestrictionsBinarySensor,
|
||||
coordinators[DATA_RESTRICTIONS_CURRENT],
|
||||
)
|
||||
|
||||
return partial(
|
||||
UniversalRestrictionsBinarySensor,
|
||||
coordinators[DATA_RESTRICTIONS_UNIVERSAL],
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
RainMachineBinarySensor(
|
||||
rainmachine, sensor_type, name, icon, enabled_by_default, api_category
|
||||
async_get_sensor(api_category)(
|
||||
controller, sensor_type, name, icon, enabled_by_default
|
||||
)
|
||||
for (
|
||||
sensor_type,
|
||||
(name, icon, enabled_by_default, api_category),
|
||||
) in BINARY_SENSORS.items()
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity):
|
||||
"""A sensor implementation for raincloud device."""
|
||||
"""Define a general RainMachine binary sensor."""
|
||||
|
||||
def __init__(
|
||||
self, rainmachine, sensor_type, name, icon, enabled_by_default, api_category
|
||||
):
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
controller: Controller,
|
||||
sensor_type: str,
|
||||
name: str,
|
||||
icon: str,
|
||||
enabled_by_default: bool,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(rainmachine)
|
||||
|
||||
self._api_category = api_category
|
||||
super().__init__(coordinator, controller)
|
||||
self._enabled_by_default = enabled_by_default
|
||||
self._icon = icon
|
||||
self._name = name
|
||||
|
@ -99,7 +133,7 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity):
|
|||
self._state = None
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self):
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Determine whether an entity is enabled by default."""
|
||||
return self._enabled_by_default
|
||||
|
||||
|
@ -109,54 +143,61 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity):
|
|||
return self._icon
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return "{}_{}".format(
|
||||
self.rainmachine.device_mac.replace(":", ""), self._sensor_type
|
||||
)
|
||||
return f"{self._unique_id}_{self._sensor_type}"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state)
|
||||
)
|
||||
await self.rainmachine.async_register_sensor_api_interest(self._api_category)
|
||||
self.update_from_latest_data()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listeners and deregister API interest."""
|
||||
super().async_will_remove_from_hass()
|
||||
self.rainmachine.async_deregister_sensor_api_interest(self._api_category)
|
||||
class CurrentRestrictionsBinarySensor(RainMachineBinarySensor):
|
||||
"""Define a binary sensor that handles current restrictions data."""
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FREEZE:
|
||||
self._state = self.coordinator.data["freeze"]
|
||||
elif self._sensor_type == TYPE_HOURLY:
|
||||
self._state = self.coordinator.data["hourly"]
|
||||
elif self._sensor_type == TYPE_MONTH:
|
||||
self._state = self.coordinator.data["month"]
|
||||
elif self._sensor_type == TYPE_RAINDELAY:
|
||||
self._state = self.coordinator.data["rainDelay"]
|
||||
elif self._sensor_type == TYPE_RAINSENSOR:
|
||||
self._state = self.coordinator.data["rainSensor"]
|
||||
elif self._sensor_type == TYPE_WEEKDAY:
|
||||
self._state = self.coordinator.data["weekDay"]
|
||||
|
||||
|
||||
class ProvisionSettingsBinarySensor(RainMachineBinarySensor):
|
||||
"""Define a binary sensor that handles provisioning data."""
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FREEZE:
|
||||
self._state = self.coordinator.data["freeze"]
|
||||
elif self._sensor_type == TYPE_HOURLY:
|
||||
self._state = self.coordinator.data["hourly"]
|
||||
elif self._sensor_type == TYPE_MONTH:
|
||||
self._state = self.coordinator.data["month"]
|
||||
elif self._sensor_type == TYPE_RAINDELAY:
|
||||
self._state = self.coordinator.data["rainDelay"]
|
||||
elif self._sensor_type == TYPE_RAINSENSOR:
|
||||
self._state = self.coordinator.data["rainSensor"]
|
||||
elif self._sensor_type == TYPE_WEEKDAY:
|
||||
self._state = self.coordinator.data["weekDay"]
|
||||
|
||||
|
||||
class UniversalRestrictionsBinarySensor(RainMachineBinarySensor):
|
||||
"""Define a binary sensor that handles universal restrictions data."""
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FLOW_SENSOR:
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"useFlowSensor"
|
||||
)
|
||||
elif self._sensor_type == TYPE_FREEZE:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["freeze"]
|
||||
elif self._sensor_type == TYPE_FREEZE_PROTECTION:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][
|
||||
"freezeProtectEnabled"
|
||||
]
|
||||
elif self._sensor_type == TYPE_HOT_DAYS:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][
|
||||
"hotDaysExtraWatering"
|
||||
]
|
||||
elif self._sensor_type == TYPE_HOURLY:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["hourly"]
|
||||
elif self._sensor_type == TYPE_MONTH:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["month"]
|
||||
elif self._sensor_type == TYPE_RAINDELAY:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["rainDelay"]
|
||||
elif self._sensor_type == TYPE_RAINSENSOR:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["rainSensor"]
|
||||
elif self._sensor_type == TYPE_WEEKDAY:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["weekDay"]
|
||||
self._state = self.coordinator.data["system"].get("useFlowSensor")
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
"""Define constants for the SimpliSafe component."""
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "rainmachine"
|
||||
|
||||
CONF_ZONE_RUN_TIME = "zone_run_time"
|
||||
|
||||
DATA_CLIENT = "client"
|
||||
DATA_CONTROLLER = "controller"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DATA_LISTENER = "listener"
|
||||
DATA_PROGRAMS = "programs"
|
||||
DATA_PROVISION_SETTINGS = "provision.settings"
|
||||
DATA_RESTRICTIONS_CURRENT = "restrictions.current"
|
||||
DATA_RESTRICTIONS_UNIVERSAL = "restrictions.universal"
|
||||
DATA_ZONES = "zones"
|
||||
DATA_ZONES_DETAILS = "zones_details"
|
||||
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_ZONE_RUN = 60 * 10
|
||||
|
||||
PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update"
|
||||
SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update"
|
||||
ZONE_UPDATE_TOPIC = f"{DOMAIN}_zone_update"
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"name": "RainMachine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
||||
"requirements": ["regenmaschine==2.1.0"],
|
||||
"requirements": ["regenmaschine==3.0.0"],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
"""This platform provides support for sensor data from RainMachine."""
|
||||
from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
from regenmaschine.controller import Controller
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import RainMachineEntity
|
||||
from .const import (
|
||||
DATA_CLIENT,
|
||||
DATA_CONTROLLER,
|
||||
DATA_COORDINATOR,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
DOMAIN as RAINMACHINE_DOMAIN,
|
||||
SENSOR_UPDATE_TOPIC,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
TYPE_FLOW_SENSOR_CLICK_M3 = "flow_sensor_clicks_cubic_meter"
|
||||
|
@ -62,20 +68,37 @@ SENSORS = {
|
|||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
|
||||
) -> None:
|
||||
"""Set up RainMachine sensors based on a config entry."""
|
||||
rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id]
|
||||
coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id]
|
||||
|
||||
@callback
|
||||
def async_get_sensor(api_category: str) -> partial:
|
||||
"""Generate the appropriate sensor object for an API category."""
|
||||
if api_category == DATA_PROVISION_SETTINGS:
|
||||
return partial(
|
||||
ProvisionSettingsSensor,
|
||||
coordinators[DATA_PROVISION_SETTINGS],
|
||||
)
|
||||
|
||||
return partial(
|
||||
UniversalRestrictionsSensor,
|
||||
coordinators[DATA_RESTRICTIONS_UNIVERSAL],
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
RainMachineSensor(
|
||||
rainmachine,
|
||||
async_get_sensor(api_category)(
|
||||
controller,
|
||||
sensor_type,
|
||||
name,
|
||||
icon,
|
||||
unit,
|
||||
device_class,
|
||||
enabled_by_default,
|
||||
api_category,
|
||||
)
|
||||
for (
|
||||
sensor_type,
|
||||
|
@ -86,23 +109,21 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
|
||||
|
||||
class RainMachineSensor(RainMachineEntity):
|
||||
"""A sensor implementation for raincloud device."""
|
||||
"""Define a general RainMachine sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rainmachine,
|
||||
sensor_type,
|
||||
name,
|
||||
icon,
|
||||
unit,
|
||||
device_class,
|
||||
enabled_by_default,
|
||||
api_category,
|
||||
):
|
||||
coordinator: DataUpdateCoordinator,
|
||||
controller: Controller,
|
||||
sensor_type: str,
|
||||
name: str,
|
||||
icon: str,
|
||||
unit: str,
|
||||
device_class: str,
|
||||
enabled_by_default: bool,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(rainmachine)
|
||||
|
||||
self._api_category = api_category
|
||||
super().__init__(coordinator, controller)
|
||||
self._device_class = device_class
|
||||
self._enabled_by_default = enabled_by_default
|
||||
self._icon = icon
|
||||
|
@ -112,7 +133,7 @@ class RainMachineSensor(RainMachineEntity):
|
|||
self._unit = unit
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self):
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Determine whether an entity is enabled by default."""
|
||||
return self._enabled_by_default
|
||||
|
||||
|
@ -129,56 +150,47 @@ class RainMachineSensor(RainMachineEntity):
|
|||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return "{}_{}".format(
|
||||
self.rainmachine.device_mac.replace(":", ""), self._sensor_type
|
||||
)
|
||||
return f"{self._unique_id}_{self._sensor_type}"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state)
|
||||
)
|
||||
await self.rainmachine.async_register_sensor_api_interest(self._api_category)
|
||||
self.update_from_latest_data()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listeners and deregister API interest."""
|
||||
super().async_will_remove_from_hass()
|
||||
self.rainmachine.async_deregister_sensor_api_interest(self._api_category)
|
||||
class ProvisionSettingsSensor(RainMachineSensor):
|
||||
"""Define a sensor that handles provisioning data."""
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self):
|
||||
"""Update the sensor's state."""
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FLOW_SENSOR_CLICK_M3:
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
self._state = self.coordinator.data["system"].get(
|
||||
"flowSensorClicksPerCubicMeter"
|
||||
)
|
||||
elif self._sensor_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
|
||||
clicks = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"flowSensorWateringClicks"
|
||||
clicks = self.coordinator.data["system"].get("flowSensorWateringClicks")
|
||||
clicks_per_m3 = self.coordinator.data["system"].get(
|
||||
"flowSensorClicksPerCubicMeter"
|
||||
)
|
||||
clicks_per_m3 = self.rainmachine.data[DATA_PROVISION_SETTINGS][
|
||||
"system"
|
||||
].get("flowSensorClicksPerCubicMeter")
|
||||
|
||||
if clicks and clicks_per_m3:
|
||||
self._state = (clicks * 1000) / clicks_per_m3
|
||||
else:
|
||||
self._state = None
|
||||
elif self._sensor_type == TYPE_FLOW_SENSOR_START_INDEX:
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"flowSensorStartIndex"
|
||||
)
|
||||
self._state = self.coordinator.data["system"].get("flowSensorStartIndex")
|
||||
elif self._sensor_type == TYPE_FLOW_SENSOR_WATERING_CLICKS:
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
self._state = self.coordinator.data["system"].get(
|
||||
"flowSensorWateringClicks"
|
||||
)
|
||||
elif self._sensor_type == TYPE_FREEZE_TEMP:
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][
|
||||
"freezeProtectTemp"
|
||||
]
|
||||
|
||||
|
||||
class UniversalRestrictionsSensor(RainMachineSensor):
|
||||
"""Define a sensor that handles universal restrictions data."""
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FREEZE_TEMP:
|
||||
self._state = self.coordinator.data["freezeProtectTemp"]
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
"""This component provides support for RainMachine programs and zones."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Callable, Coroutine
|
||||
|
||||
from regenmaschine.controller import Controller
|
||||
from regenmaschine.errors import RequestError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import RainMachineEntity
|
||||
from . import RainMachineEntity, async_update_programs_and_zones
|
||||
from .const import (
|
||||
CONF_ZONE_RUN_TIME,
|
||||
DATA_CLIENT,
|
||||
DATA_CONTROLLER,
|
||||
DATA_COORDINATOR,
|
||||
DATA_PROGRAMS,
|
||||
DATA_ZONES,
|
||||
DATA_ZONES_DETAILS,
|
||||
DOMAIN as RAINMACHINE_DOMAIN,
|
||||
PROGRAM_UPDATE_TOPIC,
|
||||
ZONE_UPDATE_TOPIC,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AREA = "area"
|
||||
ATTR_CS_ON = "cs_on"
|
||||
ATTR_CURRENT_CYCLE = "current_cycle"
|
||||
|
@ -100,36 +99,56 @@ SWITCH_TYPE_PROGRAM = "program"
|
|||
SWITCH_TYPE_ZONE = "zone"
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
|
||||
) -> None:
|
||||
"""Set up RainMachine switches based on a config entry."""
|
||||
rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id]
|
||||
programs_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
DATA_PROGRAMS
|
||||
]
|
||||
zones_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][DATA_ZONES]
|
||||
|
||||
entities = []
|
||||
for program in rainmachine.data[DATA_PROGRAMS]:
|
||||
entities.append(RainMachineProgram(rainmachine, program))
|
||||
for zone in rainmachine.data[DATA_ZONES]:
|
||||
entities.append(RainMachineZone(rainmachine, zone))
|
||||
for uid, program in programs_coordinator.data.items():
|
||||
entities.append(
|
||||
RainMachineProgram(
|
||||
programs_coordinator, controller, uid, program["name"], entry
|
||||
)
|
||||
)
|
||||
for uid, zone in zones_coordinator.data.items():
|
||||
entities.append(
|
||||
RainMachineZone(zones_coordinator, controller, uid, zone["name"], entry)
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RainMachineSwitch(RainMachineEntity, SwitchEntity):
|
||||
"""A class to represent a generic RainMachine switch."""
|
||||
|
||||
def __init__(self, rainmachine, switch_data):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
controller: Controller,
|
||||
uid: int,
|
||||
name: str,
|
||||
entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize a generic RainMachine switch."""
|
||||
super().__init__(rainmachine)
|
||||
|
||||
super().__init__(coordinator, controller)
|
||||
self._data = coordinator.data[uid]
|
||||
self._entry = entry
|
||||
self._is_active = True
|
||||
self._is_on = False
|
||||
self._name = switch_data["name"]
|
||||
self._switch_data = switch_data
|
||||
self._rainmachine_entity_id = switch_data["uid"]
|
||||
self._switch_type = None
|
||||
self._name = name
|
||||
self._switch_type = type(self).__name__
|
||||
self._uid = uid
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._switch_data["active"]
|
||||
return self._is_active and self.coordinator.last_update_success
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
|
@ -144,18 +163,14 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
|
|||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return "{}_{}_{}".format(
|
||||
self.rainmachine.device_mac.replace(":", ""),
|
||||
self._switch_type,
|
||||
self._rainmachine_entity_id,
|
||||
)
|
||||
return f"{self._unique_id}_{self._switch_type}_{self._uid}"
|
||||
|
||||
async def _async_run_switch_coroutine(self, api_coro) -> None:
|
||||
async def _async_run_switch_coroutine(self, api_coro: Coroutine) -> None:
|
||||
"""Run a coroutine to toggle the switch."""
|
||||
try:
|
||||
resp = await api_coro
|
||||
except RequestError as err:
|
||||
_LOGGER.error(
|
||||
LOGGER.error(
|
||||
'Error while toggling %s "%s": %s',
|
||||
self._switch_type,
|
||||
self.unique_id,
|
||||
|
@ -164,7 +179,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
|
|||
return
|
||||
|
||||
if resp["statusCode"] != 0:
|
||||
_LOGGER.error(
|
||||
LOGGER.error(
|
||||
'Error while toggling %s "%s": %s',
|
||||
self._switch_type,
|
||||
self.unique_id,
|
||||
|
@ -172,69 +187,60 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
|
|||
)
|
||||
return
|
||||
|
||||
self.hass.async_create_task(self.rainmachine.async_update_programs_and_zones())
|
||||
# Because of how inextricably linked programs and zones are, anytime one is
|
||||
# toggled, we make sure to update the data of both coordinators:
|
||||
self.hass.async_create_task(
|
||||
async_update_programs_and_zones(self.hass, self._entry)
|
||||
)
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
self._data = self.coordinator.data[self._uid]
|
||||
self._is_active = self._data["active"]
|
||||
|
||||
|
||||
class RainMachineProgram(RainMachineSwitch):
|
||||
"""A RainMachine program."""
|
||||
|
||||
def __init__(self, rainmachine, switch_data):
|
||||
"""Initialize a generic RainMachine switch."""
|
||||
super().__init__(rainmachine, switch_data)
|
||||
self._switch_type = SWITCH_TYPE_PROGRAM
|
||||
|
||||
@property
|
||||
def zones(self) -> list:
|
||||
"""Return a list of active zones associated with this program."""
|
||||
return [z for z in self._switch_data["wateringTimes"] if z["active"]]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
|
||||
)
|
||||
)
|
||||
return [z for z in self._data["wateringTimes"] if z["active"]]
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the program off."""
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.controller.programs.stop(self._rainmachine_entity_id)
|
||||
self._controller.programs.stop(self._uid)
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the program on."""
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.controller.programs.start(self._rainmachine_entity_id)
|
||||
self._controller.programs.start(self._uid)
|
||||
)
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update info for the program."""
|
||||
[self._switch_data] = [
|
||||
p
|
||||
for p in self.rainmachine.data[DATA_PROGRAMS]
|
||||
if p["uid"] == self._rainmachine_entity_id
|
||||
]
|
||||
"""Update the state."""
|
||||
super().update_from_latest_data()
|
||||
|
||||
self._is_on = bool(self._switch_data["status"])
|
||||
self._is_on = bool(self._data["status"])
|
||||
|
||||
try:
|
||||
if self._data.get("nextRun") is not None:
|
||||
next_run = datetime.strptime(
|
||||
"{} {}".format(
|
||||
self._switch_data["nextRun"], self._switch_data["startTime"]
|
||||
),
|
||||
f"{self._data['nextRun']} {self._data['startTime']}",
|
||||
"%Y-%m-%d %H:%M",
|
||||
).isoformat()
|
||||
except ValueError:
|
||||
else:
|
||||
next_run = None
|
||||
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_ID: self._switch_data["uid"],
|
||||
ATTR_ID: self._uid,
|
||||
ATTR_NEXT_RUN: next_run,
|
||||
ATTR_SOAK: self._switch_data.get("soak"),
|
||||
ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["status"]],
|
||||
ATTR_SOAK: self.coordinator.data[self._uid].get("soak"),
|
||||
ATTR_STATUS: RUN_STATUS_MAP[self.coordinator.data[self._uid]["status"]],
|
||||
ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
|
||||
}
|
||||
)
|
||||
|
@ -243,68 +249,41 @@ class RainMachineProgram(RainMachineSwitch):
|
|||
class RainMachineZone(RainMachineSwitch):
|
||||
"""A RainMachine zone."""
|
||||
|
||||
def __init__(self, rainmachine, switch_data):
|
||||
"""Initialize a RainMachine zone."""
|
||||
super().__init__(rainmachine, switch_data)
|
||||
self._switch_type = SWITCH_TYPE_ZONE
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, ZONE_UPDATE_TOPIC, self._update_state)
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the zone off."""
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.controller.zones.stop(self._rainmachine_entity_id)
|
||||
)
|
||||
await self._async_run_switch_coroutine(self._controller.zones.stop(self._uid))
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the zone on."""
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.controller.zones.start(
|
||||
self._rainmachine_entity_id,
|
||||
self.rainmachine.config_entry.options[CONF_ZONE_RUN_TIME],
|
||||
self._controller.zones.start(
|
||||
self._uid,
|
||||
self._entry.options[CONF_ZONE_RUN_TIME],
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update info for the zone."""
|
||||
[self._switch_data] = [
|
||||
z
|
||||
for z in self.rainmachine.data[DATA_ZONES]
|
||||
if z["uid"] == self._rainmachine_entity_id
|
||||
]
|
||||
[details] = [
|
||||
z
|
||||
for z in self.rainmachine.data[DATA_ZONES_DETAILS]
|
||||
if z["uid"] == self._rainmachine_entity_id
|
||||
]
|
||||
"""Update the state."""
|
||||
super().update_from_latest_data()
|
||||
|
||||
self._is_on = bool(self._switch_data["state"])
|
||||
self._is_on = bool(self._data["state"])
|
||||
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["state"]],
|
||||
ATTR_AREA: details.get("waterSense").get("area"),
|
||||
ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"),
|
||||
ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"),
|
||||
ATTR_ID: self._switch_data["uid"],
|
||||
ATTR_NO_CYCLES: self._switch_data.get("noOfCycles"),
|
||||
ATTR_PRECIP_RATE: details.get("waterSense").get("precipitationRate"),
|
||||
ATTR_RESTRICTIONS: self._switch_data.get("restriction"),
|
||||
ATTR_SLOPE: SLOPE_TYPE_MAP.get(details.get("slope")),
|
||||
ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(details.get("sun")),
|
||||
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(details.get("group_id")),
|
||||
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(details.get("sun")),
|
||||
ATTR_TIME_REMAINING: self._switch_data.get("remaining"),
|
||||
ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._switch_data.get("type")),
|
||||
ATTR_STATUS: RUN_STATUS_MAP[self._data["state"]],
|
||||
ATTR_AREA: self._data.get("waterSense").get("area"),
|
||||
ATTR_CURRENT_CYCLE: self._data.get("cycle"),
|
||||
ATTR_FIELD_CAPACITY: self._data.get("waterSense").get("fieldCapacity"),
|
||||
ATTR_ID: self._data["uid"],
|
||||
ATTR_NO_CYCLES: self._data.get("noOfCycles"),
|
||||
ATTR_PRECIP_RATE: self._data.get("waterSense").get("precipitationRate"),
|
||||
ATTR_RESTRICTIONS: self._data.get("restriction"),
|
||||
ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._data.get("slope")),
|
||||
ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("sun")),
|
||||
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(self._data.get("group_id")),
|
||||
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(self._data.get("sun")),
|
||||
ATTR_TIME_REMAINING: self._data.get("remaining"),
|
||||
ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._data.get("type")),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1924,7 +1924,7 @@ raspyrfm-client==1.2.8
|
|||
recollect-waste==1.0.1
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==2.1.0
|
||||
regenmaschine==3.0.0
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.0
|
||||
|
|
|
@ -920,7 +920,7 @@ pyzerproc==0.2.5
|
|||
rachiopy==1.0.3
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==2.1.0
|
||||
regenmaschine==3.0.0
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.0
|
||||
|
|
Loading…
Reference in New Issue