Compare commits

...

24 Commits
v0.2.0 ... main

Author SHA1 Message Date
Joel
c6be6be1ec
fix: initialization problem of _attr_fan_modes (#955)
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-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
## 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
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
def is_on(self) -> bool:
"""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

View File

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

View File

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

View File

@ -46,6 +46,7 @@ off Xiaomi or its affiliates' products.
Event entities for Xiaomi Home.
"""
from __future__ import annotations
import logging
from typing import Any
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.const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
@ -89,4 +92,5 @@ class Event(MIoTEventEntity, EventEntity):
self, name: str, arguments: dict[str, Any] | None = None
) -> None:
"""An event is occurred."""
_LOGGER.debug('%s, attributes: %s', name, str(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._prop_horizontal_swing = prop
elif prop.name == 'wind-reverse':
if prop.format_ == 'bool':
if prop.format_ == bool:
self._prop_wind_reverse_forward = False
self._prop_wind_reverse_reverse = True
elif prop.value_list:
@ -186,7 +186,7 @@ class Fan(MIoTServiceEntity, FanEntity):
or self._prop_wind_reverse_reverse is None
):
# NOTICE: Value may be 0 or False
_LOGGER.info(
_LOGGER.error(
'invalid wind-reverse, %s', self.entity_id)
continue
self._attr_supported_features |= FanEntityFeature.DIRECTION

View File

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

View File

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

View File

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

View File

@ -1215,9 +1215,10 @@ class MipsLocalClient(_MipsClient):
or 'eiid' 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
if 'arguments' not in msg:
self.log_info('wrong event msg, %s', payload)
msg['arguments'] = []
if handler:
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
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_storage import MIoTStorage
@ -465,7 +465,7 @@ class _MIoTSpecBase:
iid: int
type_: str
description: str
description_trans: str
description_trans: Optional[str]
proprietary: bool
need_filter: bool
name: str
@ -476,6 +476,7 @@ class _MIoTSpecBase:
device_class: Any
state_class: Any
external_unit: Any
entity_category: Optional[str]
spec_id: int
@ -494,6 +495,7 @@ class _MIoTSpecBase:
self.device_class = None
self.state_class = None
self.external_unit = None
self.entity_category = None
self.spec_id = hash(f'{self.type_}.{self.iid}')
@ -837,6 +839,7 @@ class _MIoTSpecMultiLang:
"""MIoT SPEC multi lang class."""
# pylint: disable=broad-exception-caught
_DOMAIN: str = 'miot_specs_multi_lang'
_MULTI_LANG_FILE = 'specs/multi_lang.json'
_lang: str
_storage: MIoTStorage
_main_loop: asyncio.AbstractEventLoop
@ -892,6 +895,25 @@ class _MIoTSpecMultiLang:
except Exception as err:
trans_local = {}
_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
if not trans_cache:
if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud:
@ -1183,6 +1205,9 @@ class _SpecModify:
if isinstance(self._selected, str):
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]:
return self.__get_prop_item(siid=siid, piid=piid, key='unit')
@ -1205,6 +1230,13 @@ class _SpecModify:
return None
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]:
if not self._selected:
return None
@ -1444,10 +1476,12 @@ class MIoTSpecParser:
key=':'.join(p_type_strs[:5]))
or property_['description']
or spec_prop.name)
if 'value-range' in property_:
spec_prop.value_range = property_['value-range']
elif 'value-list' in property_:
v_list: list[dict] = property_['value-list']
# Modify value-list before translation
v_list: list[dict] = self._spec_modify.get_prop_value_list(
siid=service['iid'], piid=property_['iid'])
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):
if v['description'].strip() == '':
v['description'] = f'v_{v["value"]}'
@ -1461,6 +1495,8 @@ class MIoTSpecParser:
f'{v["description"]}')
or v['name'])
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':
v_tag = ':'.join(p_type_strs[:5])
v_descriptions = (
@ -1485,6 +1521,10 @@ class MIoTSpecParser:
siid=service['iid'], piid=property_['iid'])
if 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
for event in service.get('events', []):
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
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:outlet:0000A002:chuangmi-212a01:1:
prop.5.1:
name: power-consumption
expr: round(src_value/1000, 3)
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: 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:1
urn:miot-spec-v2:device:gateway:0000A019:lumi-mcn001:1: # lumi.gateway.mcn001
prop.2.1: # access-mode
access:
- read
- notify
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:
prop.2.2:
name: power-consumption
@ -33,6 +42,23 @@ urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816:
prop.3.1:
name: electric-power
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:
prop.2.1:
name: download-speed
@ -49,3 +75,110 @@ urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5: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: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 SensorStateClass
from homeassistant.components.event import EventDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
EntityCategory,
LIGHT_LUX,
UnitOfEnergy,
UnitOfPower,
@ -137,7 +139,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': {
'properties': {'mode', 'target-humidity'}
}
},
}
},
'optional': {
'environment': {
@ -163,8 +165,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'continue-sweep',
'stop-and-gocharge'
}
},
}
}
},
'optional': {
@ -177,9 +178,9 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'required': {
'properties': {
'battery-level': {'read'}
},
}
}
},
}
},
'entity': 'vacuum'
},
@ -195,7 +196,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
},
'optional': {
'properties': {'target-humidity'}
},
}
}
},
'optional': {
@ -236,7 +237,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'properties': {
'target-temperature', 'mode', 'fan-level',
'temperature'}
},
}
}
},
'optional': {
@ -245,7 +246,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
},
}
},
'entity': 'thermostat'
},
@ -259,7 +260,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
},
'optional': {
'properties': {'target-temperature', 'heat-level'}
},
}
}
},
'optional': {
@ -268,20 +269,21 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
},
}
},
'entity': 'heater'
},
'bath-heater': {
'required': {
'ptc-bath-heater': {
'required': {},
'optional': {
'required': {
'properties': {
'target-temperature', 'heat-level',
'temperature', 'mode'
'mode':{'read', 'write'}
}
},
'optional': {
'properties': {'target-temperature', 'temperature'}
}
}
},
'optional': {
@ -291,7 +293,13 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'properties': {
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
}
},
}
},
'environment': {
'required': {},
'optional': {
'properties': {'temperature'}
}
}
},
'entity': 'bath-heater',
@ -307,7 +315,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
},
'optional': {
'properties': {'mode', 'temperature'}
},
}
}
},
'optional': {},
@ -330,7 +338,8 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'events': set<event 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'
},
'indicator-light': 'light',
'ambient-light': 'light',
'night-light': 'light',
'white-light': 'light',
'indicator-light': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'mode', 'brightness',
}
},
'entity': 'light',
'entity_category': EntityCategory.CONFIG
},
'fan': {
'required': {
'properties': {
@ -366,6 +388,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
},
'fan-control': 'fan',
'ceiling-fan': 'fan',
'air-fresh': 'fan',
'water-heater': {
'required': {
'properties': {
@ -385,14 +408,27 @@ SPEC_SERVICE_TRANS_MAP: dict = {
},
'optional': {
'properties': {
'motor-control', 'status', 'current-position', 'target-position'
'status', 'current-position', 'target-position'
}
},
'entity': 'cover'
},
'window-opener': '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
@ -419,12 +455,28 @@ SPEC_PROP_TRANS_MAP: dict = {
'format': {'int', 'float'},
'access': {'read'}
},
'binary_sensor': {
'format': {'bool', 'int'},
'access': {'read'}
},
'switch': {
'format': {'bool'},
'access': {'read', 'write'}
}
},
'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': {
'device_class': SensorDeviceClass.TEMPERATURE,
'entity': 'sensor',
@ -471,7 +523,11 @@ SPEC_PROP_TRANS_MAP: dict = {
'entity': 'sensor',
'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': {
'device_class': SensorDeviceClass.BATTERY,
'entity': 'sensor',
@ -525,12 +581,6 @@ SPEC_PROP_TRANS_MAP: dict = {
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'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:
self._attr_native_unit_of_measurement = self.spec.external_unit
# Set icon
if self.spec.icon:
if self.spec.icon and not self.device_class:
self._attr_icon = self.spec.icon
# Set value range
if self._value_range:

View File

@ -110,13 +110,13 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
self._attr_native_unit_of_measurement = list(
unit_sets)[0] if unit_sets else None
# 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
# Set state_class
if spec.state_class:
self._attr_state_class = spec.state_class
# Set icon
if spec.icon:
if spec.icon and not self.device_class:
self._attr_icon = spec.icon
@property