From bba7c15d79dcb54bc91acb82262fd00db455a4d3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 6 Nov 2020 02:58:50 -0700 Subject: [PATCH] Migrate RainMachine to DataUpdateCoordinator (#42530) --- .../components/rainmachine/__init__.py | 422 ++++++++---------- .../components/rainmachine/binary_sensor.py | 153 ++++--- homeassistant/components/rainmachine/const.py | 13 +- .../components/rainmachine/manifest.json | 2 +- .../components/rainmachine/sensor.py | 122 ++--- .../components/rainmachine/switch.py | 207 ++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 445 insertions(+), 478 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 7faac5f2f60..a520772ff77 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -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 diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index e2ef8cea64b..5d141b0f008 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -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") diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index ae38f3335dc..568108e23a6 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -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" diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 07321801381..5d03155deac 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -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"] } diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index ef1b88e6489..4533397fb54 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -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"] diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index f31d6386e06..5c54000a15f 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -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")), } ) diff --git a/requirements_all.txt b/requirements_all.txt index e885ca4b9da..f09b4c96a49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a67bf3275f7..c15da3436dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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