Migrate RainMachine to DataUpdateCoordinator (#42530)

This commit is contained in:
Aaron Bach 2020-11-06 02:58:50 -07:00 committed by GitHub
parent 4e614e0f2c
commit bba7c15d79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 445 additions and 478 deletions

View File

@ -1,12 +1,14 @@
"""Support for RainMachine devices.""" """Support for RainMachine devices."""
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging from functools import partial
from regenmaschine import Client from regenmaschine import Client
from regenmaschine.controller import Controller
from regenmaschine.errors import RainMachineError from regenmaschine.errors import RainMachineError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
CONF_IP_ADDRESS, CONF_IP_ADDRESS,
@ -14,32 +16,30 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
CONF_SSL, CONF_SSL,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv 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.service import verify_domain_control
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import ( from .const import (
CONF_ZONE_RUN_TIME, CONF_ZONE_RUN_TIME,
DATA_CLIENT, DATA_CONTROLLER,
DATA_COORDINATOR,
DATA_PROGRAMS, DATA_PROGRAMS,
DATA_PROVISION_SETTINGS, DATA_PROVISION_SETTINGS,
DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_CURRENT,
DATA_RESTRICTIONS_UNIVERSAL, DATA_RESTRICTIONS_UNIVERSAL,
DATA_ZONES, DATA_ZONES,
DATA_ZONES_DETAILS,
DEFAULT_ZONE_RUN, DEFAULT_ZONE_RUN,
DOMAIN, DOMAIN,
PROGRAM_UPDATE_TOPIC, LOGGER,
SENSOR_UPDATE_TOPIC,
ZONE_UPDATE_TOPIC,
) )
_LOGGER = logging.getLogger(__name__)
CONF_PROGRAM_ID = "program_id" CONF_PROGRAM_ID = "program_id"
CONF_SECONDS = "seconds" CONF_SECONDS = "seconds"
CONF_ZONE_ID = "zone_id" CONF_ZONE_ID = "zone_id"
@ -48,8 +48,8 @@ DATA_LISTENER = "listener"
DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC"
DEFAULT_ICON = "mdi:water" DEFAULT_ICON = "mdi:water"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=60)
DEFAULT_SSL = True DEFAULT_SSL = True
DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15)
SERVICE_ALTER_PROGRAM = vol.Schema({vol.Required(CONF_PROGRAM_ID): cv.positive_int}) 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") 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.""" """Set up the RainMachine component."""
hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} hass.data[DOMAIN] = {DATA_CONTROLLER: {}, DATA_COORDINATOR: {}, DATA_LISTENER: {}}
return True 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.""" """Set up RainMachine as config entry."""
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {}
entry_updates = {} 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: # If the config entry doesn't already have a unique ID, set one:
entry_updates["unique_id"] = config_entry.data[CONF_IP_ADDRESS] entry_updates["unique_id"] = entry.data[CONF_IP_ADDRESS]
if CONF_ZONE_RUN_TIME in config_entry.data: 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 # If a zone run time exists in the config entry's data, pop it and move it to
# options: # options:
data = {**config_entry.data} data = {**entry.data}
entry_updates["data"] = data entry_updates["data"] = data
entry_updates["options"] = { entry_updates["options"] = {
**config_entry.options, **entry.options,
CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME), CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME),
} }
if entry_updates: 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) _verify_domain_control = verify_domain_control(hass, DOMAIN)
@ -108,97 +132,133 @@ async def async_setup_entry(hass, config_entry):
try: try:
await client.load_local( await client.load_local(
config_entry.data[CONF_IP_ADDRESS], entry.data[CONF_IP_ADDRESS],
config_entry.data[CONF_PASSWORD], entry.data[CONF_PASSWORD],
port=config_entry.data[CONF_PORT], port=entry.data[CONF_PORT],
ssl=config_entry.data.get(CONF_SSL, DEFAULT_SSL), ssl=entry.data.get(CONF_SSL, DEFAULT_SSL),
) )
except RainMachineError as err: except RainMachineError as err:
_LOGGER.error("An error occurred: %s", err) LOGGER.error("An error occurred: %s", err)
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
else:
# regenmaschine can load multiple controllers at once, but we only grab the one # regenmaschine can load multiple controllers at once, but we only grab the one
# we loaded above: # we loaded above:
controller = next(iter(client.controllers.values())) controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] = next(
rainmachine = RainMachine(hass, config_entry, controller) iter(client.controllers.values())
)
# Update the data object, which at this point (prior to any sensors registering async def async_update(api_category: str) -> dict:
# "interest" in the API), will focus on grabbing the latest program and zone data: """Update the appropriate API data based on a category."""
await rainmachine.async_update() try:
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = rainmachine if api_category == DATA_PROGRAMS:
return await controller.programs.all(include_inactive=True)
for component in ("binary_sensor", "sensor", "switch"): 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.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 @_verify_domain_control
async def disable_program(call): async def disable_program(call: ServiceCall):
"""Disable a program.""" """Disable a program."""
await rainmachine.controller.programs.disable(call.data[CONF_PROGRAM_ID]) await controller.programs.disable(call.data[CONF_PROGRAM_ID])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def disable_zone(call): async def disable_zone(call: ServiceCall):
"""Disable a zone.""" """Disable a zone."""
await rainmachine.controller.zones.disable(call.data[CONF_ZONE_ID]) await controller.zones.disable(call.data[CONF_ZONE_ID])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def enable_program(call): async def enable_program(call: ServiceCall):
"""Enable a program.""" """Enable a program."""
await rainmachine.controller.programs.enable(call.data[CONF_PROGRAM_ID]) await controller.programs.enable(call.data[CONF_PROGRAM_ID])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def enable_zone(call): async def enable_zone(call: ServiceCall):
"""Enable a zone.""" """Enable a zone."""
await rainmachine.controller.zones.enable(call.data[CONF_ZONE_ID]) await controller.zones.enable(call.data[CONF_ZONE_ID])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def pause_watering(call): async def pause_watering(call: ServiceCall):
"""Pause watering for a set number of seconds.""" """Pause watering for a set number of seconds."""
await rainmachine.controller.watering.pause_all(call.data[CONF_SECONDS]) await controller.watering.pause_all(call.data[CONF_SECONDS])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def start_program(call): async def start_program(call: ServiceCall):
"""Start a particular program.""" """Start a particular program."""
await rainmachine.controller.programs.start(call.data[CONF_PROGRAM_ID]) await controller.programs.start(call.data[CONF_PROGRAM_ID])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def start_zone(call): async def start_zone(call: ServiceCall):
"""Start a particular zone for a certain amount of time.""" """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] 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 @_verify_domain_control
async def stop_all(call): async def stop_all(call: ServiceCall):
"""Stop all watering.""" """Stop all watering."""
await rainmachine.controller.watering.stop_all() await controller.watering.stop_all()
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def stop_program(call): async def stop_program(call: ServiceCall):
"""Stop a program.""" """Stop a program."""
await rainmachine.controller.programs.stop(call.data[CONF_PROGRAM_ID]) await controller.programs.stop(call.data[CONF_PROGRAM_ID])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def stop_zone(call): async def stop_zone(call: ServiceCall):
"""Stop a zone.""" """Stop a zone."""
await rainmachine.controller.zones.stop(call.data[CONF_ZONE_ID]) await controller.zones.stop(call.data[CONF_ZONE_ID])
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
@_verify_domain_control @_verify_domain_control
async def unpause_watering(call): async def unpause_watering(call: ServiceCall):
"""Unpause watering.""" """Unpause watering."""
await rainmachine.controller.watering.unpause_all() await controller.watering.unpause_all()
await rainmachine.async_update_programs_and_zones() await async_update_programs_and_zones(hass, entry)
for service, method, schema in [ for service, method, schema in [
("disable_program", disable_program, SERVICE_ALTER_PROGRAM), ("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.services.async_register(DOMAIN, service, method, schema=schema)
hass.data[DOMAIN][DATA_LISTENER] = config_entry.add_update_listener( hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry)
async_reload_entry
)
return True return True
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload an OpenUV config entry.""" """Unload an RainMachine config entry."""
hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) unload_ok = all(
cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) 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() cancel_listener()
tasks = [ return unload_ok
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.""" """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: class RainMachineEntity(CoordinatorEntity):
"""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):
"""Define a generic RainMachine entity.""" """Define a generic RainMachine entity."""
def __init__(self, rainmachine): def __init__(
self, coordinator: DataUpdateCoordinator, controller: Controller
) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator)
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._controller = controller
self._device_class = None 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._name = None
self.rainmachine = rainmachine
@property @property
def device_class(self): def device_class(self) -> str:
"""Return the device class.""" """Return the device class."""
return self._device_class return self._device_class
@property @property
def device_info(self): def device_info(self) -> dict:
"""Return device registry information for this entity.""" """Return device registry information for this entity."""
return { return {
"identifiers": {(DOMAIN, self.rainmachine.controller.mac)}, "identifiers": {(DOMAIN, self._controller.mac)},
"name": self.rainmachine.controller.name, "name": self._controller.name,
"manufacturer": "RainMachine", "manufacturer": "RainMachine",
"model": ( "model": (
f"Version {self.rainmachine.controller.hardware_version} " f"Version {self._controller.hardware_version} "
f"(API: {self.rainmachine.controller.api_version})" f"(API: {self._controller.api_version})"
), ),
"sw_version": self.rainmachine.controller.software_version, "sw_version": self._controller.software_version,
} }
@property @property
@ -415,18 +349,18 @@ class RainMachineEntity(Entity):
"""Return the name of the entity.""" """Return the name of the entity."""
return self._name return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@callback @callback
def _update_state(self): def _handle_coordinator_update(self):
"""Update the state.""" """Respond to a DataUpdateCoordinator update."""
self.update_from_latest_data() self.update_from_latest_data()
self.async_write_ha_state() 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 @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the entity.""" """Update the state."""
raise NotImplementedError raise NotImplementedError

View File

@ -1,16 +1,22 @@
"""This platform provides binary sensors for key RainMachine data.""" """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.components.binary_sensor import BinarySensorEntity
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
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
from .const import ( from .const import (
DATA_CLIENT, DATA_CONTROLLER,
DATA_COORDINATOR,
DATA_PROVISION_SETTINGS, DATA_PROVISION_SETTINGS,
DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_CURRENT,
DATA_RESTRICTIONS_UNIVERSAL, DATA_RESTRICTIONS_UNIVERSAL,
DOMAIN as RAINMACHINE_DOMAIN, DOMAIN,
SENSOR_UPDATE_TOPIC,
) )
TYPE_FLOW_SENSOR = "flow_sensor" 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.""" """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( async_add_entities(
[ [
RainMachineBinarySensor( async_get_sensor(api_category)(
rainmachine, sensor_type, name, icon, enabled_by_default, api_category controller, sensor_type, name, icon, enabled_by_default
) )
for ( for (
sensor_type, sensor_type,
(name, icon, enabled_by_default, api_category), (name, icon, enabled_by_default, api_category),
) in BINARY_SENSORS.items() ) in BINARY_SENSORS.items()
], ]
) )
class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity): class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity):
"""A sensor implementation for raincloud device.""" """Define a general RainMachine binary sensor."""
def __init__( 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.""" """Initialize the sensor."""
super().__init__(rainmachine) super().__init__(coordinator, controller)
self._api_category = api_category
self._enabled_by_default = enabled_by_default self._enabled_by_default = enabled_by_default
self._icon = icon self._icon = icon
self._name = name self._name = name
@ -99,7 +133,7 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity):
self._state = None self._state = None
@property @property
def entity_registry_enabled_default(self): def entity_registry_enabled_default(self) -> bool:
"""Determine whether an entity is enabled by default.""" """Determine whether an entity is enabled by default."""
return self._enabled_by_default return self._enabled_by_default
@ -109,54 +143,61 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity):
return self._icon return self._icon
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return the status of the sensor.""" """Return the status of the sensor."""
return self._state return self._state
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity.""" """Return a unique, Home Assistant friendly identifier for this entity."""
return "{}_{}".format( return f"{self._unique_id}_{self._sensor_type}"
self.rainmachine.device_mac.replace(":", ""), 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): class CurrentRestrictionsBinarySensor(RainMachineBinarySensor):
"""Disconnect dispatcher listeners and deregister API interest.""" """Define a binary sensor that handles current restrictions data."""
super().async_will_remove_from_hass()
self.rainmachine.async_deregister_sensor_api_interest(self._api_category)
@callback @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.""" """Update the state."""
if self._sensor_type == TYPE_FLOW_SENSOR: if self._sensor_type == TYPE_FLOW_SENSOR:
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( self._state = self.coordinator.data["system"].get("useFlowSensor")
"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"]

View File

@ -1,19 +1,20 @@
"""Define constants for the SimpliSafe component.""" """Define constants for the SimpliSafe component."""
import logging
LOGGER = logging.getLogger(__package__)
DOMAIN = "rainmachine" DOMAIN = "rainmachine"
CONF_ZONE_RUN_TIME = "zone_run_time" CONF_ZONE_RUN_TIME = "zone_run_time"
DATA_CLIENT = "client" DATA_CONTROLLER = "controller"
DATA_COORDINATOR = "coordinator"
DATA_LISTENER = "listener"
DATA_PROGRAMS = "programs" DATA_PROGRAMS = "programs"
DATA_PROVISION_SETTINGS = "provision.settings" DATA_PROVISION_SETTINGS = "provision.settings"
DATA_RESTRICTIONS_CURRENT = "restrictions.current" DATA_RESTRICTIONS_CURRENT = "restrictions.current"
DATA_RESTRICTIONS_UNIVERSAL = "restrictions.universal" DATA_RESTRICTIONS_UNIVERSAL = "restrictions.universal"
DATA_ZONES = "zones" DATA_ZONES = "zones"
DATA_ZONES_DETAILS = "zones_details"
DEFAULT_PORT = 8080 DEFAULT_PORT = 8080
DEFAULT_ZONE_RUN = 60 * 10 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"

View File

@ -3,6 +3,6 @@
"name": "RainMachine", "name": "RainMachine",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine", "documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==2.1.0"], "requirements": ["regenmaschine==3.0.0"],
"codeowners": ["@bachya"] "codeowners": ["@bachya"]
} }

