From 90aa9a8da87398ae73f4dbb89f8c932381bda6ce Mon Sep 17 00:00:00 2001 From: Artem Sorokin Date: Thu, 26 Oct 2023 15:48:37 +0300 Subject: [PATCH] New unit conversion rules for float properties (#448, #468) --- .../yandex_smart_home/__init__.py | 8 +- .../yandex_smart_home/config_validation.py | 34 +++-- custom_components/yandex_smart_home/const.py | 1 + .../yandex_smart_home/entry_data.py | 21 +++- .../yandex_smart_home/property_custom.py | 18 ++- .../yandex_smart_home/property_float.py | 49 +++++--- .../yandex_smart_home/translations/en.json | 6 + .../yandex_smart_home/unit_conversion.py | 23 +++- docs/breaking-changes.md | 7 +- docs/devices/sensor/float.md | 86 +++++++++++++ docs/supported-devices.md | 4 +- tests/fixtures/valid-config.yaml | 11 +- tests/test_config_validation.py | 74 +++++++++-- tests/test_entry_data.py | 24 +++- tests/test_init.py | 21 +++- tests/test_property_custom.py | 73 ++++++++--- tests/test_property_float.py | 117 ++++++++---------- 17 files changed, 441 insertions(+), 136 deletions(-) diff --git a/custom_components/yandex_smart_home/__init__.py b/custom_components/yandex_smart_home/__init__.py index 0e768a00..e91d9748 100644 --- a/custom_components/yandex_smart_home/__init__.py +++ b/custom_components/yandex_smart_home/__init__.py @@ -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, @@ -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, - } + }, ) diff --git a/custom_components/yandex_smart_home/config_validation.py b/custom_components/yandex_smart_home/config_validation.py index d92e2caa..bd12a4e1 100644 --- a/custom_components/yandex_smart_home/config_validation.py +++ b/custom_components/yandex_smart_home/config_validation.py @@ -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 @@ -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 @@ -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) diff --git a/custom_components/yandex_smart_home/const.py b/custom_components/yandex_smart_home/const.py index 208e635f..bc5c74dd 100644 --- a/custom_components/yandex_smart_home/const.py +++ b/custom_components/yandex_smart_home/const.py @@ -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" diff --git a/custom_components/yandex_smart_home/entry_data.py b/custom_components/yandex_smart_home/entry_data.py index 6ae2920b..b62626df 100644 --- a/custom_components/yandex_smart_home/entry_data.py +++ b/custom_components/yandex_smart_home/entry_data.py @@ -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 @@ -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: @@ -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.""" diff --git a/custom_components/yandex_smart_home/property_custom.py b/custom_components/yandex_smart_home/property_custom.py index 5f6459d5..6efe7956 100644 --- a/custom_components/yandex_smart_home/property_custom.py +++ b/custom_components/yandex_smart_home/property_custom.py @@ -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, @@ -51,6 +52,7 @@ WaterLevelPercentageProperty, ) from .schema import PropertyType, ResponseCode +from .unit_conversion import UnitOfPressure, UnitOfTemperature if TYPE_CHECKING: from homeassistant.core import HomeAssistant @@ -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 @@ -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 diff --git a/custom_components/yandex_smart_home/property_float.py b/custom_components/yandex_smart_home/property_float.py index 742500f4..369562dd 100644 --- a/custom_components/yandex_smart_home/property_float.py +++ b/custom_components/yandex_smart_home/property_float.py @@ -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 @@ -16,8 +17,8 @@ UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfPower, - UnitOfTemperature, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.unit_conversion import ( BaseUnitConverter, ElectricCurrentConverter, @@ -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): @@ -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: @@ -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 @@ -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: @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/custom_components/yandex_smart_home/translations/en.json b/custom_components/yandex_smart_home/translations/en.json index 96f97ba5..7ac43897 100644 --- a/custom_components/yandex_smart_home/translations/en.json +++ b/custom_components/yandex_smart_home/translations/en.json @@ -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" + } } } diff --git a/custom_components/yandex_smart_home/unit_conversion.py b/custom_components/yandex_smart_home/unit_conversion.py index 1d093549..55f7b367 100644 --- a/custom_components/yandex_smart_home/unit_conversion.py +++ b/custom_components/yandex_smart_home/unit_conversion.py @@ -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, @@ -16,7 +18,7 @@ BaseUnitConverter, ) -from .schema import PressureUnit +from .schema import PressureUnit, TemperatureUnit class TVOCConcentrationConverter(BaseUnitConverter): @@ -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.""" diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md index ba3b8557..ea52f8ae 100644 --- a/docs/breaking-changes.md +++ b/docs/breaking-changes.md @@ -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`. @@ -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)) diff --git a/docs/devices/sensor/float.md b/docs/devices/sensor/float.md index 29c8a2fb..8061d7f5 100644 --- a/docs/devices/sensor/float.md +++ b/docs/devices/sensor/float.md @@ -73,3 +73,89 @@ entity: switch.humidifer_socket attribute: current_power ``` + +### Единицы измерения в HA { id=property-unit-of-measurement } +> Параметр: `unit_of_measurement` + +Задаёт единицы измерения, в котором находится значение датчика в Home Assistant. Следует задавать, **только** если автоматическая [конвертация значений](#unit-conversion) работает неверно. В большинстве случаев использовать этот параметр не требуется. + +Альтернативные способы задать единицы измерения: + +1. Параметр `device_class` при создании датчика на [шаблоне](https://www.home-assistant.io/integrations/template/#sensor) +2. Настройки объекта на странице `Настройки` --> `Устройства и службы` --> [`Объекты`](https://my.home-assistant.io/redirect/entities/) + +!!! example "Пример" + ```yaml + yandex_smart_home: + entity_config: + humidifier.room: + properties: + - type: temperature + attribute: current_temperature + unit_of_measurement: °F + ``` + +Возможные значения `unit_of_measurement` (регистр важен): + +| Тип | `unit_of_measurement` | +|-------------|-------------------------------------------------------------------------| +| amperage | `A`, `mA` | +| power | `W`, `kW` | +| pressure | `atm`, `Pa`, `hPa`, `kPa`, `bar`, `cbar`, `mbar`, `mmHg`, `inHg`, `psi` | +| temperature | `°C`, `°F`, `K` | +| tvoc | `µg/m³`, `mg/m³`, `μg/ft³`, `p/m³`, `ppm`, `ppb` | +| voltage | `V`, `mV` | + + +### Единицы измерения в УДЯ { id=property-target-unit-of-measurement } +> Параметр: `target_unit_of_measurement` + +Задаёт единицы измерения, в которых значение датчика должно быть представлено в УДЯ. Поддерживается только для температуры и давления. + +Следует использовать в тех **редких** случаях, когда вам хочется, чтобы единицы измерения не совпадали между HA и УДЯ. Например: в HA давление в паскалях, а в УДЯ нужно в мм. рт. ст. + +!!! info "Пример" + ```yaml + yandex_smart_home: + entity_config: + sensor.temperature: + properties: + - type: temperature + entity: sensor.temperature + target_unit_of_measurement: °С # поддерживаются: °С и K + - type: pressure + entity_sensor.pressure + target_unit_of_measurement: mmHg # поддерживаются: mmHg, Pa, atm, bar + ``` + +## Конвертация значений { id=unit-conversion } +Единицы измерения значения датчика в УДЯ фиксированы и не могут быть произвольными: + +| Тип | Единицы измерения | +|----------------------------------------------|------------------------------------------------| +| amperage | Всегда амперы | +| battery_level | Всегда проценты | +| co2_level | Всегда миллионые доли (ppm) | +| food_level | Всегда проценты | +| humidity | Всегда проценты | +| illumination | Всегда люксы | +| pm1_density
pm2.5_density
pm10_density | Всегда мкг/м³ | +| power | Всегда ватты | +| pressure | На выбор: атмосферы, паскали, бары, мм рт. ст. | +| temperature | На выбор: градусы по цельсию, кельвины | +| tvoc | Всегда мкг/м³ | +| voltage | Всегда вольты | +| water_level | Всегда проценты | + +Компонент автоматически выполняет конвертацию значений из единиц измерения в HA в единицы измерения в УДЯ. + +Если в HA используется значение в единцах, поддерживаемых УДЯ - конвертация выполнена не будет (например если в HA давление в барах, то и в УДЯ оно будет передано в барах). + +### Изменение единиц измерения { id=select-unit-of-measurement } +В некоторых случаях компонент не может сам определить, в каких единицах измерения находится значение датчика. В этом случае значение может быть сконвертировано неверно. + +Несколько способов исправить эту ситуацию: + +1. Задать верные единицы измерения через `device_class` при создании [датчика на шаблоне](https://www.home-assistant.io/integrations/template/#configuration-variables) +2. Задать верные единицы измерения в настройках объекта на странице `Настройки` --> `Устройства и службы` --> [`Объекты`](https://my.home-assistant.io/redirect/entities/) +3. Использовать параметр [`unit_of_measurement`](#property-unit-of-measurement) при ручной настройке датчика diff --git a/docs/supported-devices.md b/docs/supported-devices.md index 45bac65d..bf5066cc 100644 --- a/docs/supported-devices.md +++ b/docs/supported-devices.md @@ -113,7 +113,7 @@ | `device_tracker` | | | `lawn_mower` | Поддержка запланирована | | `notify` | | -| `number`, `input_number` | Можно привязать к устройству через [пользовательские умения](advanced/capabilties.md) | -| `remote` | Можно задействовать в [пользовательском умении](advanced/capabilties.md) | +| `number`, `input_number` | Можно привязать к устройству через [пользовательские умения](advanced/capabilities.md) | +| `remote` | Можно задействовать в [пользовательском умении](advanced/capabilities.md) | | `select`, `input_select` | Нет ясности что делать при выборе разных значений
Можно задействовать в [пользовательском умении](advanced/capabilities.md) | | `siren` | Поддержка запланирована | diff --git a/tests/fixtures/valid-config.yaml b/tests/fixtures/valid-config.yaml index 5d703d20..4b827e15 100644 --- a/tests/fixtures/valid-config.yaml +++ b/tests/fixtures/valid-config.yaml @@ -4,7 +4,6 @@ yandex_smart_home: skill_id: d38d4c39-5846-ba53-67acc27e08bc user_id: e8701ad48ba05a91604e480dd60899a3 settings: - pressure_unit: mmHg beta: true color_profile: test: @@ -180,3 +179,13 @@ yandex_smart_home: custom_modes: input_source: state_template: buz + sensor.sun: + properties: + - type: temperature + value_template: '{{ 15000000 }}' + unit_of_measurement: °C + target_unit_of_measurement: K + - type: pressure + value_template: '{{ 0 }}' + unit_of_measurement: mmHg + target_unit_of_measurement: bar diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index 6de318a8..5b89cea0 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -67,6 +67,68 @@ async def test_invalid_float_property_type(hass, caplog): ) in caplog.messages[-1] +async def test_invalid_property_target_unit_of_measurement(hass, caplog): + files = { + YAML_CONFIG_FILE: """ +yandex_smart_home: + entity_config: + sensor.test: + properties: + - type: battery_level + entity: sensor.test + target_unit_of_measurement: foo +""" + } + with patch_yaml_files(files): + assert await async_integration_yaml_config(hass, DOMAIN) is None + + assert ( + "Target unit of measurement 'foo' is not supported for battery_level property, see valid values at " + "https://docs.yaha-cloud.ru/master/devices/sensor/float/#property-target-unit-of-measurement" + ) in caplog.messages[-1] + + for type_prefix in ["", "float."]: + files = { + YAML_CONFIG_FILE: f""" + yandex_smart_home: + entity_config: + sensor.test: + properties: + - type: {type_prefix}temperature + entity: sensor.test + target_unit_of_measurement: °F + """ + } + with patch_yaml_files(files): + assert await async_integration_yaml_config(hass, DOMAIN) is None + + assert ( + f"Target unit of measurement '°F' is not supported for {type_prefix}temperature property, " + f"see valid values " + f"at https://docs.yaha-cloud.ru/master/devices/sensor/float/#property-target-unit-of-measurement" + ) in caplog.messages[-1] + + files = { + YAML_CONFIG_FILE: f""" + yandex_smart_home: + entity_config: + sensor.test: + properties: + - type: {type_prefix}pressure + entity: sensor.test + target_unit_of_measurement: psi + """ + } + with patch_yaml_files(files): + assert await async_integration_yaml_config(hass, DOMAIN) is None + + assert ( + f"Target unit of measurement 'psi' is not supported for {type_prefix}pressure property, " + f"see valid values " + f"at https://docs.yaha-cloud.ru/master/devices/sensor/float/#property-target-unit-of-measurement" + ) in caplog.messages[-1] + + async def test_invalid_property(hass, caplog): files = { YAML_CONFIG_FILE: """ @@ -220,18 +282,6 @@ async def test_invalid_device_type(hass, caplog): assert "Device type 'unsupported' is not supported" in caplog.messages[-1] -async def test_invalid_pressure_unit(hass): - files = { - YAML_CONFIG_FILE: """ -yandex_smart_home: - settings: - pressure_unit: invalid -""" - } - with patch_yaml_files(files): - assert await async_integration_yaml_config(hass, DOMAIN) is None - - async def test_invalid_color_name(hass, caplog): files = { YAML_CONFIG_FILE: """ diff --git a/tests/test_entry_data.py b/tests/test_entry_data.py index be57d5d0..57cafe83 100644 --- a/tests/test_entry_data.py +++ b/tests/test_entry_data.py @@ -1,8 +1,9 @@ from unittest.mock import patch +from homeassistant.helpers import issue_registry as ir from pytest_homeassistant_custom_component.common import MockConfigEntry -from custom_components.yandex_smart_home import const +from custom_components.yandex_smart_home import DOMAIN, YandexSmartHome, const from custom_components.yandex_smart_home.entry_data import ConfigEntryData from custom_components.yandex_smart_home.helpers import APIError from custom_components.yandex_smart_home.schema import ResponseCode @@ -34,3 +35,24 @@ def test_entry_data_trackable_states(hass, caplog): ): assert entry_data._get_trackable_states() == {} assert caplog.messages == ["Failed to track custom capability: foo"] + + +async def test_deprecated_pressure_unit(hass, config_entry_direct): + issue_registry = ir.async_get(hass) + + config_entry_direct.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_direct.entry_id) + await hass.async_block_till_done() + assert issue_registry.async_get_issue(DOMAIN, "deprecated_pressure_unit") is None + await hass.config_entries.async_unload(config_entry_direct.entry_id) + + component: YandexSmartHome = hass.data[DOMAIN] + component._yaml_config = {const.CONF_SETTINGS: {const.CONF_PRESSURE_UNIT: "foo"}} + await hass.config_entries.async_setup(config_entry_direct.entry_id) + assert issue_registry.async_get_issue(DOMAIN, "deprecated_pressure_unit") is not None + await hass.config_entries.async_unload(config_entry_direct.entry_id) + + component._yaml_config = {const.CONF_SETTINGS: {}} + await hass.config_entries.async_setup(config_entry_direct.entry_id) + assert issue_registry.async_get_issue(DOMAIN, "deprecated_pressure_unit") is None + await hass.config_entries.async_unload(config_entry_direct.entry_id) diff --git a/tests/test_init.py b/tests/test_init.py index 53a55cae..756f2a36 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -35,7 +35,7 @@ async def test_valid_config(hass): "user_id": "e8701ad48ba05a91604e480dd60899a3", } ] - assert config[DOMAIN]["settings"] == {"pressure_unit": "mmHg", "beta": True} + assert config[DOMAIN]["settings"] == {"beta": True} assert config[DOMAIN]["color_profile"] == {"test": {"red": 16711680, "green": 65280, "warm_white": 3000}} assert config[DOMAIN]["filter"] == { "include_domains": ["switch", "light", "climate"], @@ -47,7 +47,7 @@ async def test_valid_config(hass): } entity_config = config[DOMAIN]["entity_config"] - assert len(entity_config) == 15 + assert len(entity_config) == 16 assert entity_config["switch.kitchen"] == { "name": "Выключатель", @@ -184,6 +184,23 @@ async def test_valid_config(hass): "custom_toggles": {"backlight": {"state_template": Template("bar", hass)}}, } + assert entity_config["sensor.sun"] == { + "properties": [ + { + "target_unit_of_measurement": "K", + "type": "temperature", + "unit_of_measurement": "°C", + "value_template": Template("{{ 15000000 }}", hass), + }, + { + "target_unit_of_measurement": "bar", + "type": "pressure", + "unit_of_measurement": "mmHg", + "value_template": Template("{{ 0 }}", hass), + }, + ] + } + async def test_empty_dict_config(hass): files = { diff --git a/tests/test_property_custom.py b/tests/test_property_custom.py index 0a34d14a..29898926 100644 --- a/tests/test_property_custom.py +++ b/tests/test_property_custom.py @@ -276,20 +276,29 @@ async def test_property_custom_value_float_limit(hass): @pytest.mark.parametrize( - "instance,unit_of_measurement,unit,value", + "instance,unit_of_measurement,unit,fallback_unit,assert_value", [ - ("pressure", "bar", "unit.pressure.mmhg", 75006.16), - ("tvoc", "ppb", "unit.density.mcg_m3", 449.63), - ("amperage", "mA", "unit.ampere", 0.1), - ("voltage", "mV", "unit.volt", 0.1), - ("temperature", "K", "unit.temperature.celsius", -173.15), - ("humidity", "x", "unit.percent", 100), + ("pressure", "bar", "unit.pressure.bar", "unit.pressure.mmhg", None), + ("pressure", "Pa", "unit.pressure.pascal", "unit.pressure.mmhg", None), + ("pressure", "mmHg", "unit.pressure.mmhg", None, None), + ("pressure", "atm", "unit.pressure.atm", "unit.pressure.mmhg", None), + ("pressure", "psi", "unit.pressure.mmhg", None, 5171.49), + ("tvoc", "ppb", "unit.density.mcg_m3", None, 449.63), + ("amperage", "mA", "unit.ampere", None, 0.1), + ("voltage", "mV", "unit.volt", None, 0.1), + ("temperature", "°F", "unit.temperature.celsius", None, 37.78), + ("temperature", "°C", "unit.temperature.celsius", None, None), + ("temperature", "K", "unit.temperature.kelvin", "unit.temperature.celsius", None), + ("humidity", "x", "unit.percent", None, None), ], ) -async def test_property_custom_get_value_float_conversion(hass, instance, unit_of_measurement, unit, value): - state = State("sensor.test", "100") +async def test_property_custom_get_value_float_conversion( + hass, instance, unit_of_measurement, unit, fallback_unit, assert_value +): + value = 100 + state = State("sensor.test", str(value)) hass.states.async_set(state.entity_id, state.state) - hass.states.async_set("climate.test", STATE_ON, {ATTR_UNIT_OF_MEASUREMENT: unit_of_measurement, "t": 100}) + hass.states.async_set("climate.test", STATE_ON, {ATTR_UNIT_OF_MEASUREMENT: unit_of_measurement, "t": str(value)}) prop = get_custom_property( hass, @@ -301,7 +310,7 @@ async def test_property_custom_get_value_float_conversion(hass, instance, unit_o state.entity_id, ) assert prop.parameters.dict()["unit"] == unit - assert prop.get_value() == value + assert prop.get_value() == (value if assert_value is None else assert_value) hass.states.async_set(state.entity_id, STATE_UNAVAILABLE) assert prop.get_value() is None @@ -310,7 +319,7 @@ async def test_property_custom_get_value_float_conversion(hass, instance, unit_o hass.states.async_set(state.entity_id, state.state, state.attributes) prop = get_custom_property(hass, BASIC_ENTRY_DATA, {const.CONF_ENTITY_PROPERTY_TYPE: instance}, state.entity_id) assert prop.parameters.dict()["unit"] == unit - assert prop.get_value() == value + assert prop.get_value() == (value if assert_value is None else assert_value) # ignore unit_of_measurement when use attribute prop = get_custom_property( @@ -323,8 +332,8 @@ async def test_property_custom_get_value_float_conversion(hass, instance, unit_o }, state.entity_id, ) - assert prop.parameters.dict()["unit"] == unit - assert prop.get_value() == 100 + assert prop.parameters.dict()["unit"] == (unit if fallback_unit is None else fallback_unit) + assert prop.get_value() == value # override unit_of_measurement when use attribute prop = get_custom_property( @@ -339,4 +348,38 @@ async def test_property_custom_get_value_float_conversion(hass, instance, unit_o state.entity_id, ) assert prop.parameters.dict()["unit"] == unit - assert prop.get_value() == value + assert prop.get_value() == (value if assert_value is None else assert_value) + + +@pytest.mark.parametrize( + "instance,unit_of_measurement,target_unit_of_measurement,target_unit,assert_value", + [ + ("pressure", "bar", "Pa", "unit.pressure.pascal", 10000000.0), + ("pressure", "bar", "mmHg", "unit.pressure.mmhg", 75006.16), + ("pressure", "bar", "atm", "unit.pressure.atm", 98.69), + ("pressure", "atm", "bar", "unit.pressure.bar", 101.33), + ("temperature", "°F", "K", "unit.temperature.kelvin", 310.93), + ("temperature", "°C", "K", "unit.temperature.kelvin", 373.15), + ("temperature", "K", "°C", "unit.temperature.celsius", -173.15), + ], +) +async def test_property_custom_get_value_float_conversion_override_target_unit( + hass, instance, unit_of_measurement, target_unit_of_measurement, target_unit, assert_value +): + value = 100 + state = State("sensor.test", str(value), {ATTR_UNIT_OF_MEASUREMENT: unit_of_measurement}) + hass.states.async_set(state.entity_id, state.state, state.attributes) + + prop = get_custom_property( + hass, + BASIC_ENTRY_DATA, + { + const.CONF_ENTITY_PROPERTY_TYPE: instance, + const.CONF_ENTITY_PROPERTY_ENTITY: state.entity_id, + const.CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT: target_unit_of_measurement, + }, + state.entity_id, + ) + + assert prop.parameters.dict()["unit"] == target_unit + assert prop.get_value() == assert_value diff --git a/tests/test_property_float.py b/tests/test_property_float.py index 5f46f2fa..26899249 100644 --- a/tests/test_property_float.py +++ b/tests/test_property_float.py @@ -24,12 +24,12 @@ from homeassistant.exceptions import HomeAssistantError import pytest -from custom_components.yandex_smart_home import const +from custom_components.yandex_smart_home.helpers import APIError from custom_components.yandex_smart_home.property_float import PropertyType -from custom_components.yandex_smart_home.schema import FloatPropertyInstance +from custom_components.yandex_smart_home.schema import FloatPropertyInstance, ResponseCode from custom_components.yandex_smart_home.unit_conversion import UnitOfPressure -from . import BASIC_ENTRY_DATA, MockConfigEntryData +from . import BASIC_ENTRY_DATA from .test_property import assert_no_properties, get_exact_one_property @@ -156,6 +156,20 @@ async def test_property_float_temperature_convertion(hass): assert prop.parameters == {"instance": "temperature", "unit": "unit.temperature.celsius"} assert prop.get_value() == 34.76 + state = State( + "sensor.test", + "34.756", + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.KELVIN, + }, + ) + prop = get_exact_one_property(hass, BASIC_ENTRY_DATA, state, PropertyType.FLOAT, FloatPropertyInstance.TEMPERATURE) + + assert prop.retrievable is True + assert prop.parameters == {"instance": "temperature", "unit": "unit.temperature.kelvin"} + assert prop.get_value() == 34.76 + state = State( "sensor.test", "50.10", @@ -170,84 +184,57 @@ async def test_property_float_temperature_convertion(hass): assert prop.parameters == {"instance": "temperature", "unit": "unit.temperature.celsius"} assert prop.get_value() == 10.06 - -@pytest.mark.parametrize("device_class", [SensorDeviceClass.PRESSURE, SensorDeviceClass.ATMOSPHERIC_PRESSURE]) -@pytest.mark.parametrize( - "unit_of_measurement,property_unit,v", - [ - (UnitOfPressure.PA, "unit.pressure.pascal", 98658.57), - (UnitOfPressure.MMHG, "unit.pressure.mmhg", 740), - (UnitOfPressure.ATM, "unit.pressure.atm", 0.97), - (UnitOfPressure.BAR, "unit.pressure.bar", 0.99), - ], -) -def test_property_float_pressure_from_mmhg( - hass, config_entry_direct, device_class, unit_of_measurement, property_unit, v -): - entry_data = MockConfigEntryData( - entry=config_entry_direct, yaml_config={const.CONF_SETTINGS: {const.CONF_PRESSURE_UNIT: unit_of_measurement}} - ) state = State( "sensor.test", - "740", - {ATTR_DEVICE_CLASS: device_class, ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MMHG}, + "50.10", + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: "foo", + }, ) - prop = get_exact_one_property(hass, entry_data, state, PropertyType.FLOAT, FloatPropertyInstance.PRESSURE) - assert prop.retrievable is True - assert prop.parameters == {"instance": "pressure", "unit": property_unit} - assert prop.get_value() == v + prop = get_exact_one_property(hass, BASIC_ENTRY_DATA, state, PropertyType.FLOAT, FloatPropertyInstance.TEMPERATURE) - prop.state = State( - "sensor.test", - "-5", - {ATTR_DEVICE_CLASS: device_class, ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MMHG}, + assert prop.retrievable is True + assert prop.parameters == {"instance": "temperature", "unit": "unit.temperature.celsius"} + with pytest.raises(APIError) as e: + prop.get_value() + assert e.value.code == ResponseCode.INVALID_VALUE + assert e.value.message == ( + "Failed to convert value from 'foo' to '°C' for instance temperature of float property of sensor.test: " + "foo is not a recognized temperature unit." ) - assert prop.get_value() == 0 @pytest.mark.parametrize("device_class", [SensorDeviceClass.PRESSURE, SensorDeviceClass.ATMOSPHERIC_PRESSURE]) @pytest.mark.parametrize( - "unit_of_measurement,property_unit,v", + "unit_of_measurement,property_unit,assert_value", [ - (UnitOfPressure.PA, "unit.pressure.pascal", 106868.73), - (UnitOfPressure.MMHG, "unit.pressure.mmhg", 801.58), - (UnitOfPressure.ATM, "unit.pressure.atm", 1.05), - (UnitOfPressure.BAR, "unit.pressure.bar", 1.07), + (None, "unit.pressure.mmhg", None), + (UnitOfPressure.PA, "unit.pressure.pascal", None), + (UnitOfPressure.MMHG, "unit.pressure.mmhg", None), + (UnitOfPressure.ATM, "unit.pressure.atm", None), + (UnitOfPressure.BAR, "unit.pressure.bar", None), + (UnitOfPressure.PSI, "unit.pressure.mmhg", 38294.9), ], ) -def test_property_float_pressure_from_psi( - hass, config_entry_direct, device_class, unit_of_measurement, property_unit, v -): - entry_data = MockConfigEntryData( - entry=config_entry_direct, yaml_config={const.CONF_SETTINGS: {const.CONF_PRESSURE_UNIT: unit_of_measurement}} - ) - state = State( - "sensor.test", - "15.5", - {ATTR_DEVICE_CLASS: device_class, ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.PSI}, - ) - prop = get_exact_one_property(hass, entry_data, state, PropertyType.FLOAT, FloatPropertyInstance.PRESSURE) +def test_property_float_pressure(hass, device_class, unit_of_measurement, property_unit, assert_value): + value = 740.5 + attributes = {ATTR_DEVICE_CLASS: device_class} + if unit_of_measurement: + attributes[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement + + state = State("sensor.test", str(value), attributes) + prop = get_exact_one_property(hass, BASIC_ENTRY_DATA, state, PropertyType.FLOAT, FloatPropertyInstance.PRESSURE) assert prop.retrievable is True assert prop.parameters == {"instance": "pressure", "unit": property_unit} - assert prop.get_value() == v + if assert_value: + assert prop.get_value() == assert_value + else: + assert prop.get_value() == value -def test_property_float_pressure_unsupported_target(hass, config_entry_direct): - entry_data = MockConfigEntryData( - entry=config_entry_direct, yaml_config={const.CONF_SETTINGS: {const.CONF_PRESSURE_UNIT: "kPa"}} - ) - state = State( - "sensor.test", - "15.5", - {ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.PSI}, - ) - prop = get_exact_one_property(hass, entry_data, state, PropertyType.FLOAT, FloatPropertyInstance.PRESSURE) - assert prop.retrievable is True - with pytest.raises(ValueError): - assert prop.parameters - - with pytest.raises(ValueError): - prop.get_value() + prop.state = State("sensor.test", "-5", attributes) + assert prop.get_value() == 0 @pytest.mark.parametrize(