first commit

This commit is contained in:
Your Name
2024-01-11 15:10:51 +03:00
commit eddf07c652
602 changed files with 67154 additions and 0 deletions
@@ -0,0 +1,433 @@
"""Implement the Yandex Smart Home float properties."""
from __future__ import annotations
from abc import ABC
import logging
from typing import Any
from homeassistant.components import air_quality, climate, fan, humidifier, light, sensor, switch, water_heater
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
ATTR_VOLTAGE,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
ELECTRIC_CURRENT_MILLIAMPERE,
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from . import const
from .const import ERR_NOT_SUPPORTED_IN_CURRENT_MODE, STATE_EMPTY, STATE_NONE, STATE_NONE_UI
from .error import SmartHomeError
from .prop import PREFIX_PROPERTIES, AbstractProperty, register_property
_LOGGER = logging.getLogger(__name__)
PROPERTY_FLOAT = PREFIX_PROPERTIES + 'float'
PRESSURE_UNITS_TO_YANDEX_UNITS = {
const.PRESSURE_UNIT_PASCAL: 'unit.pressure.pascal',
const.PRESSURE_UNIT_MMHG: 'unit.pressure.mmhg',
const.PRESSURE_UNIT_ATM: 'unit.pressure.atm',
const.PRESSURE_UNIT_BAR: 'unit.pressure.bar'
}
PRESSURE_TO_PASCAL = {
const.PRESSURE_UNIT_PASCAL: 1,
const.PRESSURE_UNIT_HECTOPASCAL: 100,
const.PRESSURE_UNIT_KILOPASCAL: 1000,
const.PRESSURE_UNIT_MEGAPASCAL: 1000000,
const.PRESSURE_UNIT_MMHG: 133.322,
const.PRESSURE_UNIT_ATM: 101325,
const.PRESSURE_UNIT_BAR: 100000,
const.PRESSURE_UNIT_MBAR: 0.01
}
PRESSURE_FROM_PASCAL = {
const.PRESSURE_UNIT_PASCAL: 1,
const.PRESSURE_UNIT_MMHG: 0.00750061575846,
const.PRESSURE_UNIT_ATM: 0.00000986923266716,
const.PRESSURE_UNIT_BAR: 0.00001,
}
# average molecular weight of tVOC = 110 g/mol
TVOC_CONCENTRATION_TO_MCG_M3 = {
CONCENTRATION_PARTS_PER_BILLION: 4.49629381184,
CONCENTRATION_PARTS_PER_MILLION: 4496.29381184,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: 35.3146667215,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1000,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1
}
PROPERTY_FLOAT_INSTANCE_TO_UNITS = {
const.FLOAT_INSTANCE_HUMIDITY: 'unit.percent',
const.FLOAT_INSTANCE_TEMPERATURE: 'unit.temperature.celsius',
const.FLOAT_INSTANCE_PRESSURE: PRESSURE_UNITS_TO_YANDEX_UNITS[const.PRESSURE_UNIT_MMHG],
const.FLOAT_INSTANCE_WATER_LEVEL: 'unit.percent',
const.FLOAT_INSTANCE_CO2_LEVEL: 'unit.ppm',
const.FLOAT_INSTANCE_POWER: 'unit.watt',
const.FLOAT_INSTANCE_VOLTAGE: 'unit.volt',
const.FLOAT_INSTANCE_BATTERY_LEVEL: 'unit.percent',
const.FLOAT_INSTANCE_AMPERAGE: 'unit.ampere',
const.FLOAT_INSTANCE_ILLUMINATION: 'unit.illumination.lux',
const.FLOAT_INSTANCE_TVOC: 'unit.density.mcg_m3',
const.FLOAT_INSTANCE_PM1_DENSITY: 'unit.density.mcg_m3',
const.FLOAT_INSTANCE_PM2_5_DENSITY: 'unit.density.mcg_m3',
const.FLOAT_INSTANCE_PM10_DENSITY: 'unit.density.mcg_m3'
}
PROPERTY_FLOAT_VALUE_LIMITS = {
'unit.percent': (0, 100),
'unit.pressure.atm': (0, None),
'unit.pressure.pascal': (0, None),
'unit.pressure.bar': (0, None),
'unit.pressure.mmhg': (0, None),
'unit.ppm': (0, None),
'unit.watt': (0, None),
'unit.volt': (0, None),
'unit.ampere': (0, None),
'unit.illumination.lux': (0, None),
'unit.density.mcg_m3': (0, None),
}
class FloatProperty(AbstractProperty, ABC):
type = PROPERTY_FLOAT
def parameters(self) -> dict[str, Any]:
return {
'instance': self.instance,
'unit': self.unit
}
@property
def unit(self) -> str:
return PROPERTY_FLOAT_INSTANCE_TO_UNITS[self.instance]
def float_value(self, value: Any) -> float | None:
if str(value).lower() in (STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_NONE, STATE_NONE_UI, STATE_EMPTY):
return None
try:
value = float(value)
except (ValueError, TypeError):
raise SmartHomeError(
ERR_NOT_SUPPORTED_IN_CURRENT_MODE,
f'Unsupported value {value!r} for instance {self.instance} of {self.state.entity_id}'
)
if self.unit in PROPERTY_FLOAT_VALUE_LIMITS:
lower_limit, upper_limit = PROPERTY_FLOAT_VALUE_LIMITS.get(self.unit, (None, None))
if (lower_limit is not None and value < lower_limit) or \
(upper_limit is not None and value > upper_limit):
return 0
return value
def convert_value(self, value: Any, from_unit: str | None) -> float | None:
float_value = self.float_value(value)
if float_value is None:
return None
if self.instance == const.FLOAT_INSTANCE_PRESSURE:
if from_unit not in PRESSURE_TO_PASCAL:
raise SmartHomeError(
ERR_NOT_SUPPORTED_IN_CURRENT_MODE,
f'Unsupported pressure unit "{from_unit}" '
f'for {self.instance} instance of {self.state.entity_id}'
)
return round(
float_value * PRESSURE_TO_PASCAL[from_unit] *
PRESSURE_FROM_PASCAL[self.config.pressure_unit], 2
)
elif self.instance == const.FLOAT_INSTANCE_TVOC:
return round(float_value * TVOC_CONCENTRATION_TO_MCG_M3.get(from_unit, 1), 2)
elif self.instance == const.FLOAT_INSTANCE_AMPERAGE and from_unit == ELECTRIC_CURRENT_MILLIAMPERE:
return float_value / 1000
return float_value
@register_property
class TemperatureProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_TEMPERATURE
def supported(self) -> bool:
if self.state.domain == sensor.DOMAIN:
return self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
elif self.state.domain == air_quality.DOMAIN:
return self.state.attributes.get(climate.ATTR_TEMPERATURE) is not None
elif self.state.domain in (climate.DOMAIN, fan.DOMAIN, humidifier.DOMAIN, water_heater.DOMAIN):
return self.state.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) is not None
return False
def get_value(self) -> float | None:
if self.state.domain == sensor.DOMAIN:
return self.float_value(self.state.state)
elif self.state.domain == air_quality.DOMAIN:
return self.float_value(self.state.attributes.get(climate.ATTR_TEMPERATURE))
elif self.state.domain in (climate.DOMAIN, fan.DOMAIN, humidifier.DOMAIN, water_heater.DOMAIN):
return self.float_value(self.state.attributes.get(climate.ATTR_CURRENT_TEMPERATURE))
@register_property
class HumidityProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_HUMIDITY
def supported(self) -> bool:
if self.state.domain == sensor.DOMAIN:
return self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY
elif self.state.domain == air_quality.DOMAIN:
return self.state.attributes.get(climate.ATTR_HUMIDITY) is not None
elif self.state.domain in (climate.DOMAIN, fan.DOMAIN, humidifier.DOMAIN):
return self.state.attributes.get(climate.ATTR_CURRENT_HUMIDITY) is not None
return False
def get_value(self) -> float | None:
if self.state.domain == sensor.DOMAIN:
return self.float_value(self.state.state)
elif self.state.domain == air_quality.DOMAIN:
return self.float_value(self.state.attributes.get(climate.ATTR_HUMIDITY))
elif self.state.domain in (climate.DOMAIN, fan.DOMAIN, humidifier.DOMAIN):
return self.float_value(self.state.attributes.get(climate.ATTR_CURRENT_HUMIDITY))
@register_property
class PressureProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_PRESSURE
def supported(self) -> bool:
if self.state.domain == sensor.DOMAIN:
if self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE:
if self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) in PRESSURE_TO_PASCAL:
return True
return False
def parameters(self) -> dict[str, Any]:
return {
'instance': self.instance,
'unit': self.unit,
}
@property
def unit(self) -> str:
return PRESSURE_UNITS_TO_YANDEX_UNITS[self.config.pressure_unit]
def get_value(self) -> float | None:
return self.convert_value(self.state.state, self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT))
@register_property
class IlluminanceProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_ILLUMINATION
def supported(self) -> bool:
if self.state.domain in (sensor.DOMAIN, light.DOMAIN, fan.DOMAIN):
if self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ILLUMINANCE:
return True
return const.ATTR_ILLUMINANCE in self.state.attributes
return False
def get_value(self) -> float | None:
if self.state.domain == sensor.DOMAIN:
return self.float_value(self.state.state)
return self.float_value(self.state.attributes.get(const.ATTR_ILLUMINANCE))
@register_property
class WaterLevelProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_WATER_LEVEL
def supported(self) -> bool:
if self.state.domain in (fan.DOMAIN, humidifier.DOMAIN):
return const.ATTR_WATER_LEVEL in self.state.attributes
return False
def get_value(self) -> float | None:
return self.float_value(self.state.attributes.get(const.ATTR_WATER_LEVEL))
@register_property
class CO2Property(FloatProperty):
instance = const.FLOAT_INSTANCE_CO2_LEVEL
def supported(self) -> bool:
if self.state.domain == sensor.DOMAIN:
return self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO2
elif self.state.domain in (air_quality.DOMAIN, fan.DOMAIN):
return air_quality.ATTR_CO2 in self.state.attributes
return False
def get_value(self) -> float | None:
if self.state.domain == sensor.DOMAIN:
return self.float_value(self.state.state)
return self.float_value(self.state.attributes.get(air_quality.ATTR_CO2))
@register_property
class PM1Property(FloatProperty):
instance = const.FLOAT_INSTANCE_PM1_DENSITY
def supported(self) -> bool:
if self.state.domain == air_quality.DOMAIN:
return air_quality.ATTR_PM_0_1 in self.state.attributes
return False
def get_value(self) -> float | None:
return self.float_value(self.state.attributes.get(air_quality.ATTR_PM_0_1))
@register_property
class PM25Property(FloatProperty):
instance = const.FLOAT_INSTANCE_PM2_5_DENSITY
def supported(self) -> bool:
if self.state.domain == air_quality.DOMAIN:
return air_quality.ATTR_PM_2_5 in self.state.attributes
return False
def get_value(self) -> float | None:
return self.float_value(self.state.attributes.get(air_quality.ATTR_PM_2_5))
@register_property
class PM10Property(FloatProperty):
instance = const.FLOAT_INSTANCE_PM10_DENSITY
def supported(self) -> bool:
if self.state.domain == air_quality.DOMAIN:
return air_quality.ATTR_PM_10 in self.state.attributes
return False
def get_value(self) -> float | None:
return self.float_value(self.state.attributes.get(air_quality.ATTR_PM_10))
@register_property
class TVOCProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_TVOC
def supported(self) -> bool:
if self.state.domain == air_quality.DOMAIN:
return const.ATTR_TVOC in self.state.attributes
return False
def get_value(self) -> float | None:
return self.convert_value(
self.state.attributes.get(const.ATTR_TVOC),
self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # may be missing
)
@register_property
class VoltageProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_VOLTAGE
def supported(self) -> bool:
if self.state.domain == sensor.DOMAIN:
return self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_VOLTAGE
elif self.state.domain in (switch.DOMAIN, light.DOMAIN):
return ATTR_VOLTAGE in self.state.attributes
return False
def get_value(self) -> float | None:
if self.state.domain == sensor.DOMAIN:
return self.float_value(self.state.state)
return self.float_value(self.state.attributes.get(ATTR_VOLTAGE))
@register_property
class CurrentProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_AMPERAGE
def supported(self) -> bool:
if self.state.domain == sensor.DOMAIN:
return self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT
elif self.state.domain in (switch.DOMAIN, light.DOMAIN):
return const.ATTR_CURRENT in self.state.attributes
return False
def get_value(self) -> float | None:
if self.state.domain == sensor.DOMAIN:
value = self.state.state
else:
value = self.state.attributes.get(const.ATTR_CURRENT)
return self.convert_value(value, self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT))
@register_property
class PowerProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_POWER
def supported(self) -> bool:
if self.state.domain == sensor.DOMAIN:
return self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
elif self.state.domain == switch.DOMAIN:
for attribute in [const.ATTR_POWER, const.ATTR_LOAD_POWER, const.ATTR_CURRENT_CONSUMPTION]:
if attribute in self.state.attributes:
return True
return False
def get_value(self) -> float | None:
value = None
if self.state.domain == sensor.DOMAIN:
value = self.state.state
elif self.state.domain == switch.DOMAIN:
for attribute in [const.ATTR_POWER, const.ATTR_LOAD_POWER, const.ATTR_CURRENT_CONSUMPTION]:
if attribute in self.state.attributes:
value = self.state.attributes[attribute]
return self.float_value(value)
@register_property
class BatteryLevelProperty(FloatProperty):
instance = const.FLOAT_INSTANCE_BATTERY_LEVEL
def supported(self) -> bool:
if self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY and \
self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE:
return True
return ATTR_BATTERY_LEVEL in self.state.attributes
def get_value(self) -> float | None:
value = None
if self.state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY:
value = self.state.state
elif ATTR_BATTERY_LEVEL in self.state.attributes:
value = self.state.attributes.get(ATTR_BATTERY_LEVEL)
if value in [const.STATE_LOW, const.STATE_CHARGING]:
return 0
return self.float_value(value)