Skip to content

Commit

Permalink
New unit conversion rules for float properties (#448, #468)
Browse files Browse the repository at this point in the history
  • Loading branch information
dext0r committed Oct 26, 2023
1 parent f767499 commit 90aa9a8
Show file tree
Hide file tree
Showing 17 changed files with 441 additions and 136 deletions.
8 changes: 5 additions & 3 deletions custom_components/yandex_smart_home/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
{
vol.Required(const.CONF_ENTITY_PROPERTY_TYPE): vol.Schema(vol.All(str, ycv.property_type)),
vol.Optional(const.CONF_ENTITY_PROPERTY_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(const.CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(const.CONF_ENTITY_PROPERTY_ENTITY): cv.entity_id,
vol.Optional(const.CONF_ENTITY_PROPERTY_ATTRIBUTE): cv.string,
vol.Optional(const.CONF_ENTITY_PROPERTY_VALUE_TEMPLATE): cv.template,
Expand Down Expand Up @@ -137,12 +138,13 @@
)


SETTINGS_SCHEMA = vol.Schema(
SETTINGS_SCHEMA = vol.All(
cv.deprecated(const.CONF_PRESSURE_UNIT),
{
vol.Optional(const.CONF_PRESSURE_UNIT): vol.Schema(vol.All(str, ycv.pressure_unit)),
vol.Optional(const.CONF_PRESSURE_UNIT): cv.string,
vol.Optional(const.CONF_BETA): cv.boolean,
vol.Optional(const.CONF_CLOUD_STREAM): cv.boolean,
}
},
)


Expand Down
34 changes: 24 additions & 10 deletions custom_components/yandex_smart_home/config_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
RangeCapabilityInstance,
ToggleCapabilityInstance,
)
from .unit_conversion import UnitOfPressure
from .unit_conversion import UnitOfPressure, UnitOfTemperature

if TYPE_CHECKING:
from homeassistant.helpers import ConfigType
Expand Down Expand Up @@ -75,6 +75,29 @@ def property_attributes(value: ConfigType) -> ConfigType:
if value_template and (entity or attribute):
raise vol.Invalid("entity/attribute and value_template are mutually exclusive")

property_type_value = value.get(const.CONF_ENTITY_PROPERTY_TYPE)
target_unit_of_measurement = value.get(const.CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT)
if target_unit_of_measurement:
try:
if property_type_value in [
FloatPropertyInstance.TEMPERATURE,
f"{PropertyInstanceType.FLOAT}.{FloatPropertyInstance.TEMPERATURE}",
]:
assert UnitOfTemperature(target_unit_of_measurement).as_property_unit
elif property_type_value in [
FloatPropertyInstance.PRESSURE,
f"{PropertyInstanceType.FLOAT}.{FloatPropertyInstance.PRESSURE}",
]:
assert UnitOfPressure(target_unit_of_measurement).as_property_unit
else:
raise ValueError
except ValueError:
raise vol.Invalid(
f"Target unit of measurement '{target_unit_of_measurement}' is not supported "
f"for {property_type_value} property, see valid values "
f"at https://docs.yaha-cloud.ru/master/devices/sensor/float/#property-target-unit-of-measurement"
)

return value


Expand Down Expand Up @@ -163,15 +186,6 @@ def device_type(value: str) -> str:
raise vol.Invalid(f"Device type '{value}' is not supported")


def pressure_unit(value: str) -> str:
try:
UnitOfPressure(value)
except ValueError:
raise vol.Invalid(f"Pressure unit {value!r} is not supported")

return value


def color_name(value: str) -> str:
try:
ColorName(value)
Expand Down
1 change: 1 addition & 0 deletions custom_components/yandex_smart_home/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
CONF_ENTITY_PROPERTY_ATTRIBUTE = "attribute"
CONF_ENTITY_PROPERTY_VALUE_TEMPLATE = "value_template"
CONF_ENTITY_PROPERTY_UNIT_OF_MEASUREMENT = "unit_of_measurement"
CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT = "target_unit_of_measurement"
CONF_ENTITY_PROPERTIES = "properties"
CONF_ENTITY_RANGE = "range"
CONF_ENTITY_RANGE_MIN = "min"
Expand Down
21 changes: 15 additions & 6 deletions custom_components/yandex_smart_home/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from typing import Any, Self, cast

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, UnitOfPressure
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entityfilter import EntityFilter
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType
Expand Down Expand Up @@ -65,6 +66,19 @@ async def async_setup(self) -> Self:
else:
self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_setup_notifiers)

if self._yaml_config.get(const.CONF_SETTINGS, {}).get(const.CONF_PRESSURE_UNIT):
ir.async_create_issue(
self._hass,
DOMAIN,
"deprecated_pressure_unit",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_pressure_unit",
learn_more_url="https://docs.yaha-cloud.ru/master/devices/sensor/float/#unit-conversion",
)
else:
ir.async_delete_issue(self._hass, DOMAIN, "deprecated_pressure_unit")

return self

async def async_unload(self) -> None:
Expand Down Expand Up @@ -118,11 +132,6 @@ def user_id(self) -> str | None:
"""Return user id for service calls (used only when cloud connection)."""
return self.entry.options.get(const.CONF_USER_ID)

@property
def pressure_unit(self) -> str:
settings = self._yaml_config.get(const.CONF_SETTINGS, {})
return str(settings.get(const.CONF_PRESSURE_UNIT) or UnitOfPressure.MMHG.value)

@property
def color_profiles(self) -> ColorProfiles:
"""Return color profiles."""
Expand Down
18 changes: 16 additions & 2 deletions custom_components/yandex_smart_home/property_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .const import (
CONF_ENTITY_PROPERTY_ATTRIBUTE,
CONF_ENTITY_PROPERTY_ENTITY,
CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT,
CONF_ENTITY_PROPERTY_TYPE,
CONF_ENTITY_PROPERTY_UNIT_OF_MEASUREMENT,
CONF_ENTITY_PROPERTY_VALUE_TEMPLATE,
Expand Down Expand Up @@ -51,6 +52,7 @@
WaterLevelPercentageProperty,
)
from .schema import PropertyType, ResponseCode
from .unit_conversion import UnitOfPressure, UnitOfTemperature

if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -202,7 +204,13 @@ def _native_unit_of_measurement(self) -> str | None:

@FLOAT_PROPERTIES_REGISTRY.register
class TemperatureCustomFloatProperty(TemperatureProperty, CustomFloatProperty):
pass
@property
def unit_of_measurement(self) -> UnitOfTemperature:
"""Return the unit the property value is expressed in."""
if unit := self._config.get(CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT):
return UnitOfTemperature(unit)

return super().unit_of_measurement


@FLOAT_PROPERTIES_REGISTRY.register
Expand All @@ -212,7 +220,13 @@ class HumidityCustomFloatProperty(HumidityProperty, CustomFloatProperty):

@FLOAT_PROPERTIES_REGISTRY.register
class PressureCustomFloatProperty(PressureProperty, CustomFloatProperty):
pass
@property
def unit_of_measurement(self) -> UnitOfPressure:
"""Return the unit the property value is expressed in."""
if unit := self._config.get(CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT):
return UnitOfPressure(unit)

return super().unit_of_measurement


@FLOAT_PROPERTIES_REGISTRY.register
Expand Down
49 changes: 34 additions & 15 deletions custom_components/yandex_smart_home/property_float.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Implement the Yandex Smart Home float properties."""
from abc import ABC, abstractmethod
from contextlib import suppress
from typing import Protocol, Self

from homeassistant.components import air_quality, climate, fan, humidifier, light, sensor, switch, water_heater
Expand All @@ -16,8 +17,8 @@
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfPower,
UnitOfTemperature,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
ElectricCurrentConverter,
Expand Down Expand Up @@ -48,12 +49,11 @@
PropertyType,
ResponseCode,
TemperatureFloatPropertyParameters,
TemperatureUnit,
TVOCFloatPropertyParameters,
VoltageFloatPropertyParameters,
WaterLevelFloatPropertyParameters,
)
from .unit_conversion import PressureConverter, TVOCConcentrationConverter, UnitOfPressure
from .unit_conversion import PressureConverter, TVOCConcentrationConverter, UnitOfPressure, UnitOfTemperature


class FloatProperty(Property, Protocol):
Expand Down Expand Up @@ -90,9 +90,16 @@ def get_value(self) -> float | None:
raise APIError(ResponseCode.NOT_SUPPORTED_IN_CURRENT_MODE, f"Unsupported value '{value}' for {self}")

if self._native_unit_of_measurement and self.unit_of_measurement and self._unit_converter:
float_value = self._unit_converter.convert(
float_value, self._native_unit_of_measurement, self.unit_of_measurement
)
try:
float_value = self._unit_converter.convert(
float_value, self._native_unit_of_measurement, self.unit_of_measurement
)
except HomeAssistantError as e:
raise APIError(
ResponseCode.INVALID_VALUE,
f"Failed to convert value from '{self._native_unit_of_measurement}' to "
f"'{self.unit_of_measurement}' for {self}: {e}",
)

lower_limit, upper_limit = self.parameters.range
if lower_limit is not None and float_value < lower_limit:
Expand Down Expand Up @@ -145,11 +152,17 @@ class TemperatureProperty(FloatProperty, ABC):
@property
def parameters(self) -> TemperatureFloatPropertyParameters:
"""Return parameters for a devices list request."""
return TemperatureFloatPropertyParameters(unit=TemperatureUnit.CELSIUS)
return TemperatureFloatPropertyParameters(unit=self.unit_of_measurement.as_property_unit)

@property
def unit_of_measurement(self) -> str:
def unit_of_measurement(self) -> UnitOfTemperature:
"""Return the unit the property value is expressed in."""
if self._native_unit_of_measurement:
with suppress(ValueError):
unit = UnitOfTemperature(self._native_unit_of_measurement)
if unit.as_property_unit:
return unit

return UnitOfTemperature.CELSIUS

@property
Expand Down Expand Up @@ -182,7 +195,13 @@ def parameters(self) -> PressureFloatPropertyParameters:
@property
def unit_of_measurement(self) -> UnitOfPressure:
"""Return the unit the property value is expressed in."""
return UnitOfPressure(self._entry_data.pressure_unit)
if self._native_unit_of_measurement:
with suppress(ValueError):
unit = UnitOfPressure(self._native_unit_of_measurement)
if unit.as_property_unit:
return unit

return UnitOfPressure.MMHG

@property
def _unit_converter(self) -> PressureConverter:
Expand Down Expand Up @@ -442,7 +461,7 @@ def _get_native_value(self) -> float | str | None:
@property
def _native_unit_of_measurement(self) -> str:
"""Return the unit the native value is expressed in."""
return str(self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, self.unit_of_measurement))
return str(self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, UnitOfPressure.MMHG))


@STATE_PROPERTIES_REGISTRY.register
Expand Down Expand Up @@ -620,7 +639,7 @@ def _native_unit_of_measurement(self) -> str | None:
if self.state.domain == sensor.DOMAIN:
return str(self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, self.unit_of_measurement))

return self.unit_of_measurement
return None


@STATE_PROPERTIES_REGISTRY.register
Expand All @@ -646,12 +665,12 @@ def _get_native_value(self) -> float | str | None:
return self.state.attributes.get(const.ATTR_CURRENT)

@property
def _native_unit_of_measurement(self) -> str:
def _native_unit_of_measurement(self) -> str | None:
"""Return the unit the native value is expressed in."""
if self.state.domain == sensor.DOMAIN:
return str(self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, self.unit_of_measurement))

return self.unit_of_measurement
return None


@STATE_PROPERTIES_REGISTRY.register
Expand Down Expand Up @@ -681,12 +700,12 @@ def _get_native_value(self) -> float | str | None:
return self.state.state

@property
def _native_unit_of_measurement(self) -> str:
def _native_unit_of_measurement(self) -> str | None:
"""Return the unit the native value is expressed in."""
if self.state.domain == sensor.DOMAIN:
return str(self.state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, self.unit_of_measurement))

return self.unit_of_measurement
return None


@STATE_PROPERTIES_REGISTRY.register
Expand Down
6 changes: 6 additions & 0 deletions custom_components/yandex_smart_home/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,11 @@
"description": "Реквизиты для привязки Home Assistant к УДЯ:\n* ID: `{id}`\n* Пароль: `{password}`\n\nТеперь вы можете добавить Home Assistant в Умный дом Яндекса, для этого:\n * Откройте [квазар](https://yandex.ru/quasar/iot) или приложение [Дом с Алисой](https://mobile.yandex.ru/apps/smarthome)\n* Нажмите кнопку \"+\" в правом верхнем углу, выберите \"Устройство умного дома\"\n* Найдите в списке и выберите производителя \"Yaha Cloud\"\n* Нажмите кнопку \"Привязать к Яндексу\", откроется страница авторизации\n* Выполните привязку используя реквизиты выше"
}
}
},
"issues": {
"deprecated_pressure_unit": {
"description": "Параметр `pressure_unit` (раздел `settings`) больше не поддерживается, удалите его из YAML конфигурации.\n\nТеперь компонент автоматически пытается сохранить единицы измерения при передаче значений датчиков из Home Assistant в УДЯ ([подробнее о конвертации значений](https://docs.yaha-cloud.ru/master/devices/sensor/float/#unit-conversion))",
"title": "Устаревший параметр pressure_unit"
}
}
}
23 changes: 22 additions & 1 deletion custom_components/yandex_smart_home/unit_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
)

# noinspection PyProtectedMember
from homeassistant.util.unit_conversion import (
_IN_TO_M,
_MERCURY_DENSITY,
Expand All @@ -16,7 +18,7 @@
BaseUnitConverter,
)

from .schema import PressureUnit
from .schema import PressureUnit, TemperatureUnit


class TVOCConcentrationConverter(BaseUnitConverter):
Expand All @@ -42,6 +44,25 @@ class TVOCConcentrationConverter(BaseUnitConverter):
}


class UnitOfTemperature(StrEnum):
"""Temperature units."""

CELSIUS = "°C"
FAHRENHEIT = "°F"
KELVIN = "K"

@property
def as_property_unit(self) -> TemperatureUnit:
"""Return value as property unit."""
match self:
case self.CELSIUS:
return TemperatureUnit.CELSIUS
case self.KELVIN:
return TemperatureUnit.KELVIN

raise ValueError


class UnitOfPressure(StrEnum):
"""Extended pressure units."""

Expand Down
7 changes: 6 additions & 1 deletion docs/breaking-changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## Переход с 0.x на 1.x
### Состояние для custom_toggles
### Состояние для custom_toggles { id=v1-custom-toggles }
Пользовательские умения типа "Переключатели" теперь ожидают бинарные значения (`on/off/yes/no/True/False/1/0`) при определении своего состояния.
До версии 1.0 умение считалось включенным, когда состояние отличалось от `off`.

Expand All @@ -23,3 +23,8 @@
state_template: '{{ not is_state("select.humidifier_led_brightness", "off") }}'

```

### Параметр pressure_unit { id=v1-pressure-unit }
Параметр `pressure_unit` (раздел `settings`) больше не поддерживается, удалите его из YAML конфигурации.

Теперь компонент автоматически пытается сохранить единицы измерения при передаче значений датчиков из Home Assistant в УДЯ ([подробнее о конвертации значений](devices/sensor/float.md#unit-conversion))
Loading

0 comments on commit 90aa9a8

Please sign in to comment.