Handle None values in Xiaomi Miio integration (#58880)

* Initial commit

* Improve _handle_coordinator_update()

* Fix entity_description define

* Improve sensor & binary_sensor platforms

* Log None value

* Use coordinator variable

* Improve log strings

* Filter attributes with None values

* Add hasattr condition

* Update homeassistant/components/xiaomi_miio/sensor.py

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

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Maciej Bieniek 2021-11-01 17:40:15 +01:00 committed by GitHub
parent f7b63e9fd7
commit 43ccf1d967
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 72 additions and 86 deletions

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import logging
from typing import Callable from typing import Callable
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -12,6 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC
from homeassistant.core import callback
from . import VacuumCoordinatorDataAttributes from . import VacuumCoordinatorDataAttributes
from .const import ( from .const import (
@ -30,6 +32,8 @@ from .const import (
) )
from .device import XiaomiCoordinatedMiioEntity from .device import XiaomiCoordinatedMiioEntity
_LOGGER = logging.getLogger(__name__)
ATTR_NO_WATER = "no_water" ATTR_NO_WATER = "no_water"
ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached" ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached"
ATTR_WATER_TANK_DETACHED = "water_tank_detached" ATTR_WATER_TANK_DETACHED = "water_tank_detached"
@ -108,21 +112,29 @@ HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED)
def _setup_vacuum_sensors(hass, config_entry, async_add_entities): def _setup_vacuum_sensors(hass, config_entry, async_add_entities):
"""Only vacuums with mop should have binary sensor registered.""" """Only vacuums with mop should have binary sensor registered."""
if config_entry.data[CONF_MODEL] not in MODELS_VACUUM_WITH_MOP: if config_entry.data[CONF_MODEL] not in MODELS_VACUUM_WITH_MOP:
return return
device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE)
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
entities = [] entities = []
for sensor, description in VACUUM_SENSORS.items(): for sensor, description in VACUUM_SENSORS.items():
parent_key_data = getattr(coordinator.data, description.parent_key)
if getattr(parent_key_data, description.key, None) is None:
_LOGGER.debug(
"It seems the %s does not support the %s as the initial value is None",
config_entry.data[CONF_MODEL],
description.key,
)
continue
entities.append( entities.append(
XiaomiGenericBinarySensor( XiaomiGenericBinarySensor(
f"{config_entry.title} {description.name}", f"{config_entry.title} {description.name}",
device, device,
config_entry, config_entry,
f"{sensor}_{config_entry.unique_id}", f"{sensor}_{config_entry.unique_id}",
hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], coordinator,
description, description,
) )
) )
@ -168,18 +180,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity): class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity):
"""Representation of a Xiaomi Humidifier binary sensor.""" """Representation of a Xiaomi Humidifier binary sensor."""
entity_description: XiaomiMiioBinarySensorDescription
def __init__(self, name, device, entry, unique_id, coordinator, description): def __init__(self, name, device, entry, unique_id, coordinator, description):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(name, device, entry, unique_id, coordinator) super().__init__(name, device, entry, unique_id, coordinator)
self.entity_description: XiaomiMiioBinarySensorDescription = description self.entity_description = description
self._attr_entity_registry_enabled_default = ( self._attr_entity_registry_enabled_default = (
description.entity_registry_enabled_default description.entity_registry_enabled_default
) )
self._attr_is_on = self._determine_native_value()
@property @callback
def is_on(self): def _handle_coordinator_update(self) -> None:
"""Return true if the binary sensor is on.""" self._attr_is_on = self._determine_native_value()
super()._handle_coordinator_update()
def _determine_native_value(self):
"""Determine native value."""
if self.entity_description.parent_key is not None: if self.entity_description.parent_key is not None:
return self._extract_value_from_attribute( return self._extract_value_from_attribute(
getattr(self.coordinator.data, self.entity_description.parent_key), getattr(self.coordinator.data, self.entity_description.parent_key),

View File

@ -169,17 +169,8 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity):
return cls._parse_datetime_datetime(value) return cls._parse_datetime_datetime(value)
if isinstance(value, datetime.timedelta): if isinstance(value, datetime.timedelta):
return cls._parse_time_delta(value) return cls._parse_time_delta(value)
if isinstance(value, float): if value is None:
return value _LOGGER.debug("Attribute %s is None, this is unexpected", attribute)
if isinstance(value, int):
return value
_LOGGER.warning(
"Could not determine how to parse state value of type %s for state %s and attribute %s",
type(value),
type(state),
attribute,
)
return value return value

View File

@ -1,7 +1,6 @@
"""Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier."""
from abc import abstractmethod from abc import abstractmethod
import asyncio import asyncio
from enum import Enum
import logging import logging
import math import math
@ -363,14 +362,6 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice):
return None return None
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self):
"""Fetch state from the device.""" """Fetch state from the device."""

View File

