Compare commits

...

24 Commits
v0.2.0 ... main

Author SHA1 Message Date
Joel
c6be6be1ec
fix: initialization problem of _attr_fan_modes (#955)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-02 11:30:58 +08:00
Necroneco
77b0a4531b
fix: fix some specs (#949) 2025-04-02 08:53:15 +08:00
Li Shuzhen
7d9250914c
docs: update changelog and version to v0.2.4 (#937)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-28 09:26:10 +08:00
Li Shuzhen
a09289ef90
Fix specs (#929)
* fix: cuco.plug.cp2 voltage and power value ratio

* fix: cgllc.airmonitor.s1 unit ppb

* fix: roswan.waterpuri.lte01 tds unit

* fix: lumi.relay.c2acn01 power consumption unit

* fix: xiaomi.bhf_light.s1 fan level of ventilation

* fix: error log
2025-03-28 09:10:09 +08:00
Necroneco
b0428dc95a
feat: make submersion-state, contact-state, occupancy-status as binary_sensor (#905)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-27 15:45:46 +08:00
whoami
19ed04f2f5
fix: correct unit,icon and translations for hhcc-v1 (#927)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-25 09:54:02 +08:00
dw881114
e174a73f52
Update spec_modify.yaml (#921)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-24 16:34:48 +08:00
Li Shuzhen
a1aa1c024f
docs: update changelog and version to v0.2.3 (#911)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-21 09:53:39 +08:00
Li Shuzhen
372e635681
Fix specs (#910)
* fix: chuangmi.plug.212a01 power consumption value

* fix: yeelink.bhf_light.v10 mode description in English
2025-03-21 09:36:14 +08:00
Li Shuzhen
3759aa9a1b
fix: climate on/off feature initialization (#899)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-20 18:02:25 +08:00
Li Shuzhen
60d054cf19
docs: update changelog and version to v0.2.2 (#882)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-14 08:47:45 +08:00
Li Shuzhen
6680d9e8cb
feat: add conversion rules for the air-conditioner service and the air-fresh service (#879) 2025-03-14 08:23:03 +08:00
Li Shuzhen
0ef8cb6370
fix: xiaomi.aircondition.m9 humidity-range unit (#878)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-13 17:41:02 +08:00
Li Shuzhen
8f0a69c611
feat: convert the mode of the ptc bath heater to the preset mode (#874) 2025-03-13 17:37:44 +08:00
Li Shuzhen
8be0fa5d61
fix: MIoT-Spec-V2 conflicts of xiaomi.fan.p5 and mike.bhf_light.2 (#866)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-12 15:22:03 +08:00
Necroneco
07cb4ed193
feat: avoid setting icon when device_class is defined (#855) 2025-03-12 15:17:02 +08:00
Li Shuzhen
5c46504d0e
docs: update changelog and version to v0.2.1 (#848)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-07 14:20:17 +08:00
Li Shuzhen
97d89b3a04
feat: thermostat preset mode (#833)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-07 08:48:35 +08:00
Li Shuzhen
4482d257dc
revert: multi_lang.json (#834) 2025-03-07 08:48:12 +08:00
wilds
d0387be15b
fix #838 (#839) 2025-03-07 08:47:52 +08:00
Necroneco
27cf1085bd
fix: opening and closing for linp.wopener.wd1lb (#826) 2025-03-07 08:46:17 +08:00
Necroneco
e69448f2eb
feat: add entity_category for indicator-light (#697)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-05 16:13:17 +08:00
Li Shuzhen
7901607648
fix: fan-level without value-list but with value-range (#808) 2025-03-05 15:31:18 +08:00
Li Shuzhen
5adcb7ce00
fix: wind-reverse format type (#810) 2025-03-05 15:31:02 +08:00
16 changed files with 661 additions and 193 deletions

View File

@ -1,4 +1,53 @@
# CHANGELOG # CHANGELOG
## v0.2.4
### Added
- Convert the submersion-state, the contact-state and the occupancy-status property to the binary_sensor entity. [#905](https://github.com/XiaoMi/ha_xiaomi_home/pull/905)
### Changed
- suittc.airrtc.wk168 mode descriptions are set to strings of numbers from 1 to 16. [#921](https://github.com/XiaoMi/ha_xiaomi_home/pull/921)
- Do not set _attr_suggested_display_precision when the spec.expr is set in spec_modify.yaml [#929](https://github.com/XiaoMi/ha_xiaomi_home/pull/929)
- Set "unknown event msg" log to info level.
### Fixed
- hhcc.plantmonitor.v1 soil moisture and soil ec icon and unit. [#927](https://github.com/XiaoMi/ha_xiaomi_home/pull/27)
- cuco.plug.cp2 voltage and power value ratio.
- cgllc.airmonitor.s1 unit ppb.
- roswan.waterpuri.lte01 tds unit.
- lumi.relay.c2acn01 power consumption unit
- xiaomi.bhf_light.s1 fan level of ventilation.
## v0.2.3
### Changed
- Specify the service name and the property name during the climate entity's on/off feature initialization. [#899](https://github.com/XiaoMi/ha_xiaomi_home/pull/899)
- Remove the useless total-battery property from `SPEC_PROP_TRANS_MAP`.
### Fixed
- Fix the hvac mode setting error when changing the preset mode of the ptc-bath-heater.
- Fix the ambiguous descriptions of yeelink.bhf_light.v10 ptc-bath-heater mode value-list.
- Fix the power consumption value of chuangmi.plug.212a01. [#910](https://github.com/XiaoMi/ha_xiaomi_home/pull/910)
## v0.2.2
This version has modified the conversion rules of the climate entity, which will have effect on the devices with the ptc-bath-heater, the air-conditioner and the air-fresh service. After updating, you need to restart Home Assistant and check `xiaomi_home > CONFIGURE >
Update entity conversion rules > NEXT` to reload the integration.
这个版本修改了浴霸、空调、新风机的实体转换规则,更新之后需要重启 Home Assistant并且勾选 `xiaomi_home > 配置 > 更新实体转换规则 > 下一步` 重新加载集成。
### Added
- Add conversion rules for the air-conditioner service and the air-fresh service. [#879](https://github.com/XiaoMi/ha_xiaomi_home/pull/879)
### Changed
- Convert the mode of the ptc bath heater to the preset mode of the climate entity. [#874](https://github.com/XiaoMi/ha_xiaomi_home/pull/874)
- Use Home Assistant default icon when device_class is set. [#855](https://github.com/XiaoMi/ha_xiaomi_home/pull/855)
### Fixed
- Fix xiaomi.aircondition.m9 humidity-range unit. [#878](https://github.com/XiaoMi/ha_xiaomi_home/pull/878)
- Fix MIoT-Spec-V2 conflicts of xiaomi.fan.p5 and mike.bhf_light.2. [#866](https://github.com/XiaoMi/ha_xiaomi_home/pull/866)
## v0.2.1
### Added
- Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833)
### Changed
- Change paho-mqtt version to adapt Home Assistant 2025.03. [#839](https://github.com/XiaoMi/ha_xiaomi_home/pull/839)
- Revert to use multi_lang.json. [#834](https://github.com/XiaoMi/ha_xiaomi_home/pull/834)
### Fixed
- Fix the opening and the closing status of linp.wopener.wd1lb. [#826](https://github.com/XiaoMi/ha_xiaomi_home/pull/826)
- Fix the format type of the wind-reverse property. [#810](https://github.com/XiaoMi/ha_xiaomi_home/pull/810)
- Fix the fan-level property without value-list but with value-range. [#808](https://github.com/XiaoMi/ha_xiaomi_home/pull/808)
## v0.2.0 ## v0.2.0
This version has modified some default units of sensors. After updating, it may cause Home Assistant to pop up some compatibility warnings. You can re-add the integration to resolve it. This version has modified some default units of sensors. After updating, it may cause Home Assistant to pop up some compatibility warnings. You can re-add the integration to resolve it.

View File

@ -89,4 +89,8 @@ class BinarySensor(MIoTPropertyEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""On/Off state. True if the binary sensor is on, False otherwise.""" """On/Off state. True if the binary sensor is on, False otherwise."""
if self.spec.name == 'contact-state':
return self._value is False
elif self.spec.name == 'occupancy-status':
return bool(self._value)
return self._value is True return self._value is True

View File

@ -51,6 +51,7 @@ from typing import Any, Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.const import UnitOfTemperature
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.climate import ( from homeassistant.components.climate import (
FAN_ON, FAN_OFF, SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL, FAN_ON, FAN_OFF, SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL,
@ -101,21 +102,19 @@ class FeatureOnOff(MIoTServiceEntity, ClimateEntity):
self._prop_on = None self._prop_on = None
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props: def _init_on_off(self, service_name: str, prop_name: str) -> None:
if prop.name == 'on': """Initialize the on_off feature."""
if ( for prop in self.entity_data.props:
# The "on" property of the "fan-control" service is not if prop.name == prop_name and prop.service.name == service_name:
# the on/off feature of the entity. if prop.format_ != bool:
prop.service.name == 'air-conditioner' or _LOGGER.error('wrong format %s %s, %s', service_name,
prop.service.name == 'heater' or prop_name, self.entity_id)
prop.service.name == 'thermostat' or continue
prop.service.name == 'electric-blanket'): self._attr_supported_features |= ClimateEntityFeature.TURN_ON
self._attr_supported_features |= ( self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
ClimateEntityFeature.TURN_ON) self._prop_on = prop
self._attr_supported_features |= ( break
ClimateEntityFeature.TURN_OFF)
self._prop_on = prop
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn on.""" """Turn on."""
@ -134,6 +133,7 @@ class FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity):
entity_data: MIoTEntityData) -> None: entity_data: MIoTEntityData) -> None:
"""Initialize the feature class.""" """Initialize the feature class."""
self._prop_target_temp = None self._prop_target_temp = None
self._attr_temperature_unit = None
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties # properties
@ -151,6 +151,10 @@ class FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity):
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE) ClimateEntityFeature.TARGET_TEMPERATURE)
self._prop_target_temp = prop self._prop_target_temp = prop
break
# temperature_unit is required by the climate entity
if not self._attr_temperature_unit:
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set the target temperature.""" """Set the target temperature."""
@ -189,7 +193,7 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity):
for prop in self.entity_data.props: for prop in self.entity_data.props:
if prop.name == prop_name and prop.service.name == service_name: if prop.name == prop_name and prop.service.name == service_name:
if not prop.value_list: if not prop.value_list:
_LOGGER.error('invalid %s %s value_list, %s',service_name, _LOGGER.error('invalid %s %s value_list, %s', service_name,
prop_name, self.entity_id) prop_name, self.entity_id)
continue continue
self._mode_map = prop.value_list.to_map() self._mode_map = prop.value_list.to_map()
@ -197,6 +201,7 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity):
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.PRESET_MODE) ClimateEntityFeature.PRESET_MODE)
self._prop_mode = prop self._prop_mode = prop
break
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode.""" """Set the preset mode."""
@ -225,11 +230,14 @@ class FeatureFanMode(MIoTServiceEntity, ClimateEntity):
self._prop_fan_on = None self._prop_fan_on = None
self._prop_fan_level = None self._prop_fan_level = None
self._fan_mode_map = None self._fan_mode_map = None
self._attr_fan_modes = None
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'fan-level' and prop.service.name == 'fan-control': if (prop.name == 'fan-level' and
(prop.service.name == 'fan-control' or
prop.service.name == 'thermostat')):
if not prop.value_list: if not prop.value_list:
_LOGGER.error('invalid fan-level value_list, %s', _LOGGER.error('invalid fan-level value_list, %s',
self.entity_id) self.entity_id)
@ -363,6 +371,7 @@ class FeatureTemperature(MIoTServiceEntity, ClimateEntity):
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'temperature': if prop.name == 'temperature':
self._prop_env_temperature = prop self._prop_env_temperature = prop
break
@property @property
def current_temperature(self) -> Optional[float]: def current_temperature(self) -> Optional[float]:
@ -385,6 +394,7 @@ class FeatureHumidity(MIoTServiceEntity, ClimateEntity):
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'relative-humidity': if prop.name == 'relative-humidity':
self._prop_env_humidity = prop self._prop_env_humidity = prop
break
@property @property
def current_humidity(self) -> Optional[float]: def current_humidity(self) -> Optional[float]:
@ -416,6 +426,7 @@ class FeatureTargetHumidity(MIoTServiceEntity, ClimateEntity):
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.TARGET_HUMIDITY) ClimateEntityFeature.TARGET_HUMIDITY)
self._prop_target_humidity = prop self._prop_target_humidity = prop
break
async def async_set_humidity(self, humidity): async def async_set_humidity(self, humidity):
"""Set the target humidity.""" """Set the target humidity."""
@ -445,6 +456,8 @@ class Heater(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
self._attr_icon = 'mdi:radiator' self._attr_icon = 'mdi:radiator'
# hvac modes # hvac modes
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
# on/off
self._init_on_off('heater', 'on')
# preset modes # preset modes
self._init_preset_modes('heater', 'heat-level') self._init_preset_modes('heater', 'heat-level')
@ -480,10 +493,12 @@ class AirConditioner(FeatureOnOff, FeatureTargetTemperature,
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_icon = 'mdi:air-conditioner' self._attr_icon = 'mdi:air-conditioner'
# on/off
self._init_on_off('air-conditioner', 'on')
# hvac modes # hvac modes
self._attr_hvac_modes = None self._attr_hvac_modes = None
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'mode': if prop.name == 'mode' and prop.service.name == 'air-conditioner':
if not prop.value_list: if not prop.value_list:
_LOGGER.error('invalid mode value_list, %s', self.entity_id) _LOGGER.error('invalid mode value_list, %s', self.entity_id)
continue continue
@ -603,7 +618,7 @@ class AirConditioner(FeatureOnOff, FeatureTargetTemperature,
class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature, class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature,
FeatureFanMode, FeatureSwingMode): FeatureFanMode, FeatureSwingMode, FeaturePresetMode):
"""Ptc bath heater""" """Ptc bath heater"""
_prop_mode: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty]
_hvac_mode_map: Optional[dict[int, HVACMode]] _hvac_mode_map: Optional[dict[int, HVACMode]]
@ -618,70 +633,7 @@ class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature,
self._attr_icon = 'mdi:hvac' self._attr_icon = 'mdi:hvac'
# hvac modes # hvac modes
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'mode': if prop.name == 'mode' and prop.service.name == 'ptc-bath-heater':
if not prop.value_list:
_LOGGER.error('invalid mode value_list, %s', self.entity_id)
continue
self._hvac_mode_map = {}
for item in prop.value_list.items:
if item.name in {'off', 'idle'
} and (HVACMode.OFF not in list(
self._hvac_mode_map.values())):
self._hvac_mode_map[item.value] = HVACMode.OFF
elif item.name in {'auto'}:
self._hvac_mode_map[item.value] = HVACMode.AUTO
elif item.name in {'ventilate'}:
self._hvac_mode_map[item.value] = HVACMode.COOL
elif item.name in {'heat', 'quick_heat'
} and (HVACMode.HEAT not in list(
self._hvac_mode_map.values())):
self._hvac_mode_map[item.value] = HVACMode.HEAT
elif item.name in {'defog'}:
self._hvac_mode_map[item.value] = HVACMode.HEAT_COOL
elif item.name in {'dry'}:
self._hvac_mode_map[item.value] = HVACMode.DRY
elif item.name in {'fan'}:
self._hvac_mode_map[item.value] = HVACMode.FAN_ONLY
self._attr_hvac_modes = list(self._hvac_mode_map.values())
self._prop_mode = prop
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the target hvac mode."""
if self._prop_mode is None:
return
mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode)
if mode_value is None or not await self.set_property_async(
prop=self._prop_mode, value=mode_value):
raise RuntimeError(
f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}')
@property
def hvac_mode(self) -> Optional[HVACMode]:
"""The current hvac mode."""
return (self.get_map_value(map_=self._hvac_mode_map,
key=self.get_prop_value(
prop=self._prop_mode))
if self._prop_mode else None)
class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
FeatureHumidity, FeatureFanMode):
"""Thermostat"""
_prop_mode: Optional[MIoTSpecProperty]
_hvac_mode_map: Optional[dict[int, HVACMode]]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the thermostat."""
self._prop_mode = None
self._hvac_mode_map = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_icon = 'mdi:thermostat'
# hvac modes
self._attr_hvac_modes = None
for prop in entity_data.props:
if prop.name == 'mode':
if not prop.value_list: if not prop.value_list:
_LOGGER.error('invalid mode value_list, %s', self.entity_id) _LOGGER.error('invalid mode value_list, %s', self.entity_id)
continue continue
@ -689,54 +641,65 @@ class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
for item in prop.value_list.items: for item in prop.value_list.items:
if item.name in {'off', 'idle'}: if item.name in {'off', 'idle'}:
self._hvac_mode_map[item.value] = HVACMode.OFF self._hvac_mode_map[item.value] = HVACMode.OFF
elif item.name in {'auto'}: break
self._hvac_mode_map[item.value] = HVACMode.AUTO if self._hvac_mode_map:
elif item.name in {'cool'}: self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]
self._hvac_mode_map[item.value] = HVACMode.COOL else:
elif item.name in {'heat'}: _LOGGER.error('no idle mode, %s', self.entity_id)
self._hvac_mode_map[item.value] = HVACMode.HEAT # preset modes
elif item.name in {'dry'}: self._init_preset_modes('ptc-bath-heater', 'mode')
self._hvac_mode_map[item.value] = HVACMode.DRY
elif item.name in {'fan'}:
self._hvac_mode_map[item.value] = HVACMode.FAN_ONLY
self._attr_hvac_modes = list(self._hvac_mode_map.values())
self._prop_mode = prop
if self._attr_hvac_modes is None:
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO]
elif HVACMode.OFF not in self._attr_hvac_modes:
self._attr_hvac_modes.insert(0, HVACMode.OFF)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the target hvac mode.""" """Set the target hvac mode."""
# set the device off if self._prop_mode is None or hvac_mode != HVACMode.OFF:
if hvac_mode == HVACMode.OFF:
if not await self.set_property_async(prop=self._prop_on,
value=False):
raise RuntimeError(f'set climate prop.on failed, {hvac_mode}, '
f'{self.entity_id}')
return
# set the device on
elif self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async(prop=self._prop_on, value=True)
# set mode
if self._prop_mode is None:
return return
mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode) mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode)
if mode_value is None or not await self.set_property_async( if mode_value is None or not await self.set_property_async(
prop=self._prop_mode, value=mode_value): prop=self._prop_mode, value=mode_value):
raise RuntimeError( raise RuntimeError(
f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}') f'set ptc-bath-heater {hvac_mode} failed, {self.entity_id}')
@property @property
def hvac_mode(self) -> Optional[HVACMode]: def hvac_mode(self) -> Optional[HVACMode]:
"""The current hvac mode.""" """The current hvac mode."""
if self.get_prop_value(prop=self._prop_on) is False: if self._prop_mode is None:
return HVACMode.OFF return None
return (self.get_map_value(map_=self._hvac_mode_map, current_mode = self.get_prop_value(prop=self._prop_mode)
key=self.get_prop_value( if current_mode is None:
prop=self._prop_mode)) return None
if self._prop_mode else None) mode_value = self.get_map_value(map_=self._hvac_mode_map,
key=current_mode)
return HVACMode.OFF if mode_value == HVACMode.OFF else HVACMode.AUTO
class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
FeatureHumidity, FeatureFanMode, FeaturePresetMode):
"""Thermostat"""
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the thermostat."""
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_icon = 'mdi:thermostat'
# hvac modes
self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]
# on/off
self._init_on_off('thermostat', 'on')
# preset modes
self._init_preset_modes('thermostat', 'mode')
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the target hvac mode."""
await self.set_property_async(
prop=self._prop_on,
value=False if hvac_mode == HVACMode.OFF else True)
@property
def hvac_mode(self) -> Optional[HVACMode]:
"""The current hvac mode."""
return (HVACMode.AUTO if self.get_prop_value(
prop=self._prop_on) else HVACMode.OFF)
class ElectricBlanket(FeatureOnOff, FeatureTargetTemperature, class ElectricBlanket(FeatureOnOff, FeatureTargetTemperature,
@ -751,6 +714,8 @@ class ElectricBlanket(FeatureOnOff, FeatureTargetTemperature,
self._attr_icon = 'mdi:rug' self._attr_icon = 'mdi:rug'
# hvac modes # hvac modes
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
# on/off
self._init_on_off('electric-blanket', 'on')
# preset modes # preset modes
self._init_preset_modes('electric-blanket', 'mode') self._init_preset_modes('electric-blanket', 'mode')

View File

@ -47,7 +47,7 @@ Cover entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Optional from typing import Any, Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -101,7 +101,11 @@ class Cover(MIoTServiceEntity, CoverEntity):
_prop_status_closed: Optional[list[int]] _prop_status_closed: Optional[list[int]]
_prop_current_position: Optional[MIoTSpecProperty] _prop_current_position: Optional[MIoTSpecProperty]
_prop_target_position: Optional[MIoTSpecProperty] _prop_target_position: Optional[MIoTSpecProperty]
_prop_position_value_min: Optional[int]
_prop_position_value_max: Optional[int]
_prop_position_value_range: Optional[int] _prop_position_value_range: Optional[int]
_prop_pos_closing: bool
_prop_pos_opening: bool
def __init__(self, miot_device: MIoTDevice, def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None: entity_data: MIoTEntityData) -> None:
@ -122,7 +126,11 @@ class Cover(MIoTServiceEntity, CoverEntity):
self._prop_status_closed = [] self._prop_status_closed = []
self._prop_current_position = None self._prop_current_position = None
self._prop_target_position = None self._prop_target_position = None
self._prop_position_value_min = None
self._prop_position_value_max = None
self._prop_position_value_range = None self._prop_position_value_range = None
self._prop_pos_closing = False
self._prop_pos_opening = False
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
@ -166,6 +174,8 @@ class Cover(MIoTServiceEntity, CoverEntity):
'invalid current-position value_range format, %s', 'invalid current-position value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._prop_position_value_min = prop.value_range.min_
self._prop_position_value_max = prop.value_range.max_
self._prop_position_value_range = (prop.value_range.max_ - self._prop_position_value_range = (prop.value_range.max_ -
prop.value_range.min_) prop.value_range.min_)
self._prop_current_position = prop self._prop_current_position = prop
@ -175,23 +185,52 @@ class Cover(MIoTServiceEntity, CoverEntity):
'invalid target-position value_range format, %s', 'invalid target-position value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._prop_position_value_min = prop.value_range.min_
self._prop_position_value_max = prop.value_range.max_
self._prop_position_value_range = (prop.value_range.max_ - self._prop_position_value_range = (prop.value_range.max_ -
prop.value_range.min_) prop.value_range.min_)
self._attr_supported_features |= CoverEntityFeature.SET_POSITION self._attr_supported_features |= CoverEntityFeature.SET_POSITION
self._prop_target_position = prop self._prop_target_position = prop
# For the device that has the current position property but no status
# property, the current position property will be used to determine the
# opening and the closing status.
if (self._prop_status is None) and (self._prop_current_position
is not None):
self.sub_prop_changed(self._prop_current_position,
self._position_changed_handler)
def _position_changed_handler(self, prop: MIoTSpecProperty,
ctx: Any) -> None:
self._prop_pos_closing = False
self._prop_pos_opening = False
self.async_write_ha_state()
async def async_open_cover(self, **kwargs) -> None: async def async_open_cover(self, **kwargs) -> None:
"""Open the cover.""" """Open the cover."""
current = None if (self._prop_current_position
is None) else self.get_prop_value(
prop=self._prop_current_position)
if (current is not None) and (current < self._prop_position_value_max):
self._prop_pos_opening = True
self._prop_pos_closing = False
await self.set_property_async(self._prop_motor_control, await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_open) self._prop_motor_value_open)
async def async_close_cover(self, **kwargs) -> None: async def async_close_cover(self, **kwargs) -> None:
"""Close the cover.""" """Close the cover."""
current = None if (self._prop_current_position
is None) else self.get_prop_value(
prop=self._prop_current_position)
if (current is not None) and (current > self._prop_position_value_min):
self._prop_pos_opening = False
self._prop_pos_closing = True
await self.set_property_async(self._prop_motor_control, await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_close) self._prop_motor_value_close)
async def async_stop_cover(self, **kwargs) -> None: async def async_stop_cover(self, **kwargs) -> None:
"""Stop the cover.""" """Stop the cover."""
self._prop_pos_opening = False
self._prop_pos_closing = False
await self.set_property_async(self._prop_motor_control, await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_pause) self._prop_motor_value_pause)
@ -200,6 +239,10 @@ class Cover(MIoTServiceEntity, CoverEntity):
pos = kwargs.get(ATTR_POSITION, None) pos = kwargs.get(ATTR_POSITION, None)
if pos is None: if pos is None:
return None return None
current = self.current_cover_position
if current is not None:
self._prop_pos_opening = pos > current
self._prop_pos_closing = pos < current
pos = round(pos * self._prop_position_value_range / 100) pos = round(pos * self._prop_position_value_range / 100)
await self.set_property_async(prop=self._prop_target_position, await self.set_property_async(prop=self._prop_target_position,
value=pos) value=pos)
@ -214,9 +257,11 @@ class Cover(MIoTServiceEntity, CoverEntity):
# Assume that the current position is the same as the target # Assume that the current position is the same as the target
# position when the current position is not defined in the device's # position when the current position is not defined in the device's
# MIoT-Spec-V2. # MIoT-Spec-V2.
return None if (self._prop_target_position if self._prop_target_position is None:
is None) else self.get_prop_value( return None
prop=self._prop_target_position) self._prop_pos_opening = False
self._prop_pos_closing = False
return self.get_prop_value(prop=self._prop_target_position)
pos = self.get_prop_value(prop=self._prop_current_position) pos = self.get_prop_value(prop=self._prop_current_position)
return None if pos is None else round(pos * 100 / return None if pos is None else round(pos * 100 /
self._prop_position_value_range) self._prop_position_value_range)
@ -227,14 +272,9 @@ class Cover(MIoTServiceEntity, CoverEntity):
if self._prop_status and self._prop_status_opening: if self._prop_status and self._prop_status_opening:
return (self.get_prop_value(prop=self._prop_status) return (self.get_prop_value(prop=self._prop_status)
in self._prop_status_opening) in self._prop_status_opening)
# The status is prior to the numerical relationship of the current # The status has higher priority when determining whether the cover
# position and the target position when determining whether the cover
# is opening. # is opening.
if (self._prop_target_position and return self._prop_pos_opening
self.current_cover_position is not None):
return (self.current_cover_position
< self.get_prop_value(prop=self._prop_target_position))
return None
@property @property
def is_closing(self) -> Optional[bool]: def is_closing(self) -> Optional[bool]:
@ -242,14 +282,9 @@ class Cover(MIoTServiceEntity, CoverEntity):
if self._prop_status and self._prop_status_closing: if self._prop_status and self._prop_status_closing:
return (self.get_prop_value(prop=self._prop_status) return (self.get_prop_value(prop=self._prop_status)
in self._prop_status_closing) in self._prop_status_closing)
# The status is prior to the numerical relationship of the current # The status has higher priority when determining whether the cover
# position and the target position when determining whether the cover
# is closing. # is closing.
if (self._prop_target_position and return self._prop_pos_closing
self.current_cover_position is not None):
return (self.current_cover_position
> self.get_prop_value(prop=self._prop_target_position))
return None
@property @property
def is_closed(self) -> Optional[bool]: def is_closed(self) -> Optional[bool]:

View File

@ -46,6 +46,7 @@ off Xiaomi or its affiliates' products.
Event entities for Xiaomi Home. Event entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
import logging
from typing import Any from typing import Any
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -57,6 +58,8 @@ from .miot.miot_spec import MIoTSpecEvent
from .miot.miot_device import MIoTDevice, MIoTEventEntity from .miot.miot_device import MIoTDevice, MIoTEventEntity
from .miot.const import DOMAIN from .miot.const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -89,4 +92,5 @@ class Event(MIoTEventEntity, EventEntity):
self, name: str, arguments: dict[str, Any] | None = None self, name: str, arguments: dict[str, Any] | None = None
) -> None: ) -> None:
"""An event is occurred.""" """An event is occurred."""
_LOGGER.debug('%s, attributes: %s', name, str(arguments))
self._trigger_event(event_type=name, event_attributes=arguments) self._trigger_event(event_type=name, event_attributes=arguments)

View File

@ -172,7 +172,7 @@ class Fan(MIoTServiceEntity, FanEntity):
self._attr_supported_features |= FanEntityFeature.OSCILLATE self._attr_supported_features |= FanEntityFeature.OSCILLATE
self._prop_horizontal_swing = prop self._prop_horizontal_swing = prop
elif prop.name == 'wind-reverse': elif prop.name == 'wind-reverse':
if prop.format_ == 'bool': if prop.format_ == bool:
self._prop_wind_reverse_forward = False self._prop_wind_reverse_forward = False
self._prop_wind_reverse_reverse = True self._prop_wind_reverse_reverse = True
elif prop.value_list: elif prop.value_list:
@ -186,7 +186,7 @@ class Fan(MIoTServiceEntity, FanEntity):
or self._prop_wind_reverse_reverse is None or self._prop_wind_reverse_reverse is None
): ):
# NOTICE: Value may be 0 or False # NOTICE: Value may be 0 or False
_LOGGER.info( _LOGGER.error(
'invalid wind-reverse, %s', self.entity_id) 'invalid wind-reverse, %s', self.entity_id)
continue continue
self._attr_supported_features |= FanEntityFeature.DIRECTION self._attr_supported_features |= FanEntityFeature.DIRECTION

View File

@ -179,7 +179,7 @@ class Light(MIoTServiceEntity, LightEntity):
) / prop.value_range.step) ) / prop.value_range.step)
> self._VALUE_RANGE_MODE_COUNT_MAX > self._VALUE_RANGE_MODE_COUNT_MAX
): ):
_LOGGER.info( _LOGGER.error(
'too many mode values, %s, %s, %s', 'too many mode values, %s, %s, %s',
self.entity_id, prop.name, prop.value_range) self.entity_id, prop.name, prop.value_range)
else: else:

View File

@ -20,12 +20,12 @@
], ],
"requirements": [ "requirements": [
"construct>=2.10.56", "construct>=2.10.56",
"paho-mqtt<2.0.0", "paho-mqtt",
"numpy", "numpy",
"cryptography", "cryptography",
"psutil" "psutil"
], ],
"version": "v0.2.0", "version": "v0.2.4",
"zeroconf": [ "zeroconf": [
"_miot-central._tcp.local." "_miot-central._tcp.local."
] ]

View File

@ -549,6 +549,10 @@ class MIoTDevice:
# Optional actions # Optional actions
# Optional events # Optional events
miot_service.platform = platform miot_service.platform = platform
# entity_category
if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get(
'entity_category', None):
miot_service.entity_category = entity_category
return entity_data return entity_data
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool: def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
@ -587,13 +591,8 @@ class MIoTDevice:
# Priority: spec_modify.unit > unit_convert > specv2entity.unit # Priority: spec_modify.unit > unit_convert > specv2entity.unit
miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][ miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][
prop_name]['unit_of_measurement'] prop_name]['unit_of_measurement']
if ( # Priority: default.icon when device_class is set > spec_modify.icon
not miot_prop.icon # > icon_convert
and 'icon' in SPEC_PROP_TRANS_MAP['properties'][prop_name]
):
# Priority: spec_modify.icon > icon_convert > specv2entity.icon
miot_prop.icon = SPEC_PROP_TRANS_MAP['properties'][prop_name][
'icon']
miot_prop.platform = platform miot_prop.platform = platform
return True return True
@ -899,6 +898,7 @@ class MIoTServiceEntity(Entity):
self._attr_name = ( self._attr_name = (
f'{"* "if self.entity_data.spec.proprietary else " "}' f'{"* "if self.entity_data.spec.proprietary else " "}'
f'{self.entity_data.spec.description_trans}') f'{self.entity_data.spec.description_trans}')
self._attr_entity_category = entity_data.spec.entity_category
# Set entity attr # Set entity attr
self._attr_unique_id = self.entity_id self._attr_unique_id = self.entity_id
self._attr_should_poll = False self._attr_should_poll = False

View File

@ -1215,9 +1215,10 @@ class MipsLocalClient(_MipsClient):
or 'eiid' not in msg or 'eiid' not in msg
# or 'arguments' not in msg # or 'arguments' not in msg
): ):
# self.log_error(f'on_event_msg, recv unknown msg, {payload}') self.log_info('unknown event msg, %s', payload)
return return
if 'arguments' not in msg: if 'arguments' not in msg:
self.log_info('wrong event msg, %s', payload)
msg['arguments'] = [] msg['arguments'] = []
if handler: if handler:
self.log_debug('local, on event_occurred, %s', payload) self.log_debug('local, on event_occurred, %s', payload)

View File

@ -56,7 +56,7 @@ from slugify import slugify
# pylint: disable=relative-beyond-top-level # pylint: disable=relative-beyond-top-level
from .const import DEFAULT_INTEGRATION_LANGUAGE, SPEC_STD_LIB_EFFECTIVE_TIME from .const import DEFAULT_INTEGRATION_LANGUAGE, SPEC_STD_LIB_EFFECTIVE_TIME
from .common import MIoTHttp, load_yaml_file from .common import MIoTHttp, load_yaml_file, load_json_file
from .miot_error import MIoTSpecError from .miot_error import MIoTSpecError
from .miot_storage import MIoTStorage from .miot_storage import MIoTStorage
@ -465,7 +465,7 @@ class _MIoTSpecBase:
iid: int iid: int
type_: str type_: str
description: str description: str
description_trans: str description_trans: Optional[str]
proprietary: bool proprietary: bool
need_filter: bool need_filter: bool
name: str name: str
@ -476,6 +476,7 @@ class _MIoTSpecBase:
device_class: Any device_class: Any
state_class: Any state_class: Any
external_unit: Any external_unit: Any
entity_category: Optional[str]
spec_id: int spec_id: int
@ -494,6 +495,7 @@ class _MIoTSpecBase:
self.device_class = None self.device_class = None
self.state_class = None self.state_class = None
self.external_unit = None self.external_unit = None
self.entity_category = None
self.spec_id = hash(f'{self.type_}.{self.iid}') self.spec_id = hash(f'{self.type_}.{self.iid}')
@ -837,6 +839,7 @@ class _MIoTSpecMultiLang:
"""MIoT SPEC multi lang class.""" """MIoT SPEC multi lang class."""
# pylint: disable=broad-exception-caught # pylint: disable=broad-exception-caught
_DOMAIN: str = 'miot_specs_multi_lang' _DOMAIN: str = 'miot_specs_multi_lang'
_MULTI_LANG_FILE = 'specs/multi_lang.json'
_lang: str _lang: str
_storage: MIoTStorage _storage: MIoTStorage
_main_loop: asyncio.AbstractEventLoop _main_loop: asyncio.AbstractEventLoop
@ -892,6 +895,25 @@ class _MIoTSpecMultiLang:
except Exception as err: except Exception as err:
trans_local = {} trans_local = {}
_LOGGER.info('get multi lang from local failed, %s, %s', urn, err) _LOGGER.info('get multi lang from local failed, %s, %s', urn, err)
# Revert: load multi_lang.json
try:
trans_local_json = await self._main_loop.run_in_executor(
None, load_json_file,
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
self._MULTI_LANG_FILE))
urn_strs: list[str] = urn.split(':')
urn_key: str = ':'.join(urn_strs[:6])
if (
isinstance(trans_local_json, dict)
and urn_key in trans_local_json
and self._lang in trans_local_json[urn_key]
):
trans_cache.update(trans_local_json[urn_key][self._lang])
trans_local = trans_local_json[urn_key]
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('multi lang, load json file error, %s', err)
# Revert end
# Default language # Default language
if not trans_cache: if not trans_cache:
if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud: if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud:
@ -1183,6 +1205,9 @@ class _SpecModify:
if isinstance(self._selected, str): if isinstance(self._selected, str):
return await self.set_spec_async(urn=self._selected) return await self.set_spec_async(urn=self._selected)
def get_prop_name(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='name')
def get_prop_unit(self, siid: int, piid: int) -> Optional[str]: def get_prop_unit(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='unit') return self.__get_prop_item(siid=siid, piid=piid, key='unit')
@ -1205,6 +1230,13 @@ class _SpecModify:
return None return None
return value_range return value_range
def get_prop_value_list(self, siid: int, piid: int) -> Optional[list]:
value_list = self.__get_prop_item(siid=siid, piid=piid,
key='value-list')
if not isinstance(value_list, list):
return None
return value_list
def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]: def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:
if not self._selected: if not self._selected:
return None return None
@ -1444,10 +1476,12 @@ class MIoTSpecParser:
key=':'.join(p_type_strs[:5])) key=':'.join(p_type_strs[:5]))
or property_['description'] or property_['description']
or spec_prop.name) or spec_prop.name)
if 'value-range' in property_: # Modify value-list before translation
spec_prop.value_range = property_['value-range'] v_list: list[dict] = self._spec_modify.get_prop_value_list(
elif 'value-list' in property_: siid=service['iid'], piid=property_['iid'])
v_list: list[dict] = property_['value-list'] if (v_list is None) and ('value-list' in property_):
v_list = property_['value-list']
if v_list is not None:
for index, v in enumerate(v_list): for index, v in enumerate(v_list):
if v['description'].strip() == '': if v['description'].strip() == '':
v['description'] = f'v_{v["value"]}' v['description'] = f'v_{v["value"]}'
@ -1461,6 +1495,8 @@ class MIoTSpecParser:
f'{v["description"]}') f'{v["description"]}')
or v['name']) or v['name'])
spec_prop.value_list = MIoTSpecValueList.from_spec(v_list) spec_prop.value_list = MIoTSpecValueList.from_spec(v_list)
if 'value-range' in property_:
spec_prop.value_range = property_['value-range']
elif property_['format'] == 'bool': elif property_['format'] == 'bool':
v_tag = ':'.join(p_type_strs[:5]) v_tag = ':'.join(p_type_strs[:5])
v_descriptions = ( v_descriptions = (
@ -1485,6 +1521,10 @@ class MIoTSpecParser:
siid=service['iid'], piid=property_['iid']) siid=service['iid'], piid=property_['iid'])
if custom_range: if custom_range:
spec_prop.value_range = custom_range spec_prop.value_range = custom_range
custom_name = self._spec_modify.get_prop_name(
siid=service['iid'], piid=property_['iid'])
if custom_name:
spec_prop.name = custom_name
# Parse service event # Parse service event
for event in service.get('events', []): for event in service.get('events', []):
if ( if (

View File

@ -0,0 +1,187 @@
{
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"de": {
"service:001": "Geräteinformationen",
"service:001:property:003": "Geräte-ID",
"service:001:property:005": "Seriennummer (SN)",
"service:002": "Gateway",
"service:002:event:001": "Netzwerk geändert",
"service:002:event:002": "Netzwerk geändert",
"service:002:property:001": "Zugriffsmethode",
"service:002:property:001:valuelist:000": "Kabelgebunden",
"service:002:property:001:valuelist:001": "5G Drahtlos",
"service:002:property:001:valuelist:002": "2.4G Drahtlos",
"service:002:property:002": "IP-Adresse",
"service:002:property:003": "WiFi-Netzwerkname",
"service:002:property:004": "Aktuelle Zeit",
"service:002:property:005": "DHCP-Server-MAC-Adresse",
"service:003": "Anzeigelampe",
"service:003:property:001": "Schalter",
"service:004": "Virtueller Dienst",
"service:004:action:001": "Virtuelles Ereignis erzeugen",
"service:004:event:001": "Virtuelles Ereignis aufgetreten",
"service:004:property:001": "Ereignisname"
},
"en": {
"service:001": "Device Information",
"service:001:property:003": "Device ID",
"service:001:property:005": "Serial Number (SN)",
"service:002": "Gateway",
"service:002:event:001": "Network Changed",
"service:002:event:002": "Network Changed",
"service:002:property:001": "Access Method",
"service:002:property:001:valuelist:000": "Wired",
"service:002:property:001:valuelist:001": "5G Wireless",
"service:002:property:001:valuelist:002": "2.4G Wireless",
"service:002:property:002": "IP Address",
"service:002:property:003": "WiFi Network Name",
"service:002:property:004": "Current Time",
"service:002:property:005": "DHCP Server MAC Address",
"service:003": "Indicator Light",
"service:003:property:001": "Switch",
"service:004": "Virtual Service",
"service:004:action:001": "Generate Virtual Event",
"service:004:event:001": "Virtual Event Occurred",
"service:004:property:001": "Event Name"
},
"es": {
"service:001": "Información del dispositivo",
"service:001:property:003": "ID del dispositivo",
"service:001:property:005": "Número de serie (SN)",
"service:002": "Puerta de enlace",
"service:002:event:001": "Cambio de red",
"service:002:event:002": "Cambio de red",
"service:002:property:001": "Método de acceso",
"service:002:property:001:valuelist:000": "Cableado",
"service:002:property:001:valuelist:001": "5G inalámbrico",
"service:002:property:001:valuelist:002": "2.4G inalámbrico",
"service:002:property:002": "Dirección IP",
"service:002:property:003": "Nombre de red WiFi",
"service:002:property:004": "Hora actual",
"service:002:property:005": "Dirección MAC del servidor DHCP",
"service:003": "Luz indicadora",
"service:003:property:001": "Interruptor",
"service:004": "Servicio virtual",
"service:004:action:001": "Generar evento virtual",
"service:004:event:001": "Ocurrió un evento virtual",
"service:004:property:001": "Nombre del evento"
},
"fr": {
"service:001": "Informations sur l'appareil",
"service:001:property:003": "ID de l'appareil",
"service:001:property:005": "Numéro de série (SN)",
"service:002": "Passerelle",
"service:002:event:001": "Changement de réseau",
"service:002:event:002": "Changement de réseau",
"service:002:property:001": "Méthode d'accès",
"service:002:property:001:valuelist:000": "Câblé",
"service:002:property:001:valuelist:001": "Sans fil 5G",
"service:002:property:001:valuelist:002": "Sans fil 2.4G",
"service:002:property:002": "Adresse IP",
"service:002:property:003": "Nom du réseau WiFi",
"service:002:property:004": "Heure actuelle",
"service:002:property:005": "Adresse MAC du serveur DHCP",
"service:003": "Voyant lumineux",
"service:003:property:001": "Interrupteur",
"service:004": "Service virtuel",
"service:004:action:001": "Générer un événement virtuel",
"service:004:event:001": "Événement virtuel survenu",
"service:004:property:001": "Nom de l'événement"
},
"ja": {
"service:001": "デバイス情報",
"service:001:property:003": "デバイスID",
"service:001:property:005": "シリアル番号 (SN)",
"service:002": "ゲートウェイ",
"service:002:event:001": "ネットワークが変更されました",
"service:002:event:002": "ネットワークが変更されました",
"service:002:property:001": "アクセス方法",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G ワイヤレス",
"service:002:property:001:valuelist:002": "2.4G ワイヤレス",
"service:002:property:002": "IPアドレス",
"service:002:property:003": "WiFiネットワーク名",
"service:002:property:004": "現在の時間",
"service:002:property:005": "DHCPサーバーMACアドレス",
"service:003": "インジケータライト",
"service:003:property:001": "スイッチ",
"service:004": "バーチャルサービス",
"service:004:action:001": "バーチャルイベントを生成",
"service:004:event:001": "バーチャルイベントが発生しました",
"service:004:property:001": "イベント名"
},
"ru": {
"service:001": "Информация об устройстве",
"service:001:property:003": "ID устройства",
"service:001:property:005": "Серийный номер (SN)",
"service:002": "Шлюз",
"service:002:event:001": "Сеть изменена",
"service:002:event:002": "Сеть изменена",
"service:002:property:001": "Метод доступа",
"service:002:property:001:valuelist:000": "Проводной",
"service:002:property:001:valuelist:001": "5G Беспроводной",
"service:002:property:001:valuelist:002": "2.4G Беспроводной",
"service:002:property:002": "IP Адрес",
"service:002:property:003": "Название WiFi сети",
"service:002:property:004": "Текущее время",
"service:002:property:005": "MAC адрес DHCP сервера",
"service:003": "Световой индикатор",
"service:003:property:001": "Переключатель",
"service:004": "Виртуальная служба",
"service:004:action:001": "Создать виртуальное событие",
"service:004:event:001": "Произошло виртуальное событие",
"service:004:property:001": "Название события"
},
"zh-Hant": {
"service:001": "設備信息",
"service:001:property:003": "設備ID",
"service:001:property:005": "序號 (SN)",
"service:002": "網關",
"service:002:event:001": "網路發生變化",
"service:002:event:002": "網路發生變化",
"service:002:property:001": "接入方式",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G 無線",
"service:002:property:001:valuelist:002": "2.4G 無線",
"service:002:property:002": "IP地址",
"service:002:property:003": "WiFi網路名稱",
"service:002:property:004": "當前時間",
"service:002:property:005": "DHCP伺服器MAC地址",
"service:003": "指示燈",
"service:003:property:001": "開關",
"service:004": "虛擬服務",
"service:004:action:001": "產生虛擬事件",
"service:004:event:001": "虛擬事件發生",
"service:004:property:001": "事件名稱"
}
},
"urn:miot-spec-v2:device:switch:0000A003:lumi-acn040": {
"en": {
"service:011": "Right Button On and Off",
"service:011:property:001": "Right Button On and Off",
"service:015:action:001": "Left Button Identify",
"service:016:action:001": "Middle Button Identify",
"service:017:action:001": "Right Button Identify"
},
"zh-Hans": {
"service:015:action:001": "左键确认",
"service:016:action:001": "中键确认",
"service:017:action:001": "右键确认"
}
},
"urn:miot-spec-v2:device:bath-heater:0000A028:yeelink-v10": {
"en": {
"service:003:property:001:valuelist:000": "Idle",
"service:003:property:001:valuelist:001": "Dry"
}
},
"urn:miot-spec-v2:device:plant-monitor:0000A030:hhcc-v1": {
"en": {
"service:002:property:001": "Soil Moisture"
},
"zh-Hans": {
"service:002:property:001": "土壤湿度",
"service:002:property:003": "光照强度"
}
}
}

View File

@ -14,12 +14,21 @@ urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
- notify - notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1 urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1 urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1: urn:miot-spec-v2:device:gateway:0000A019:lumi-mcn001:1: # lumi.gateway.mcn001
prop.5.1: prop.2.1: # access-mode
name: power-consumption access:
expr: round(src_value/1000, 3) - read
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1 - notify
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1 prop.2.2: # ip-address
icon: mdi:ip
prop.2.3: # wifi-ssid
access:
- read
- notify
prop.2.5: # access-mode
access:
- read
- notify
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1:
prop.2.2: prop.2.2:
name: power-consumption name: power-consumption
@ -33,6 +42,23 @@ urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816:
prop.3.1: prop.3.1:
name: electric-power name: electric-power
expr: round(src_value/100, 2) expr: round(src_value/100, 2)
urn:miot-spec-v2:device:outlet:0000A002:qmi-psv3:1:0000C816: # qmi.plug.psv3
prop.3.3: # voltage
unit: mV
prop.3.4: # electric-current
unit: mA
urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-acn001:1: # lumi.motion.acn001
prop.3.2: # voltage
access:
- read
- notify
unit: mV
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:izq-24:2:0000C824: # izq.sensor_occupy.24
prop.2.6: # distance
unit: cm
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:linp-hb01:2:0000C824: # linp.sensor_occupy.hb01
prop.3.3: # body-distance
unit: m
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1: urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
prop.2.1: prop.2.1:
name: download-speed name: download-speed
@ -49,3 +75,110 @@ urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1:
- 1 - 1
- 1 - 1
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:2: urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1 urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:2: urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1
urn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1:
prop.5.2:
value-list:
- value: 1
description: low
- value: 128
description: medium
- value: 255
description: high
urn:miot-spec-v2:device:bath-heater:0000A028:mike-2:1:
prop.3.1:
name: mode-a
prop.3.11:
name: mode-b
prop.3.12:
name: mode-c
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:5: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:airer:0000A00D:mrbond-m33a:1:
prop.2.3:
name: current-position-a
prop.2.11:
name: current-position-b
urn:miot-spec-v2:device:thermostat:0000A031:suittc-wk168:1:
prop.2.3:
value-list:
- value: 1
description: '1'
- value: 2
description: '2'
- value: 3
description: '3'
- value: 4
description: '4'
- value: 5
description: '5'
- value: 6
description: '6'
- value: 7
description: '7'
- value: 8
description: '8'
- value: 9
description: '9'
- value: 10
description: '10'
- value: 11
description: '11'
- value: 12
description: '12'
- value: 13
description: '13'
- value: 14
description: '14'
- value: 15
description: '15'
- value: 16
description: '16'
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3:
prop.5.1:
expr: round(src_value*6/1000000, 3)
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2:
prop.2.3:
expr: round(src_value/10, 1)
prop.2.4:
unit: mA
prop.3.2:
expr: round(src_value/10, 1)
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2
urn:miot-spec-v2:device:plant-monitor:0000A030:hhcc-v1:1:
prop.2.1:
name: soil-moisture
icon: mdi:watering-can
prop.2.2:
name: soil-ec
icon: mdi:sprout-outline
unit: μS/cm
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:
prop.2.5:
name: voc-density
urn:miot-spec-v2:device:water-purifier:0000A013:roswan-lte01:1:0000D05A:
prop.4.1:
unit: ppm
prop.4.2:
unit: ppm
urn:miot-spec-v2:device:water-purifier:0000A013:yunmi-s20:1: # yunmi.waterpuri.s20
prop.4.1: # tds-in
unit: ppm
prop.4.2: # tds-out
unit: ppm
urn:miot-spec-v2:device:relay:0000A03D:lumi-c2acn01:1:
prop.4.1:
unit: kWh
urn:miot-spec-v2:device:bath-heater:0000A028:xiaomi-s1:1:
prop.4.4:
name: fan-level-ventilation

View File

@ -48,9 +48,11 @@ Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor import SensorStateClass from homeassistant.components.sensor import SensorStateClass
from homeassistant.components.event import EventDeviceClass from homeassistant.components.event import EventDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
EntityCategory,
LIGHT_LUX, LIGHT_LUX,
UnitOfEnergy, UnitOfEnergy,
UnitOfPower, UnitOfPower,
@ -137,7 +139,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': { 'optional': {
'properties': {'mode', 'target-humidity'} 'properties': {'mode', 'target-humidity'}
} }
}, }
}, },
'optional': { 'optional': {
'environment': { 'environment': {
@ -163,8 +165,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'continue-sweep', 'continue-sweep',
'stop-and-gocharge' 'stop-and-gocharge'
} }
}, }
} }
}, },
'optional': { 'optional': {
@ -177,9 +178,9 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'required': { 'required': {
'properties': { 'properties': {
'battery-level': {'read'} 'battery-level': {'read'}
}, }
} }
}, }
}, },
'entity': 'vacuum' 'entity': 'vacuum'
}, },
@ -195,7 +196,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'target-humidity'} 'properties': {'target-humidity'}
}, }
} }
}, },
'optional': { 'optional': {
@ -236,7 +237,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'properties': { 'properties': {
'target-temperature', 'mode', 'fan-level', 'target-temperature', 'mode', 'fan-level',
'temperature'} 'temperature'}
}, }
} }
}, },
'optional': { 'optional': {
@ -245,7 +246,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': { 'optional': {
'properties': {'temperature', 'relative-humidity'} 'properties': {'temperature', 'relative-humidity'}
} }
}, }
}, },
'entity': 'thermostat' 'entity': 'thermostat'
}, },
@ -259,7 +260,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'target-temperature', 'heat-level'} 'properties': {'target-temperature', 'heat-level'}
}, }
} }
}, },
'optional': { 'optional': {
@ -268,20 +269,21 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': { 'optional': {
'properties': {'temperature', 'relative-humidity'} 'properties': {'temperature', 'relative-humidity'}
} }
}, }
}, },
'entity': 'heater' 'entity': 'heater'
}, },
'bath-heater': { 'bath-heater': {
'required': { 'required': {
'ptc-bath-heater': { 'ptc-bath-heater': {
'required': {}, 'required': {
'optional': {
'properties': { 'properties': {
'target-temperature', 'heat-level', 'mode':{'read', 'write'}
'temperature', 'mode'
} }
}, },
'optional': {
'properties': {'target-temperature', 'temperature'}
}
} }
}, },
'optional': { 'optional': {
@ -291,7 +293,13 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'properties': { 'properties': {
'on', 'fan-level', 'horizontal-swing', 'vertical-swing' 'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
} }
}, }
},
'environment': {
'required': {},
'optional': {
'properties': {'temperature'}
}
} }
}, },
'entity': 'bath-heater', 'entity': 'bath-heater',
@ -307,7 +315,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'mode', 'temperature'} 'properties': {'mode', 'temperature'}
}, }
} }
}, },
'optional': {}, 'optional': {},
@ -330,7 +338,8 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'events': set<event instance name: str>, 'events': set<event instance name: str>,
'actions': set<action instance name: str> 'actions': set<action instance name: str>
}, },
'entity': str 'entity': str,
'entity_category'?: str
} }
} }
""" """
@ -348,10 +357,23 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}, },
'entity': 'light' 'entity': 'light'
}, },
'indicator-light': 'light',
'ambient-light': 'light', 'ambient-light': 'light',
'night-light': 'light', 'night-light': 'light',
'white-light': 'light', 'white-light': 'light',
'indicator-light': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'mode', 'brightness',
}
},
'entity': 'light',
'entity_category': EntityCategory.CONFIG
},
'fan': { 'fan': {
'required': { 'required': {
'properties': { 'properties': {
@ -366,6 +388,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}, },
'fan-control': 'fan', 'fan-control': 'fan',
'ceiling-fan': 'fan', 'ceiling-fan': 'fan',
'air-fresh': 'fan',
'water-heater': { 'water-heater': {
'required': { 'required': {
'properties': { 'properties': {
@ -385,14 +408,27 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': { 'properties': {
'motor-control', 'status', 'current-position', 'target-position' 'status', 'current-position', 'target-position'
} }
}, },
'entity': 'cover' 'entity': 'cover'
}, },
'window-opener': 'curtain', 'window-opener': 'curtain',
'motor-controller': 'curtain', 'motor-controller': 'curtain',
'airer': 'curtain' 'airer': 'curtain',
'air-conditioner': {
'required': {
'properties': {
'on': {'read', 'write'},
'mode': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'target-humidity'}
},
'entity': 'air-conditioner'
}
} }
"""SPEC_PROP_TRANS_MAP """SPEC_PROP_TRANS_MAP
@ -419,12 +455,28 @@ SPEC_PROP_TRANS_MAP: dict = {
'format': {'int', 'float'}, 'format': {'int', 'float'},
'access': {'read'} 'access': {'read'}
}, },
'binary_sensor': {
'format': {'bool', 'int'},
'access': {'read'}
},
'switch': { 'switch': {
'format': {'bool'}, 'format': {'bool'},
'access': {'read', 'write'} 'access': {'read', 'write'}
} }
}, },
'properties': { 'properties': {
'submersion-state': {
'device_class': BinarySensorDeviceClass.MOISTURE,
'entity': 'binary_sensor'
},
'contact-state': {
'device_class': BinarySensorDeviceClass.DOOR,
'entity': 'binary_sensor'
},
'occupancy-status': {
'device_class': BinarySensorDeviceClass.OCCUPANCY,
'entity': 'binary_sensor',
},
'temperature': { 'temperature': {
'device_class': SensorDeviceClass.TEMPERATURE, 'device_class': SensorDeviceClass.TEMPERATURE,
'entity': 'sensor', 'entity': 'sensor',
@ -471,7 +523,11 @@ SPEC_PROP_TRANS_MAP: dict = {
'entity': 'sensor', 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT 'state_class': SensorStateClass.MEASUREMENT
}, },
'voc-density': 'tvoc-density', 'voc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT
},
'battery-level': { 'battery-level': {
'device_class': SensorDeviceClass.BATTERY, 'device_class': SensorDeviceClass.BATTERY,
'entity': 'sensor', 'entity': 'sensor',
@ -525,12 +581,6 @@ SPEC_PROP_TRANS_MAP: dict = {
'entity': 'sensor', 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT, 'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPower.WATT 'unit_of_measurement': UnitOfPower.WATT
},
'total-battery': {
'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor',
'state_class': SensorStateClass.TOTAL_INCREASING,
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
} }
} }
} }

View File

@ -88,7 +88,7 @@ class Number(MIoTPropertyEntity, NumberEntity):
if self.spec.external_unit: if self.spec.external_unit:
self._attr_native_unit_of_measurement = self.spec.external_unit self._attr_native_unit_of_measurement = self.spec.external_unit
# Set icon # Set icon
if self.spec.icon: if self.spec.icon and not self.device_class:
self._attr_icon = self.spec.icon self._attr_icon = self.spec.icon
# Set value range # Set value range
if self._value_range: if self._value_range:

View File

@ -110,13 +110,13 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
self._attr_native_unit_of_measurement = list( self._attr_native_unit_of_measurement = list(
unit_sets)[0] if unit_sets else None unit_sets)[0] if unit_sets else None
# Set suggested precision # Set suggested precision
if spec.format_ in {int, float}: if spec.format_ in {int, float} and spec.expr is None:
self._attr_suggested_display_precision = spec.precision self._attr_suggested_display_precision = spec.precision
# Set state_class # Set state_class
if spec.state_class: if spec.state_class:
self._attr_state_class = spec.state_class self._attr_state_class = spec.state_class
# Set icon # Set icon
if spec.icon: if spec.icon and not self.device_class:
self._attr_icon = spec.icon self._attr_icon = spec.icon
@property @property