From ad967cfebb875cbedf1930490a49041d5a6a6cf7 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 17 Apr 2021 15:41:45 +0200 Subject: [PATCH] Rituals Perfume Genie improvements (#49277) * Rituals Perfume Genie integration improvements * Add return type FlowResultDict to async_step_user * Rollback async_update_data * Add return type to DiffuserEntity init * check super().available too * Merge iterations * Use RitualsPerufmeGenieDataUpdateCoordinator --- .../rituals_perfume_genie/__init__.py | 55 ++++++++++--------- .../rituals_perfume_genie/binary_sensor.py | 21 ++++--- .../rituals_perfume_genie/config_flow.py | 3 +- .../rituals_perfume_genie/entity.py | 33 ++++++++--- .../rituals_perfume_genie/sensor.py | 54 ++++++++---------- .../rituals_perfume_genie/switch.py | 39 +++++++------ 6 files changed, 116 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 93e5619f446..3cc5c29d369 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -3,8 +3,8 @@ import asyncio from datetime import timedelta import logging -from aiohttp.client_exceptions import ClientConnectorError -from pyrituals import Account +import aiohttp +from pyrituals import Account, Diffuser from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,6 +19,7 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] EMPTY_CREDENTIALS = "" _LOGGER = logging.getLogger(__name__) + UPDATE_INTERVAL = timedelta(seconds=30) @@ -30,38 +31,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: account_devices = await account.get_devices() - except ClientConnectorError as ex: - raise ConfigEntryNotReady from ex - - hublots = [] - devices = {} - for device in account_devices: - hublot = device.data[HUB][HUBLOT] - hublots.append(hublot) - devices[hublot] = device + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATORS: {}, - DEVICES: devices, + DEVICES: {}, } - for hublot in hublots: - device = hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] - - async def async_update_data(): - await device.update_data() - return device.data - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{DOMAIN}-{hublot}", - update_method=async_update_data, - update_interval=UPDATE_INTERVAL, - ) + for device in account_devices: + hublot = device.data[HUB][HUBLOT] + coordinator = RitualsPerufmeGenieDataUpdateCoordinator(hass, device) await coordinator.async_refresh() + hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator for platform in PLATFORMS: @@ -86,3 +70,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class RitualsPerufmeGenieDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Rituals Perufme Genie device data from single endpoint.""" + + def __init__(self, hass: HomeAssistant, device: Diffuser): + """Initialize global Rituals Perufme Genie data updater.""" + self._device = device + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}-{device.data[HUB][HUBLOT]}", + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> dict: + """Fetch data from Rituals.""" + await self._device.update_data() + return self._device.data diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index 39c8cb8415a..a7c6732cb13 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -1,8 +1,15 @@ """Support for Rituals Perfume Genie binary sensors.""" +from typing import Callable + +from pyrituals import Diffuser + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID from .entity import SENSORS, DiffuserEntity @@ -11,7 +18,9 @@ CHARGING_SUFFIX = " Battery Charging" BATTERY_CHARGING_ID = 21 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the diffuser binary sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] @@ -27,18 +36,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): """Representation of a diffuser battery charging binary sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the battery charging binary sensor.""" super().__init__(diffuser, coordinator, CHARGING_SUFFIX) @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the battery charging binary sensor.""" - return bool( - self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID - ) + return self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the battery charging binary sensor.""" return DEVICE_CLASS_BATTERY_CHARGING diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 7bd75cdbbc0..4c46cf09d55 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACCOUNT_HASH, DOMAIN @@ -27,7 +28,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResultDict: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 4f89856ad08..a3b4f568bc5 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -1,19 +1,31 @@ """Base class for Rituals Perfume Genie diffuser entity.""" +from __future__ import annotations + +from typing import Any + +from pyrituals import Diffuser + from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTES, DOMAIN, HUB, HUBLOT, SENSORS +from .const import ATTRIBUTES, BATTERY, DOMAIN, HUB, HUBLOT, SENSORS MANUFACTURER = "Rituals Cosmetics" -MODEL = "Diffuser" +MODEL = "The Perfume Genie" +MODEL2 = "The Perfume Genie 2.0" ROOMNAME = "roomnamec" +STATUS = "status" VERSION = "versionc" +AVAILABLE_STATE = 1 + class DiffuserEntity(CoordinatorEntity): """Representation of a diffuser entity.""" - def __init__(self, diffuser, coordinator, entity_suffix): + def __init__( + self, diffuser: Diffuser, coordinator: CoordinatorEntity, entity_suffix: str + ) -> None: """Init from config, hookup diffuser and coordinator.""" super().__init__(coordinator) self._diffuser = diffuser @@ -22,22 +34,29 @@ class DiffuserEntity(CoordinatorEntity): self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME] @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the entity.""" return f"{self._hublot}{self._entity_suffix}" @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return f"{self._hubname}{self._entity_suffix}" @property - def device_info(self): + def available(self) -> bool: + """Return if the entity is available.""" + return ( + super().available and self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE + ) + + @property + def device_info(self) -> dict[str, Any]: """Return information about the device.""" return { "name": self._hubname, "identifiers": {(DOMAIN, self._hublot)}, "manufacturer": MANUFACTURER, - "model": MODEL, + "model": MODEL if BATTERY in self._diffuser.data[HUB][SENSORS] else MODEL2, "sw_version": self.coordinator.data[HUB][SENSORS][VERSION], } diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 87c2da21bc7..acdb2331e71 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -1,10 +1,16 @@ """Support for Rituals Perfume Genie sensors.""" +from typing import Callable + +from pyrituals import Diffuser + +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, PERCENTAGE, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID, SENSORS from .entity import DiffuserEntity @@ -26,7 +32,9 @@ WIFI_SUFFIX = " Wifi" ATTR_SIGNAL_STRENGTH = "signal_strength" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the diffuser sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] @@ -45,19 +53,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DiffuserPerfumeSensor(DiffuserEntity): """Representation of a diffuser perfume sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the perfume sensor.""" super().__init__(diffuser, coordinator, PERFUME_SUFFIX) @property - def icon(self): + def icon(self) -> str: """Return the perfume sensor icon.""" if self.coordinator.data[HUB][SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: return "mdi:tag-remove" return "mdi:tag-text" @property - def state(self): + def state(self) -> str: """Return the state of the perfume sensor.""" return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE] @@ -65,19 +73,19 @@ class DiffuserPerfumeSensor(DiffuserEntity): class DiffuserFillSensor(DiffuserEntity): """Representation of a diffuser fill sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the fill sensor.""" super().__init__(diffuser, coordinator, FILL_SUFFIX) @property - def icon(self): + def icon(self) -> str: """Return the fill sensor icon.""" if self.coordinator.data[HUB][SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: return "mdi:beaker-question" return "mdi:beaker" @property - def state(self): + def state(self) -> str: """Return the state of the fill sensor.""" return self.coordinator.data[HUB][SENSORS][FILL][TITLE] @@ -85,12 +93,12 @@ class DiffuserFillSensor(DiffuserEntity): class DiffuserBatterySensor(DiffuserEntity): """Representation of a diffuser battery sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the battery sensor.""" super().__init__(diffuser, coordinator, BATTERY_SUFFIX) @property - def state(self): + def state(self) -> int: """Return the state of the battery sensor.""" # Use ICON because TITLE may change in the future. # ICON filename does not match the image. @@ -103,19 +111,12 @@ class DiffuserBatterySensor(DiffuserEntity): }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] @property - def device_class(self): + def device_class(self) -> str: """Return the class of the battery sensor.""" return DEVICE_CLASS_BATTERY @property - def extra_state_attributes(self): - """Return the battery state attributes.""" - return { - ATTR_BATTERY_LEVEL: self.coordinator.data[HUB][SENSORS][BATTERY][TITLE], - } - - @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the battery unit of measurement.""" return PERCENTAGE @@ -123,12 +124,12 @@ class DiffuserBatterySensor(DiffuserEntity): class DiffuserWifiSensor(DiffuserEntity): """Representation of a diffuser wifi sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the wifi sensor.""" super().__init__(diffuser, coordinator, WIFI_SUFFIX) @property - def state(self): + def state(self) -> int: """Return the state of the wifi sensor.""" # Use ICON because TITLE may change in the future. return { @@ -139,18 +140,11 @@ class DiffuserWifiSensor(DiffuserEntity): }[self.coordinator.data[HUB][SENSORS][WIFI][ICON]] @property - def device_class(self): + def device_class(self) -> str: """Return the class of the wifi sensor.""" return DEVICE_CLASS_SIGNAL_STRENGTH @property - def extra_state_attributes(self): - """Return the wifi state attributes.""" - return { - ATTR_SIGNAL_STRENGTH: self.coordinator.data[HUB][SENSORS][WIFI][TITLE], - } - - @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the wifi unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index d1fff166f6e..1328a18d766 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,20 +1,28 @@ """Support for Rituals Perfume Genie switches.""" +from __future__ import annotations + +from typing import Any, Callable + +from pyrituals import Diffuser + from homeassistant.components.switch import SwitchEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, HUB from .entity import DiffuserEntity -STATUS = "status" FAN = "fanc" SPEED = "speedc" ROOM = "roomc" ON_STATE = "1" -AVAILABLE_STATE = 1 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the diffuser switch.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] @@ -29,23 +37,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the diffuser switch.""" super().__init__(diffuser, coordinator, "") self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE @property - def available(self): - """Return if the device is available.""" - return self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE - - @property - def icon(self): + def icon(self) -> str: """Return the icon of the device.""" return "mdi:fan" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = { "fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED], @@ -54,24 +57,24 @@ class DiffuserSwitch(SwitchEntity, DiffuserEntity): return attributes @property - def is_on(self): + def is_on(self) -> bool: """If the device is currently on or off.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._diffuser.turn_on() self._is_on = True - self.schedule_update_ha_state() + self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._diffuser.turn_off() self._is_on = False - self.schedule_update_ha_state() + self.async_write_ha_state() @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE self.async_write_ha_state()