@ -1,5 +1,4 @@
"""Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity.""" """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity."""
from enum import Enum
import logging import logging
import math import math
@ -124,14 +123,6 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity):
"""Return true if device is on.""" """Return true if device is on."""
return self._state return self._state
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
@property @property
def mode(self): def mode(self):
"""Get the current mode.""" """Get the current mode."""

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES
@ -285,14 +284,6 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity):
return False return False
return super().available return super().available
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
async def async_set_value(self, value): async def async_set_value(self, value):
"""Set an option of the miio device.""" """Set an option of the miio device."""
method = getattr(self, self.entity_description.method) method = getattr(self, self.entity_description.method)

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from miio.airfresh import LedBrightness as AirfreshLedBrightness from miio.airfresh import LedBrightness as AirfreshLedBrightness
from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness
@ -126,14 +125,6 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity):
self._attr_options = list(description.options) self._attr_options = list(description.options)
self.entity_description = description self.entity_description = description
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
class XiaomiAirHumidifierSelector(XiaomiSelector): class XiaomiAirHumidifierSelector(XiaomiSelector):
"""Representation of a Xiaomi Air Humidifier selector.""" """Representation of a Xiaomi Air Humidifier selector."""
@ -153,7 +144,7 @@ class XiaomiAirHumidifierSelector(XiaomiSelector):
) )
# Sometimes (quite rarely) the device returns None as the LED brightness so we # Sometimes (quite rarely) the device returns None as the LED brightness so we
# check that the value is not None before updating the state. # check that the value is not None before updating the state.
if led_brightness: if led_brightness is not None:
self._current_led_brightness = led_brightness self._current_led_brightness = led_brightness
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -48,6 +48,7 @@ from homeassistant.const import (
TIME_SECONDS, TIME_SECONDS,
VOLUME_CUBIC_METERS, VOLUME_CUBIC_METERS,
) )
from homeassistant.core import callback
from . import VacuumCoordinatorDataAttributes from . import VacuumCoordinatorDataAttributes
from .const import ( from .const import (
@ -529,17 +530,27 @@ VACUUM_SENSORS = {
def _setup_vacuum_sensors(hass, config_entry, async_add_entities): def _setup_vacuum_sensors(hass, config_entry, async_add_entities):
"""Set up the Xiaomi vacuum sensors."""
device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE)
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
entities = [] entities = []
for sensor, description in VACUUM_SENSORS.items(): for sensor, description in VACUUM_SENSORS.items():
parent_key_data = getattr(coordinator.data, description.parent_key)
if getattr(parent_key_data, description.key, None) is None:
_LOGGER.debug(
"It seems the %s does not support the %s as the initial value is None",
config_entry.data[CONF_MODEL],
description.key,
)
continue
entities.append( entities.append(
XiaomiGenericSensor( XiaomiGenericSensor(
f"{config_entry.title} {description.name}", f"{config_entry.title} {description.name}",
device, device,
config_entry, config_entry,
f"{sensor}_{config_entry.unique_id}", f"{sensor}_{config_entry.unique_id}",
hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], coordinator,
description, description,
) )
) )
@ -637,23 +648,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity):
"""Representation of a Xiaomi generic sensor.""" """Representation of a Xiaomi generic sensor."""
def __init__( entity_description: XiaomiMiioSensorDescription
self,
name, def __init__(self, name, device, entry, unique_id, coordinator, description):
device,
entry,
unique_id,
coordinator,
description: XiaomiMiioSensorDescription,
):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(name, device, entry, unique_id, coordinator) super().__init__(name, device, entry, unique_id, coordinator)
self.entity_description = description
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
self.entity_description: XiaomiMiioSensorDescription = description self._attr_native_value = self._determine_native_value()
self._attr_extra_state_attributes = self._extract_attributes(coordinator.data)
@property @callback
def native_value(self): def _extract_attributes(self, data):
"""Return the state of the device.""" """Return state attributes with valid values."""
return {
attr: value
for attr in self.entity_description.attributes
if hasattr(data, attr)
and (value := self._extract_value_from_attribute(data, attr)) is not None
}
@callback
def _handle_coordinator_update(self):
"""Fetch state from the device."""
native_value = self._determine_native_value()
# Sometimes (quite rarely) the device returns None as the sensor value so we
# check that the value is not None before updating the state.
if native_value is not None:
self._attr_native_value = native_value
self._attr_extra_state_attributes = self._extract_attributes(
self.coordinator.data
)
self.async_write_ha_state()
def _determine_native_value(self):
"""Determine native value."""
if self.entity_description.parent_key is not None: if self.entity_description.parent_key is not None:
return self._extract_value_from_attribute( return self._extract_value_from_attribute(
getattr(self.coordinator.data, self.entity_description.parent_key), getattr(self.coordinator.data, self.entity_description.parent_key),
@ -664,15 +693,6 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity):
self.coordinator.data, self.entity_description.key self.coordinator.data, self.entity_description.key
) )
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {
attr: self._extract_value_from_attribute(self.coordinator.data, attr)
for attr in self.entity_description.attributes
if hasattr(self.coordinator.data, attr)
}
class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity):
"""Representation of a Xiaomi Air Quality Monitor.""" """Representation of a Xiaomi Air Quality Monitor."""

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from functools import partial from functools import partial
import logging import logging
@ -474,14 +473,6 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity):
return False return False
return super().available return super().available
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
async def async_turn_on(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None:
"""Turn on an option of the miio device.""" """Turn on an option of the miio device."""
method = getattr(self, self.entity_description.method_on) method = getattr(self, self.entity_description.method_on)