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
This commit is contained in:
Milan Meulemans 2021-04-17 15:41:45 +02:00 committed by GitHub
parent 006bcde435
commit ad967cfebb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 116 additions and 89 deletions

View File

@ -3,8 +3,8 @@ import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from aiohttp.client_exceptions import ClientConnectorError import aiohttp
from pyrituals import Account from pyrituals import Account, Diffuser
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -19,6 +19,7 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"]
EMPTY_CREDENTIALS = "" EMPTY_CREDENTIALS = ""
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=30) UPDATE_INTERVAL = timedelta(seconds=30)
@ -30,38 +31,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
try: try:
account_devices = await account.get_devices() account_devices = await account.get_devices()
except ClientConnectorError as ex: except aiohttp.ClientError as err:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from err
hublots = []
devices = {}
for device in account_devices:
hublot = device.data[HUB][HUBLOT]
hublots.append(hublot)
devices[hublot] = device
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
COORDINATORS: {}, COORDINATORS: {},
DEVICES: devices, DEVICES: {},
} }
for hublot in hublots: for device in account_devices:
device = hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] hublot = device.data[HUB][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,
)
coordinator = RitualsPerufmeGenieDataUpdateCoordinator(hass, device)
await coordinator.async_refresh() await coordinator.async_refresh()
hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device
hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator
for platform in PLATFORMS: for platform in PLATFORMS:
@ -86,3 +70,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok 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

View File

@ -1,8 +1,15 @@
"""Support for Rituals Perfume Genie binary sensors.""" """Support for Rituals Perfume Genie binary sensors."""
from typing import Callable
from pyrituals import Diffuser
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY_CHARGING,
BinarySensorEntity, 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 .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID
from .entity import SENSORS, DiffuserEntity from .entity import SENSORS, DiffuserEntity
@ -11,7 +18,9 @@ CHARGING_SUFFIX = " Battery Charging"
BATTERY_CHARGING_ID = 21 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.""" """Set up the diffuser binary sensors."""
diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES]
coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] 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): class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity):
"""Representation of a diffuser battery charging binary sensor.""" """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.""" """Initialize the battery charging binary sensor."""
super().__init__(diffuser, coordinator, CHARGING_SUFFIX) super().__init__(diffuser, coordinator, CHARGING_SUFFIX)
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return the state of the battery charging binary sensor.""" """Return the state of the battery charging binary sensor."""
return bool( return self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID
self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID
)
@property @property
def device_class(self): def device_class(self) -> str:
"""Return the device class of the battery charging binary sensor.""" """Return the device class of the battery charging binary sensor."""
return DEVICE_CLASS_BATTERY_CHARGING return DEVICE_CLASS_BATTERY_CHARGING

View File

@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD 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 homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACCOUNT_HASH, DOMAIN from .const import ACCOUNT_HASH, DOMAIN
@ -27,7 +28,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 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.""" """Handle the initial step."""
if user_input is None: if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)

View File

@ -1,19 +1,31 @@
"""Base class for Rituals Perfume Genie diffuser entity.""" """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 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" MANUFACTURER = "Rituals Cosmetics"
MODEL = "Diffuser" MODEL = "The Perfume Genie"
MODEL2 = "The Perfume Genie 2.0"
ROOMNAME = "roomnamec" ROOMNAME = "roomnamec"
STATUS = "status"
VERSION = "versionc" VERSION = "versionc"
AVAILABLE_STATE = 1
class DiffuserEntity(CoordinatorEntity): class DiffuserEntity(CoordinatorEntity):
"""Representation of a diffuser entity.""" """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.""" """Init from config, hookup diffuser and coordinator."""
super().__init__(coordinator) super().__init__(coordinator)
self._diffuser = diffuser self._diffuser = diffuser
@ -22,22 +34,29 @@ class DiffuserEntity(CoordinatorEntity):
self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME] self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME]
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique ID of the entity.""" """Return the unique ID of the entity."""
return f"{self._hublot}{self._entity_suffix}" return f"{self._hublot}{self._entity_suffix}"
@property @property
def name(self): def name(self) -> str:
"""Return the name of the entity.""" """Return the name of the entity."""
return f"{self._hubname}{self._entity_suffix}" return f"{self._hubname}{self._entity_suffix}"
@property @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 information about the device."""
return { return {
"name": self._hubname, "name": self._hubname,
"identifiers": {(DOMAIN, self._hublot)}, "identifiers": {(DOMAIN, self._hublot)},
"manufacturer": MANUFACTURER, "manufacturer": MANUFACTURER,
"model": MODEL, "model": MODEL if BATTERY in self._diffuser.data[HUB][SENSORS] else MODEL2,
"sw_version": self.coordinator.data[HUB][SENSORS][VERSION], "sw_version": self.coordinator.data[HUB][SENSORS][VERSION],
} }

