Refactor Xiaomi vacuum to sensors (#54990)

* Refactor Xiaomi vacuum with sensors.

This is the first step into refactoring xiaomi vacuum attributes into sensors.
What is missing are some switches and binary sensors etc.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Use generic sensor for Xiaomi vacuum sensors.

By using HA coordinator, the generic Xiaomi sensor class can be used with these
coordinators to get the status sensors from vacuum. This also means now that
sensors are available as soon as HA starts, which is a nice plus.

Now the only reason to create a subclass of the generic sensors is when custom
value parsing needs to be done.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Working vacuum sensors via 1 coordinator.

Vacuum needs a custom coordinator to ensure that it fetches all the needed data
and puts it in a dict. From this dict the sensors will then get their data
accordingly.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Remove vacuum setup method in sensor

Sensor is generic enough that vacuum does not require its own setup method.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Don't auto register generic sensors.

Let the user decide which sensor is useful for them and enable them.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Remove converted attributes from xiaomi vacuum.

The attributes that have been converted so far should be removed from the vacuum
attributes list.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Fetch data from vacuum sequentially.

It seems some vacuums do not like parallel requests. The requests that came
before are ignored.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Refactor vacuum sensors to its own container.

By moving vacuum sensors to its own container, there is no more key collisions.
This in turns means that there is need for the split hack to ensure key names
are correct.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* fixup! fix bad rebase.

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Fix sensor naming and default registration.

Use proper names for sensors, no need to include from which device status it
came.

Registration of the sensor by default has been parameterised. If the param is
not set, the sensor is not registered.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Make vacuum platform also use coordinator for its data.

By using the coordinator for data in vacuum platfrom, removes the cases where
request gets ignored during the case where the requests are done concurrently by
separate platforms.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Add binary sensor for vacuum

Add binary sensor for waterbox, mop, and water shortage.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Added proper icons to sensors.

https://github.com/home-assistant/core/issues/51361

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Refactor sensors to use dataclass.

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Make vacuum use coordinator for its data.

This commit also insures that the binary sensors are only registered for devices
that include a mop.

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Minor refactoring

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Update data from coordinator after running command.

This is is to have a faster status change when executing a command like changing
fan speeds. If a manual refresh is not triggered. Worst case scenario it will
take 10s for the new fan speed to be reported in HA.

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Refresh within coroutine is ok.

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Move logging to _handle_coordinator_update

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Use internal state attribute.

Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com>

* Fix vacuum typing

* Fix tests constants

* Fix vacuum inconsistent return values

* Fix vacuum state update

* Fix vacuum tests

* Remove impossible cases

* Parametrize custom services test

* Move goto test

* Move clean segment test

* Move clean single segment test

* Test service pause

* Include vacuum in coverage

* Delint

* Fix vacuum sensor dict collision.

This also prevents collision for unique id. As the key is used to construct
unique ids.

* Use f strings as dict keys

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Kevin Hellemun 2021-10-07 18:30:17 +02:00 committed by GitHub
parent c651cff6a0
commit dc5e4392ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 664 additions and 364 deletions

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import logging
from miio import AirQualityMonitor, DeviceException
@ -22,6 +21,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@ -37,15 +37,18 @@ from homeassistant.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
PRESSURE_HPA,
TEMP_CELSIUS,
TIME_HOURS,
TIME_SECONDS,
VOLUME_CUBIC_METERS,
)
from . import VacuumCoordinatorDataAttributes
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
@ -75,6 +78,7 @@ from .const import (
MODELS_HUMIDIFIER_MJJSQ,
MODELS_PURIFIER_MIIO,
MODELS_PURIFIER_MIOT,
MODELS_VACUUM,
)
from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity
from .gateway import XiaomiGatewayDevice
@ -109,6 +113,20 @@ ATTR_PRESSURE = "pressure"
ATTR_PURIFY_VOLUME = "purify_volume"
ATTR_SENSOR_STATE = "sensor_state"
ATTR_WATER_LEVEL = "water_level"
ATTR_DND_START = "start"
ATTR_DND_END = "end"
ATTR_LAST_CLEAN_TIME = "duration"
ATTR_LAST_CLEAN_AREA = "area"
ATTR_LAST_CLEAN_START = "start"
ATTR_LAST_CLEAN_END = "end"
ATTR_CLEAN_HISTORY_TOTAL_DURATION = "total_duration"
ATTR_CLEAN_HISTORY_TOTAL_AREA = "total_area"
ATTR_CLEAN_HISTORY_COUNT = "count"
ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT = "dust_collection_count"
ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT = "main_brush_left"
ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT = "side_brush_left"
ATTR_CONSUMABLE_STATUS_FILTER_LEFT = "filter_left"
ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT = "sensor_dirty_left"
@dataclass
@ -116,6 +134,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription):
"""Class that holds device specific info for a xiaomi aqara or humidifier sensor."""
attributes: tuple = ()
parent_key: str | None = None
SENSOR_TYPES = {
@ -348,6 +367,138 @@ MODEL_TO_SENSORS_MAP = {
MODEL_FAN_ZA5: FAN_ZA5_SENSORS,
}
VACUUM_SENSORS = {
f"dnd_{ATTR_DND_START}": XiaomiMiioSensorDescription(
key=ATTR_DND_START,
icon="mdi:minus-circle-off",
name="DnD Start",
device_class=DEVICE_CLASS_TIMESTAMP,
parent_key=VacuumCoordinatorDataAttributes.dnd_status,
entity_registry_enabled_default=False,
),
f"dnd_{ATTR_DND_END}": XiaomiMiioSensorDescription(
key=ATTR_DND_END,
icon="mdi:minus-circle-off",
name="DnD End",
device_class=DEVICE_CLASS_TIMESTAMP,
parent_key=VacuumCoordinatorDataAttributes.dnd_status,
entity_registry_enabled_default=False,
),
f"last_clean_{ATTR_LAST_CLEAN_START}": XiaomiMiioSensorDescription(
key=ATTR_LAST_CLEAN_START,
icon="mdi:clock-time-twelve",
name="Last Clean Start",
device_class=DEVICE_CLASS_TIMESTAMP,
parent_key=VacuumCoordinatorDataAttributes.last_clean_details,
),
f"last_clean_{ATTR_LAST_CLEAN_END}": XiaomiMiioSensorDescription(
key=ATTR_LAST_CLEAN_END,
icon="mdi:clock-time-twelve",
device_class=DEVICE_CLASS_TIMESTAMP,
parent_key=VacuumCoordinatorDataAttributes.last_clean_details,
name="Last Clean End",
),
f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription(
native_unit_of_measurement=TIME_SECONDS,
icon="mdi:timer-sand",
key=ATTR_LAST_CLEAN_TIME,
parent_key=VacuumCoordinatorDataAttributes.last_clean_details,
name="Last Clean Duration",
),
f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription(
native_unit_of_measurement=AREA_SQUARE_METERS,
icon="mdi:texture-box",
key=ATTR_LAST_CLEAN_AREA,
parent_key=VacuumCoordinatorDataAttributes.last_clean_details,
name="Last Clean Area",
),
f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription(
native_unit_of_measurement=TIME_SECONDS,
icon="mdi:timer-sand",
key=ATTR_CLEAN_HISTORY_TOTAL_DURATION,
parent_key=VacuumCoordinatorDataAttributes.clean_history_status,
name="Total duration",
entity_registry_enabled_default=False,
),
f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_AREA}": XiaomiMiioSensorDescription(
native_unit_of_measurement=AREA_SQUARE_METERS,
icon="mdi:texture-box",
key=ATTR_CLEAN_HISTORY_TOTAL_AREA,
parent_key=VacuumCoordinatorDataAttributes.clean_history_status,
name="Total Clean Area",
entity_registry_enabled_default=False,
),
f"clean_history_{ATTR_CLEAN_HISTORY_COUNT}": XiaomiMiioSensorDescription(
native_unit_of_measurement="",
icon="mdi:counter",
state_class=STATE_CLASS_TOTAL_INCREASING,
key=ATTR_CLEAN_HISTORY_COUNT,
parent_key=VacuumCoordinatorDataAttributes.clean_history_status,
name="Total Clean Count",
entity_registry_enabled_default=False,
),
f"clean_history_{ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT}": XiaomiMiioSensorDescription(
native_unit_of_measurement="",
icon="mdi:counter",
state_class="total_increasing",
key=ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT,
parent_key=VacuumCoordinatorDataAttributes.clean_history_status,
name="Total Dust Collection Count",
entity_registry_enabled_default=False,
),
f"consumable_{ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT}": XiaomiMiioSensorDescription(
native_unit_of_measurement=TIME_SECONDS,
icon="mdi:brush",
key=ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT,
parent_key=VacuumCoordinatorDataAttributes.consumable_status,
name="Main Brush Left",
entity_registry_enabled_default=False,
),
f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription(
native_unit_of_measurement=TIME_SECONDS,
icon="mdi:brush",
key=ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT,
parent_key=VacuumCoordinatorDataAttributes.consumable_status,
name="Side Brush Left",
entity_registry_enabled_default=False,
),
f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription(
native_unit_of_measurement=TIME_SECONDS,
icon="mdi:air-filter",
key=ATTR_CONSUMABLE_STATUS_FILTER_LEFT,
parent_key=VacuumCoordinatorDataAttributes.consumable_status,
name="Filter Left",
entity_registry_enabled_default=False,
),
f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription(
native_unit_of_measurement=TIME_SECONDS,
icon="mdi:eye-outline",
key=ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT,
parent_key=VacuumCoordinatorDataAttributes.consumable_status,
name="Sensor Dirty Left",
entity_registry_enabled_default=False,
),
}
def _setup_vacuum_sensors(hass, config_entry, async_add_entities):
device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE)
entities = []
for sensor, description in VACUUM_SENSORS.items():
entities.append(
XiaomiGenericSensor(
f"{config_entry.title} {description.name}",
device,
config_entry,
f"{sensor}_{config_entry.unique_id}",
hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR],
description,
)
)
async_add_entities(entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Xiaomi sensor from a config entry."""
@ -416,6 +567,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
sensors = PURIFIER_MIIO_SENSORS
elif model in MODELS_PURIFIER_MIOT:
sensors = PURIFIER_MIOT_SENSORS
elif model in MODELS_VACUUM:
return _setup_vacuum_sensors(hass, config_entry, async_add_entities)
for sensor, description in SENSOR_TYPES.items():
if sensor not in sensors:
@ -435,19 +588,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity):
"""Representation of a Xiaomi Humidifier sensor."""
"""Representation of a Xiaomi generic sensor."""
def __init__(self, name, device, entry, unique_id, coordinator, description):
def __init__(
self,
name,
device,
entry,
unique_id,
coordinator,
description: XiaomiMiioSensorDescription,
):
"""Initialize the entity."""
super().__init__(name, device, entry, unique_id, coordinator)
self._attr_name = name
self._attr_unique_id = unique_id
self.entity_description = description
self.entity_description: XiaomiMiioSensorDescription = description
@property
def native_value(self):
"""Return the state of the device."""
if self.entity_description.parent_key is not None:
return self._extract_value_from_attribute(
getattr(self.coordinator.data, self.entity_description.parent_key),
self.entity_description.key,
)
return self._extract_value_from_attribute(
self.coordinator.data, self.entity_description.key
)
@ -461,14 +626,6 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity):
if hasattr(self.coordinator.data, attr)
}
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity):
"""Representation of a Xiaomi Air Quality Monitor."""