core/homeassistant/components/xiaomi_miio/air_quality.py

273 lines
8.7 KiB
Python
Raw Normal View History

"""Support for Xiaomi Mi Air Quality Monitor (PM2.5)."""
from collections.abc import Callable
import logging
from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException
from homeassistant.components.air_quality import AirQualityEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
MODEL_AIRQUALITYMONITOR_B1,
MODEL_AIRQUALITYMONITOR_CGDN1,
MODEL_AIRQUALITYMONITOR_S1,
MODEL_AIRQUALITYMONITOR_V1,
)
from .device import XiaomiMiioEntity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor"
ATTR_CO2E = "carbon_dioxide_equivalent"
ATTR_TVOC = "total_volatile_organic_compounds"
ATTR_TEMP = "temperature"
ATTR_HUM = "humidity"
PROP_TO_ATTR = {
"carbon_dioxide_equivalent": ATTR_CO2E,
"total_volatile_organic_compounds": ATTR_TVOC,
"temperature": ATTR_TEMP,
"humidity": ATTR_HUM,
}
class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity):
"""Air Quality class for Xiaomi cgllc.airmonitor.b1 device."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the entity."""
super().__init__(name, device, entry, unique_id)
self._icon = "mdi:cloud"
self._available = None
self._air_quality_index = None
self._carbon_dioxide = None
self._carbon_dioxide_equivalent = None
self._particulate_matter_2_5 = None
self._total_volatile_organic_compounds = None
self._temperature = None
self._humidity = None
async def async_update(self):
"""Fetch state from the miio device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._carbon_dioxide_equivalent = state.co2e
self._particulate_matter_2_5 = round(state.pm25, 1)
self._total_volatile_organic_compounds = round(state.tvoc, 3)
self._temperature = round(state.temperature, 2)
self._humidity = round(state.humidity, 2)
self._available = True
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def icon(self):
"""Return the icon to use for device if any."""
return self._icon
@property
def available(self):
"""Return true when state is known."""
return self._available
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
return self._air_quality_index
@property
def carbon_dioxide(self):
"""Return the CO2 (carbon dioxide) level."""
return self._carbon_dioxide
@property
def carbon_dioxide_equivalent(self):
"""Return the CO2e (carbon dioxide equivalent) level."""
return self._carbon_dioxide_equivalent
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self._particulate_matter_2_5
@property
def total_volatile_organic_compounds(self):
"""Return the total volatile organic compounds."""
return self._total_volatile_organic_compounds
@property
def temperature(self):
"""Return the current temperature."""
return self._temperature
@property
def humidity(self):
"""Return the current humidity."""
return self._humidity
@property
def extra_state_attributes(self):
"""Return the state attributes."""
data = {}
for prop, attr in PROP_TO_ATTR.items():
2021-10-31 18:01:16 +00:00
if (value := getattr(self, prop)) is not None:
data[attr] = value
return data
class AirMonitorS1(AirMonitorB1):
"""Air Quality class for Xiaomi cgllc.airmonitor.s1 device."""
async def async_update(self):
"""Fetch state from the miio device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._carbon_dioxide = state.co2
self._particulate_matter_2_5 = state.pm25
self._total_volatile_organic_compounds = state.tvoc
self._temperature = state.temperature
self._humidity = state.humidity
self._available = True
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
class AirMonitorV1(AirMonitorB1):
"""Air Quality class for Xiaomi cgllc.airmonitor.s1 device."""
async def async_update(self):
"""Fetch state from the miio device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._air_quality_index = state.aqi
self._available = True
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return None
class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity):
"""Air Quality class for cgllc.airm.cgdn1 device."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the entity."""
super().__init__(name, device, entry, unique_id)
self._icon = "mdi:cloud"
self._available = None
self._carbon_dioxide = None
self._particulate_matter_2_5 = None
self._particulate_matter_10 = None
async def async_update(self):
"""Fetch state from the miio device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._carbon_dioxide = state.co2
self._particulate_matter_2_5 = round(state.pm25, 1)
self._particulate_matter_10 = round(state.pm10, 1)
self._available = True
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def icon(self):
"""Return the icon to use for device if any."""
return self._icon
@property
def available(self):
"""Return true when state is known."""
return self._available
@property
def carbon_dioxide(self):
"""Return the CO2 (carbon dioxide) level."""
return self._carbon_dioxide
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self._particulate_matter_2_5
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return self._particulate_matter_10
DEVICE_MAP: dict[str, dict[str, Callable]] = {
MODEL_AIRQUALITYMONITOR_S1: {
"device_class": AirQualityMonitor,
"entity_class": AirMonitorS1,
},
MODEL_AIRQUALITYMONITOR_B1: {
"device_class": AirQualityMonitor,
"entity_class": AirMonitorB1,
},
MODEL_AIRQUALITYMONITOR_V1: {
"device_class": AirQualityMonitor,
"entity_class": AirMonitorV1,
},
MODEL_AIRQUALITYMONITOR_CGDN1: {
"device_class": lambda host, token, model: AirQualityMonitorCGDN1(host, token),
"entity_class": AirMonitorCGDN1,
},
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Xiaomi Air Quality from a config entry."""
entities = []
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
name = config_entry.title
model = config_entry.data[CONF_MODEL]
unique_id = config_entry.unique_id
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
if model in DEVICE_MAP:
device_entry = DEVICE_MAP[model]
entities.append(
device_entry["entity_class"](
name,
device_entry["device_class"](host, token, model=model),
config_entry,
unique_id,
)
)
else:
_LOGGER.warning("AirQualityMonitor model '%s' is not yet supported", model)
async_add_entities(entities, update_before_add=True)