diff --git a/.coveragerc b/.coveragerc index 3d126dfd23b..982db1eeade 100644 --- a/.coveragerc +++ b/.coveragerc @@ -826,6 +826,8 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py + homeassistant/components/rituals_perfume_genie/entity.py + homeassistant/components/rituals_perfume_genie/sensor.py homeassistant/components/rituals_perfume_genie/switch.py homeassistant/components/rituals_perfume_genie/__init__.py homeassistant/components/rocketchat/notify.py diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index f2fd13a9ef4..610700e8fe5 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -1,5 +1,6 @@ """The Rituals Perfume Genie integration.""" import asyncio +from datetime import timedelta import logging from aiohttp.client_exceptions import ClientConnectorError @@ -9,19 +10,16 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ACCOUNT_HASH, DOMAIN +from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUB, HUBLOT -_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["switch", "sensor"] EMPTY_CREDENTIALS = "" -PLATFORMS = ["switch"] - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Rituals Perfume Genie component.""" - return True +_LOGGER = logging.getLogger(__name__) +UPDATE_INTERVAL = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -31,11 +29,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)} try: - await account.get_devices() + account_devices = await account.get_devices() except ClientConnectorError as ex: raise ConfigEntryNotReady from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = account + 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] = { + COORDINATORS: {}, + 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, + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator for platform in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index 075d79ec8de..16189c8335e 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -1,5 +1,10 @@ """Constants for the Rituals Perfume Genie integration.""" - DOMAIN = "rituals_perfume_genie" +COORDINATORS = "coordinators" +DEVICES = "devices" + ACCOUNT_HASH = "account_hash" +ATTRIBUTES = "attributes" +HUB = "hub" +HUBLOT = "hublot" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py new file mode 100644 index 00000000000..ba8f583d042 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -0,0 +1,44 @@ +"""Base class for Rituals Perfume Genie diffuser entity.""" +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTES, DOMAIN, HUB, HUBLOT + +MANUFACTURER = "Rituals Cosmetics" +MODEL = "Diffuser" + +SENSORS = "sensors" +ROOMNAME = "roomnamec" +VERSION = "versionc" + + +class DiffuserEntity(CoordinatorEntity): + """Representation of a diffuser entity.""" + + def __init__(self, diffuser, coordinator, entity_suffix): + """Init from config, hookup diffuser and coordinator.""" + super().__init__(coordinator) + self._diffuser = diffuser + self._entity_suffix = entity_suffix + self._hublot = self.coordinator.data[HUB][HUBLOT] + self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME] + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"{self._hublot}{self._entity_suffix}" + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._hubname}{self._entity_suffix}" + + @property + def device_info(self): + """Return information about the device.""" + return { + "name": self._hubname, + "identifiers": {(DOMAIN, self._hublot)}, + "manufacturer": MANUFACTURER, + "model": MODEL, + "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 new file mode 100644 index 00000000000..4a3ac34cc58 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -0,0 +1,168 @@ +"""Support for Rituals Perfume Genie sensors.""" +from homeassistant.const import ( + ATTR_BATTERY_CHARGING, + ATTR_BATTERY_LEVEL, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + PERCENTAGE, +) + +from .const import COORDINATORS, DEVICES, DOMAIN, HUB +from .entity import SENSORS, DiffuserEntity + +ID = "id" +TITLE = "title" +ICON = "icon" +WIFI = "wific" +BATTERY = "battc" +PERFUME = "rfidc" +FILL = "fillc" + +BATTERY_CHARGING_ID = 21 +PERFUME_NO_CARTRIDGE_ID = 19 +FILL_NO_CARTRIDGE_ID = 12 + +BATTERY_SUFFIX = " Battery" +PERFUME_SUFFIX = " Perfume" +FILL_SUFFIX = " Fill" +WIFI_SUFFIX = " Wifi" + +ATTR_SIGNAL_STRENGTH = "signal_strength" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the diffuser sensors.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities = [] + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserPerfumeSensor(diffuser, coordinator)) + entities.append(DiffuserFillSensor(diffuser, coordinator)) + entities.append(DiffuserWifiSensor(diffuser, coordinator)) + if BATTERY in diffuser.data[HUB][SENSORS]: + entities.append(DiffuserBatterySensor(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserPerfumeSensor(DiffuserEntity): + """Representation of a diffuser perfume sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the perfume sensor.""" + super().__init__(diffuser, coordinator, PERFUME_SUFFIX) + + @property + def icon(self): + """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): + """Return the state of the perfume sensor.""" + return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE] + + +class DiffuserFillSensor(DiffuserEntity): + """Representation of a diffuser fill sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the fill sensor.""" + super().__init__(diffuser, coordinator, FILL_SUFFIX) + + @property + def icon(self): + """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): + """Return the state of the fill sensor.""" + return self.coordinator.data[HUB][SENSORS][FILL][TITLE] + + +class DiffuserBatterySensor(DiffuserEntity): + """Representation of a diffuser battery sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the battery sensor.""" + super().__init__(diffuser, coordinator, BATTERY_SUFFIX) + + @property + def state(self): + """Return the state of the battery sensor.""" + # Use ICON because TITLE may change in the future. + # ICON filename does not match the image. + return { + "battery-charge.png": 100, + "battery-full.png": 100, + "battery-75.png": 50, + "battery-50.png": 25, + "battery-low.png": 10, + }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] + + @property + def _charging(self): + """Return battery charging state.""" + return bool( + self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID + ) + + @property + def device_class(self): + """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], + ATTR_BATTERY_CHARGING: self._charging, + } + + @property + def unit_of_measurement(self): + """Return the battery unit of measurement.""" + return PERCENTAGE + + +class DiffuserWifiSensor(DiffuserEntity): + """Representation of a diffuser wifi sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the wifi sensor.""" + super().__init__(diffuser, coordinator, WIFI_SUFFIX) + + @property + def state(self): + """Return the state of the wifi sensor.""" + # Use ICON because TITLE may change in the future. + return { + "icon-signal.png": 100, + "icon-signal-75.png": 70, + "icon-signal-low.png": 25, + "icon-signal-0.png": 0, + }[self.coordinator.data[HUB][SENSORS][WIFI][ICON]] + + @property + def device_class(self): + """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): + """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 bc8e2b5e175..d1fff166f6e 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,104 +1,77 @@ """Support for Rituals Perfume Genie switches.""" -from datetime import timedelta -import logging - -import aiohttp - from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback -from .const import DOMAIN +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, HUB +from .entity import DiffuserEntity -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=30) +STATUS = "status" +FAN = "fanc" +SPEED = "speedc" +ROOM = "roomc" ON_STATE = "1" AVAILABLE_STATE = 1 -MANUFACTURER = "Rituals Cosmetics" -MODEL = "Diffuser" -ICON = "mdi:fan" - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the diffuser switch.""" - account = hass.data[DOMAIN][config_entry.entry_id] - diffusers = await account.get_devices() - + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] entities = [] - for diffuser in diffusers: - entities.append(DiffuserSwitch(diffuser)) + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserSwitch(diffuser, coordinator)) - async_add_entities(entities, True) + async_add_entities(entities) -class DiffuserSwitch(SwitchEntity): +class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" - def __init__(self, diffuser): - """Initialize the switch.""" - self._diffuser = diffuser - self._available = True - - @property - def device_info(self): - """Return information about the device.""" - return { - "name": self._diffuser.data["hub"]["attributes"]["roomnamec"], - "identifiers": {(DOMAIN, self._diffuser.data["hub"]["hublot"])}, - "manufacturer": MANUFACTURER, - "model": MODEL, - "sw_version": self._diffuser.data["hub"]["sensors"]["versionc"], - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._diffuser.data["hub"]["hublot"] + def __init__(self, diffuser, coordinator): + """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._available - - @property - def name(self): - """Return the name of the device.""" - return self._diffuser.data["hub"]["attributes"]["roomnamec"] + return self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE @property def icon(self): """Return the icon of the device.""" - return ICON + return "mdi:fan" @property def extra_state_attributes(self): """Return the device state attributes.""" attributes = { - "fan_speed": self._diffuser.data["hub"]["attributes"]["speedc"], - "room_size": self._diffuser.data["hub"]["attributes"]["roomc"], + "fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED], + "room_size": self.coordinator.data[HUB][ATTRIBUTES][ROOM], } return attributes @property def is_on(self): """If the device is currently on or off.""" - return self._diffuser.data["hub"]["attributes"]["fanc"] == ON_STATE + return self._is_on async def async_turn_on(self, **kwargs): """Turn the device on.""" await self._diffuser.turn_on() + self._is_on = True + self.schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" await self._diffuser.turn_off() + self._is_on = False + self.schedule_update_ha_state() - async def async_update(self): - """Update the data of the device.""" - try: - await self._diffuser.update_data() - except aiohttp.ClientError: - self._available = False - _LOGGER.error("Unable to retrieve data from rituals.sense-company.com") - else: - self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE + @callback + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" + self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE + self.async_write_ha_state() diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index 92c3e15c247..e5c64dd54c9 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -32,8 +32,6 @@ async def test_form(hass): "homeassistant.components.rituals_perfume_genie.config_flow.Account", side_effect=_mock_account, ), patch( - "homeassistant.components.rituals_perfume_genie.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.rituals_perfume_genie.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -49,7 +47,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == TEST_EMAIL assert isinstance(result2["data"][ACCOUNT_HASH], str) - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1