View File

@ -1,15 +1,21 @@
"""This platform provides support for sensor data from RainMachine.""" """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.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import RainMachineEntity from . import RainMachineEntity
from .const import ( from .const import (
DATA_CLIENT, DATA_CONTROLLER,
DATA_COORDINATOR,
DATA_PROVISION_SETTINGS, DATA_PROVISION_SETTINGS,
DATA_RESTRICTIONS_UNIVERSAL, DATA_RESTRICTIONS_UNIVERSAL,
DOMAIN as RAINMACHINE_DOMAIN, DOMAIN,
SENSOR_UPDATE_TOPIC,
) )
TYPE_FLOW_SENSOR_CLICK_M3 = "flow_sensor_clicks_cubic_meter" 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.""" """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( async_add_entities(
[ [
RainMachineSensor( async_get_sensor(api_category)(
rainmachine, controller,
sensor_type, sensor_type,
name, name,
icon, icon,
unit, unit,
device_class, device_class,
enabled_by_default, enabled_by_default,
api_category,
) )
for ( for (
sensor_type, sensor_type,
@ -86,23 +109,21 @@ async def async_setup_entry(hass, entry, async_add_entities):
class RainMachineSensor(RainMachineEntity): class RainMachineSensor(RainMachineEntity):
"""A sensor implementation for raincloud device.""" """Define a general RainMachine sensor."""
def __init__( def __init__(
self, self,
rainmachine, coordinator: DataUpdateCoordinator,
sensor_type, controller: Controller,
name, sensor_type: str,
icon, name: str,
unit, icon: str,
device_class, unit: str,
enabled_by_default, device_class: str,
api_category, enabled_by_default: bool,
): ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(rainmachine) super().__init__(coordinator, controller)
self._api_category = api_category
self._device_class = device_class self._device_class = device_class
self._enabled_by_default = enabled_by_default self._enabled_by_default = enabled_by_default
self._icon = icon self._icon = icon
@ -112,7 +133,7 @@ class RainMachineSensor(RainMachineEntity):
self._unit = unit self._unit = unit
@property @property
def entity_registry_enabled_default(self): def entity_registry_enabled_default(self) -> bool:
"""Determine whether an entity is enabled by default.""" """Determine whether an entity is enabled by default."""
return self._enabled_by_default return self._enabled_by_default
@ -129,56 +150,47 @@ class RainMachineSensor(RainMachineEntity):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity.""" """Return a unique, Home Assistant friendly identifier for this entity."""
return "{}_{}".format( return f"{self._unique_id}_{self._sensor_type}"
self.rainmachine.device_mac.replace(":", ""), self._sensor_type
)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str:
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return self._unit 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): class ProvisionSettingsSensor(RainMachineSensor):
"""Disconnect dispatcher listeners and deregister API interest.""" """Define a sensor that handles provisioning data."""
super().async_will_remove_from_hass()
self.rainmachine.async_deregister_sensor_api_interest(self._api_category)
@callback @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the sensor's state.""" """Update the state."""
if self._sensor_type == TYPE_FLOW_SENSOR_CLICK_M3: 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" "flowSensorClicksPerCubicMeter"
) )
elif self._sensor_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS: elif self._sensor_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
clicks = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( clicks = self.coordinator.data["system"].get("flowSensorWateringClicks")
"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: if clicks and clicks_per_m3:
self._state = (clicks * 1000) / clicks_per_m3 self._state = (clicks * 1000) / clicks_per_m3
else: else:
self._state = None self._state = None
elif self._sensor_type == TYPE_FLOW_SENSOR_START_INDEX: elif self._sensor_type == TYPE_FLOW_SENSOR_START_INDEX:
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( self._state = self.coordinator.data["system"].get("flowSensorStartIndex")
"flowSensorStartIndex"
)
elif self._sensor_type == TYPE_FLOW_SENSOR_WATERING_CLICKS: 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" "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"]

View File

@ -1,28 +1,27 @@
"""This component provides support for RainMachine programs and zones.""" """This component provides support for RainMachine programs and zones."""
from datetime import datetime from datetime import datetime
import logging from typing import Callable, Coroutine
from regenmaschine.controller import Controller
from regenmaschine.errors import RequestError from regenmaschine.errors import RequestError
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ID from homeassistant.const import ATTR_ID
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import RainMachineEntity from . import RainMachineEntity, async_update_programs_and_zones
from .const import ( from .const import (
CONF_ZONE_RUN_TIME, CONF_ZONE_RUN_TIME,
DATA_CLIENT, DATA_CONTROLLER,
DATA_COORDINATOR,
DATA_PROGRAMS, DATA_PROGRAMS,
DATA_ZONES, DATA_ZONES,
DATA_ZONES_DETAILS, DOMAIN,
DOMAIN as RAINMACHINE_DOMAIN, LOGGER,
PROGRAM_UPDATE_TOPIC,
ZONE_UPDATE_TOPIC,
) )
_LOGGER = logging.getLogger(__name__)
ATTR_AREA = "area" ATTR_AREA = "area"
ATTR_CS_ON = "cs_on" ATTR_CS_ON = "cs_on"
ATTR_CURRENT_CYCLE = "current_cycle" ATTR_CURRENT_CYCLE = "current_cycle"
@ -100,36 +99,56 @@ SWITCH_TYPE_PROGRAM = "program"
SWITCH_TYPE_ZONE = "zone" 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.""" """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 = [] entities = []
for program in rainmachine.data[DATA_PROGRAMS]: for uid, program in programs_coordinator.data.items():
entities.append(RainMachineProgram(rainmachine, program)) entities.append(
for zone in rainmachine.data[DATA_ZONES]: RainMachineProgram(
entities.append(RainMachineZone(rainmachine, zone)) 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): class RainMachineSwitch(RainMachineEntity, SwitchEntity):
"""A class to represent a generic RainMachine switch.""" """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.""" """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._is_on = False
self._name = switch_data["name"] self._name = name
self._switch_data = switch_data self._switch_type = type(self).__name__
self._rainmachine_entity_id = switch_data["uid"] self._uid = uid
self._switch_type = None
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return self._switch_data["active"] return self._is_active and self.coordinator.last_update_success
@property @property
def icon(self) -> str: def icon(self) -> str:
@ -144,18 +163,14 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity.""" """Return a unique, Home Assistant friendly identifier for this entity."""
return "{}_{}_{}".format( return f"{self._unique_id}_{self._switch_type}_{self._uid}"
self.rainmachine.device_mac.replace(":", ""),
self._switch_type,
self._rainmachine_entity_id,
)
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.""" """Run a coroutine to toggle the switch."""
try: try:
resp = await api_coro resp = await api_coro
except RequestError as err: except RequestError as err:
_LOGGER.error( LOGGER.error(
'Error while toggling %s "%s": %s', 'Error while toggling %s "%s": %s',
self._switch_type, self._switch_type,
self.unique_id, self.unique_id,
@ -164,7 +179,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
return return
if resp["statusCode"] != 0: if resp["statusCode"] != 0:
_LOGGER.error( LOGGER.error(
'Error while toggling %s "%s": %s', 'Error while toggling %s "%s": %s',
self._switch_type, self._switch_type,
self.unique_id, self.unique_id,
@ -172,69 +187,60 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
) )
return 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): class RainMachineProgram(RainMachineSwitch):
"""A RainMachine program.""" """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 @property
def zones(self) -> list: def zones(self) -> list:
"""Return a list of active zones associated with this program.""" """Return a list of active zones associated with this program."""
return [z for z in self._switch_data["wateringTimes"] if z["active"]] return [z for z in self._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
)
)
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn the program off.""" """Turn the program off."""
await self._async_run_switch_coroutine( 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: async def async_turn_on(self, **kwargs) -> None:
"""Turn the program on.""" """Turn the program on."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(
self.rainmachine.controller.programs.start(self._rainmachine_entity_id) self._controller.programs.start(self._uid)
) )
@callback @callback
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Update info for the program.""" """Update the state."""
[self._switch_data] = [ super().update_from_latest_data()
p
for p in self.rainmachine.data[DATA_PROGRAMS]
if p["uid"] == self._rainmachine_entity_id
]
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( next_run = datetime.strptime(
"{} {}".format( f"{self._data['nextRun']} {self._data['startTime']}",
self._switch_data["nextRun"], self._switch_data["startTime"]
),
"%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M",
).isoformat() ).isoformat()
except ValueError: else:
next_run = None next_run = None
self._attrs.update( self._attrs.update(
{ {
ATTR_ID: self._switch_data["uid"], ATTR_ID: self._uid,
ATTR_NEXT_RUN: next_run, ATTR_NEXT_RUN: next_run,
ATTR_SOAK: self._switch_data.get("soak"), ATTR_SOAK: self.coordinator.data[self._uid].get("soak"),
ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["status"]], ATTR_STATUS: RUN_STATUS_MAP[self.coordinator.data[self._uid]["status"]],
ATTR_ZONES: ", ".join(z["name"] for z in self.zones), ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
} }
) )
@ -243,68 +249,41 @@ class RainMachineProgram(RainMachineSwitch):
class RainMachineZone(RainMachineSwitch): class RainMachineZone(RainMachineSwitch):
"""A RainMachine zone.""" """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: async def async_turn_off(self, **kwargs) -> None:
"""Turn the zone off.""" """Turn the zone off."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(self._controller.zones.stop(self._uid))
self.rainmachine.controller.zones.stop(self._rainmachine_entity_id)
)
async def async_turn_on(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None:
"""Turn the zone on.""" """Turn the zone on."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(
self.rainmachine.controller.zones.start( self._controller.zones.start(
self._rainmachine_entity_id, self._uid,
self.rainmachine.config_entry.options[CONF_ZONE_RUN_TIME], self._entry.options[CONF_ZONE_RUN_TIME],
) )
) )
@callback @callback
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Update info for the zone.""" """Update the state."""
[self._switch_data] = [ super().update_from_latest_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
]
self._is_on = bool(self._switch_data["state"]) self._is_on = bool(self._data["state"])
self._attrs.update( self._attrs.update(
{ {
ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["state"]], ATTR_STATUS: RUN_STATUS_MAP[self._data["state"]],
ATTR_AREA: details.get("waterSense").get("area"), ATTR_AREA: self._data.get("waterSense").get("area"),
ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"), ATTR_CURRENT_CYCLE: self._data.get("cycle"),
ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"), ATTR_FIELD_CAPACITY: self._data.get("waterSense").get("fieldCapacity"),
ATTR_ID: self._switch_data["uid"], ATTR_ID: self._data["uid"],
ATTR_NO_CYCLES: self._switch_data.get("noOfCycles"), ATTR_NO_CYCLES: self._data.get("noOfCycles"),
ATTR_PRECIP_RATE: details.get("waterSense").get("precipitationRate"), ATTR_PRECIP_RATE: self._data.get("waterSense").get("precipitationRate"),
ATTR_RESTRICTIONS: self._switch_data.get("restriction"), ATTR_RESTRICTIONS: self._data.get("restriction"),
ATTR_SLOPE: SLOPE_TYPE_MAP.get(details.get("slope")), ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._data.get("slope")),
ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(details.get("sun")), ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("sun")),
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(details.get("group_id")), ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(self._data.get("group_id")),
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(details.get("sun")), ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(self._data.get("sun")),
ATTR_TIME_REMAINING: self._switch_data.get("remaining"), ATTR_TIME_REMAINING: self._data.get("remaining"),
ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._switch_data.get("type")), ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._data.get("type")),
} }
) )

View File

@ -1924,7 +1924,7 @@ raspyrfm-client==1.2.8
recollect-waste==1.0.1 recollect-waste==1.0.1
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==2.1.0 regenmaschine==3.0.0
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==5.0 restrictedpython==5.0

View File

@ -920,7 +920,7 @@ pyzerproc==0.2.5
rachiopy==1.0.3 rachiopy==1.0.3
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==2.1.0 regenmaschine==3.0.0
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==5.0 restrictedpython==5.0