Add Rituals Perfume Genie sensor platform (#48270)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Milan Meulemans 2021-04-11 22:36:44 +02:00 committed by GitHub
parent b86bba246a
commit 71e0e42792
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 289 additions and 73 deletions

View File

@ -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

View File

@ -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(

View File

@ -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"

View File

@ -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],
}

View File

@ -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

View File

@ -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()

View File

@ -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