View File

@ -1,10 +1,16 @@
"""Support for Rituals Perfume Genie sensors.""" """Support for Rituals Perfume Genie sensors."""
from typing import Callable
from pyrituals import Diffuser
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
PERCENTAGE, PERCENTAGE,
) )
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID, SENSORS from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID, SENSORS
from .entity import DiffuserEntity from .entity import DiffuserEntity
@ -26,7 +32,9 @@ WIFI_SUFFIX = " Wifi"
ATTR_SIGNAL_STRENGTH = "signal_strength" 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.""" """Set up the diffuser sensors."""
diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES]
coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] 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): class DiffuserPerfumeSensor(DiffuserEntity):
"""Representation of a diffuser perfume sensor.""" """Representation of a diffuser perfume sensor."""
def __init__(self, diffuser, coordinator): def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None:
"""Initialize the perfume sensor.""" """Initialize the perfume sensor."""
super().__init__(diffuser, coordinator, PERFUME_SUFFIX) super().__init__(diffuser, coordinator, PERFUME_SUFFIX)
@property @property
def icon(self): def icon(self) -> str:
"""Return the perfume sensor icon.""" """Return the perfume sensor icon."""
if self.coordinator.data[HUB][SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: if self.coordinator.data[HUB][SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID:
return "mdi:tag-remove" return "mdi:tag-remove"
return "mdi:tag-text" return "mdi:tag-text"
@property @property
def state(self): def state(self) -> str:
"""Return the state of the perfume sensor.""" """Return the state of the perfume sensor."""
return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE] return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE]
@ -65,19 +73,19 @@ class DiffuserPerfumeSensor(DiffuserEntity):
class DiffuserFillSensor(DiffuserEntity): class DiffuserFillSensor(DiffuserEntity):
"""Representation of a diffuser fill sensor.""" """Representation of a diffuser fill sensor."""
def __init__(self, diffuser, coordinator): def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None:
"""Initialize the fill sensor.""" """Initialize the fill sensor."""
super().__init__(diffuser, coordinator, FILL_SUFFIX) super().__init__(diffuser, coordinator, FILL_SUFFIX)
@property @property
def icon(self): def icon(self) -> str:
"""Return the fill sensor icon.""" """Return the fill sensor icon."""
if self.coordinator.data[HUB][SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: if self.coordinator.data[HUB][SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID:
return "mdi:beaker-question" return "mdi:beaker-question"
return "mdi:beaker" return "mdi:beaker"
@property @property
def state(self): def state(self) -> str:
"""Return the state of the fill sensor.""" """Return the state of the fill sensor."""
return self.coordinator.data[HUB][SENSORS][FILL][TITLE] return self.coordinator.data[HUB][SENSORS][FILL][TITLE]
@ -85,12 +93,12 @@ class DiffuserFillSensor(DiffuserEntity):
class DiffuserBatterySensor(DiffuserEntity): class DiffuserBatterySensor(DiffuserEntity):
"""Representation of a diffuser battery sensor.""" """Representation of a diffuser battery sensor."""
def __init__(self, diffuser, coordinator): def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None:
"""Initialize the battery sensor.""" """Initialize the battery sensor."""
super().__init__(diffuser, coordinator, BATTERY_SUFFIX) super().__init__(diffuser, coordinator, BATTERY_SUFFIX)
@property @property
def state(self): def state(self) -> int:
"""Return the state of the battery sensor.""" """Return the state of the battery sensor."""
# Use ICON because TITLE may change in the future. # Use ICON because TITLE may change in the future.
# ICON filename does not match the image. # ICON filename does not match the image.
@ -103,19 +111,12 @@ class DiffuserBatterySensor(DiffuserEntity):
}[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]]
@property @property
def device_class(self): def device_class(self) -> str:
"""Return the class of the battery sensor.""" """Return the class of the battery sensor."""
return DEVICE_CLASS_BATTERY return DEVICE_CLASS_BATTERY
@property @property
def extra_state_attributes(self): def unit_of_measurement(self) -> str:
"""Return the battery state attributes."""
return {
ATTR_BATTERY_LEVEL: self.coordinator.data[HUB][SENSORS][BATTERY][TITLE],
}
@property
def unit_of_measurement(self):
"""Return the battery unit of measurement.""" """Return the battery unit of measurement."""
return PERCENTAGE return PERCENTAGE
@ -123,12 +124,12 @@ class DiffuserBatterySensor(DiffuserEntity):
class DiffuserWifiSensor(DiffuserEntity): class DiffuserWifiSensor(DiffuserEntity):
"""Representation of a diffuser wifi sensor.""" """Representation of a diffuser wifi sensor."""
def __init__(self, diffuser, coordinator): def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None:
"""Initialize the wifi sensor.""" """Initialize the wifi sensor."""
super().__init__(diffuser, coordinator, WIFI_SUFFIX) super().__init__(diffuser, coordinator, WIFI_SUFFIX)
@property @property
def state(self): def state(self) -> int:
"""Return the state of the wifi sensor.""" """Return the state of the wifi sensor."""
# Use ICON because TITLE may change in the future. # Use ICON because TITLE may change in the future.
return { return {
@ -139,18 +140,11 @@ class DiffuserWifiSensor(DiffuserEntity):
}[self.coordinator.data[HUB][SENSORS][WIFI][ICON]] }[self.coordinator.data[HUB][SENSORS][WIFI][ICON]]
@property @property
def device_class(self): def device_class(self) -> str:
"""Return the class of the wifi sensor.""" """Return the class of the wifi sensor."""
return DEVICE_CLASS_SIGNAL_STRENGTH return DEVICE_CLASS_SIGNAL_STRENGTH
@property @property
def extra_state_attributes(self): def unit_of_measurement(self) -> str:
"""Return the wifi state attributes."""
return {
ATTR_SIGNAL_STRENGTH: self.coordinator.data[HUB][SENSORS][WIFI][TITLE],
}
@property
def unit_of_measurement(self):
"""Return the wifi unit of measurement.""" """Return the wifi unit of measurement."""
return PERCENTAGE return PERCENTAGE

View File

@ -1,20 +1,28 @@
"""Support for Rituals Perfume Genie switches.""" """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.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 .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, HUB
from .entity import DiffuserEntity from .entity import DiffuserEntity
STATUS = "status"
FAN = "fanc" FAN = "fanc"
SPEED = "speedc" SPEED = "speedc"
ROOM = "roomc" ROOM = "roomc"
ON_STATE = "1" 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.""" """Set up the diffuser switch."""
diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES]
coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] 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): class DiffuserSwitch(SwitchEntity, DiffuserEntity):
"""Representation of a diffuser switch.""" """Representation of a diffuser switch."""
def __init__(self, diffuser, coordinator): def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None:
"""Initialize the diffuser switch.""" """Initialize the diffuser switch."""
super().__init__(diffuser, coordinator, "") super().__init__(diffuser, coordinator, "")
self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE
@property @property
def available(self): def icon(self) -> str:
"""Return if the device is available."""
return self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE
@property
def icon(self):
"""Return the icon of the device.""" """Return the icon of the device."""
return "mdi:fan" return "mdi:fan"
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes.""" """Return the device state attributes."""
attributes = { attributes = {
"fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED], "fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED],
@ -54,24 +57,24 @@ class DiffuserSwitch(SwitchEntity, DiffuserEntity):
return attributes return attributes
@property @property
def is_on(self): def is_on(self) -> bool:
"""If the device is currently on or off.""" """If the device is currently on or off."""
return self._is_on return self._is_on
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
await self._diffuser.turn_on() await self._diffuser.turn_on()
self._is_on = True 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.""" """Turn the device off."""
await self._diffuser.turn_off() await self._diffuser.turn_off()
self._is_on = False self._is_on = False
self.schedule_update_ha_state() self.async_write_ha_state()
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE
self.async_write_ha_state() self.async_write_ha_state()