Compare commits

..

19 Commits
v0.2.1 ... main

Author SHA1 Message Date
Li Shuzhen
cb34b6ce46
chore: change issue_template (#964)
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-05 11:44:38 +08:00
Li Shuzhen
d0a7940c59
feat: support _attr_hvac_action of the climate entity (#956) 2025-04-05 11:38:03 +08:00
Li Shuzhen
899d616da4
feat: custom defined MIoT-Spec-V2 instance (#953) 2025-04-05 11:37:17 +08:00
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
18 changed files with 907 additions and 491 deletions

View File

@ -1,7 +1,7 @@
name: Bug Report / 报告问题 name: Bug Report / 报告问题
description: Create a report to help us improve. / 报告问题以帮助我们改进 description: Create a report to help us improve. / 报告问题以帮助我们改进
body: body:
- type: input - type: textarea
attributes: attributes:
label: Describe the Bug / 描述问题 label: Describe the Bug / 描述问题
description: | description: |

View File

@ -1,13 +1,49 @@
# 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 ## v0.2.1
### Added ### Added
- Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833) - Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833)
### Changed ### Changed
- Change paho-mqtt version to adapt Home Assistant 2025.03. [#839](https://github.com/XiaoMi/ha_xiaomi_home/pull/839) - 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) - Revert to use multi_lang.json. [#834](https://github.com/XiaoMi/ha_xiaomi_home/pull/834)
### Fixed ### Fixed
- Fix the opening and the closing status of linp.wopener.wd1lb. [#826](https://github.com/XiaoMi/ha_xiaomi_home/pull/826) - 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 format type of the wind-reverse property. [#810](https://github.com/XiaoMi/ha_xiaomi_home/pull/810)

View File

@ -156,7 +156,8 @@ async def async_setup_entry(
device.entity_list[platform].remove(entity) device.entity_list[platform].remove(entity)
entity_id = device.gen_service_entity_id( entity_id = device.gen_service_entity_id(
ha_domain=platform, ha_domain=platform,
siid=entity.spec.iid) # type: ignore siid=entity.spec.iid,
description=entity.spec.description)
if er.async_get(entity_id_or_uuid=entity_id): if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id) er.async_remove(entity_id=entity_id)
if platform in device.prop_list: if platform in device.prop_list:

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,10 +51,11 @@ 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,
ATTR_TEMPERATURE, HVACMode, ClimateEntity, ClimateEntityFeature) ATTR_TEMPERATURE, HVACMode, HVACAction, ClimateEntity, ClimateEntityFeature)
from .miot.const import DOMAIN from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
@ -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._attr_supported_features |= (
ClimateEntityFeature.TURN_OFF)
self._prop_on = prop self._prop_on = prop
break
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."""
@ -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,6 +230,7 @@ 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
@ -365,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]:
@ -387,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]:
@ -418,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."""
@ -447,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')
@ -462,6 +473,13 @@ class Heater(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
return (HVACMode.HEAT if self.get_prop_value( return (HVACMode.HEAT if self.get_prop_value(
prop=self._prop_on) else HVACMode.OFF) prop=self._prop_on) else HVACMode.OFF)
@property
def hvac_action(self) -> Optional[HVACAction]:
"""The current hvac action."""
if self.hvac_mode == HVACMode.HEAT:
return HVACAction.HEATING
return HVACAction.OFF
class AirConditioner(FeatureOnOff, FeatureTargetTemperature, class AirConditioner(FeatureOnOff, FeatureTargetTemperature,
FeatureTargetHumidity, FeatureTemperature, FeatureHumidity, FeatureTargetHumidity, FeatureTemperature, FeatureHumidity,
@ -482,10 +500,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
@ -549,6 +569,23 @@ class AirConditioner(FeatureOnOff, FeatureTargetTemperature,
prop=self._prop_mode)) prop=self._prop_mode))
if self._prop_mode else None) if self._prop_mode else None)
@property
def hvac_action(self) -> Optional[HVACAction]:
"""The current hvac action."""
if self.hvac_mode is None:
return None
if self.hvac_mode == HVACMode.OFF:
return HVACAction.OFF
if self.hvac_mode == HVACMode.FAN_ONLY:
return HVACAction.FAN
if self.hvac_mode == HVACMode.COOL:
return HVACAction.COOLING
if self.hvac_mode == HVACMode.HEAT:
return HVACAction.HEATING
if self.hvac_mode == HVACMode.DRY:
return HVACAction.DRYING
return HVACAction.IDLE
def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None:
del prop del prop
if not isinstance(value, str): if not isinstance(value, str):
@ -605,7 +642,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]]
@ -620,50 +657,43 @@ 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: 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
self._hvac_mode_map = {} self._hvac_mode_map = {}
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'}:
} and (HVACMode.OFF not in list(
self._hvac_mode_map.values())):
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 {'ventilate'}: self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]
self._hvac_mode_map[item.value] = HVACMode.COOL else:
elif item.name in {'heat', 'quick_heat' _LOGGER.error('no idle mode, %s', self.entity_id)
} and (HVACMode.HEAT not in list( # preset modes
self._hvac_mode_map.values())): self._init_preset_modes('ptc-bath-heater', 'mode')
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: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the target hvac mode.""" """Set the target hvac mode."""
if self._prop_mode is None: if self._prop_mode is None or hvac_mode != HVACMode.OFF:
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."""
return (self.get_map_value(map_=self._hvac_mode_map, if self._prop_mode is None:
key=self.get_prop_value( return None
prop=self._prop_mode)) current_mode = self.get_prop_value(prop=self._prop_mode)
if self._prop_mode else None) 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, class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
@ -678,6 +708,8 @@ class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
self._attr_icon = 'mdi:thermostat' self._attr_icon = 'mdi:thermostat'
# hvac modes # hvac modes
self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF] self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]
# on/off
self._init_on_off('thermostat', 'on')
# preset modes # preset modes
self._init_preset_modes('thermostat', 'mode') self._init_preset_modes('thermostat', 'mode')
@ -706,6 +738,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')
@ -720,3 +754,10 @@ class ElectricBlanket(FeatureOnOff, FeatureTargetTemperature,
"""The current hvac mode.""" """The current hvac mode."""
return (HVACMode.HEAT if self.get_prop_value( return (HVACMode.HEAT if self.get_prop_value(
prop=self._prop_on) else HVACMode.OFF) prop=self._prop_on) else HVACMode.OFF)
@property
def hvac_action(self) -> Optional[HVACAction]:
"""The current hvac action."""
if self.hvac_mode == HVACMode.OFF:
return HVACAction.OFF
return HVACAction.HEATING

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

@ -25,7 +25,7 @@
"cryptography", "cryptography",
"psutil" "psutil"
], ],
"version": "v0.2.1", "version": "v0.2.4",
"zeroconf": [ "zeroconf": [
"_miot-central._tcp.local." "_miot-central._tcp.local."
] ]

View File

@ -345,10 +345,11 @@ class MIoTDevice:
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_' f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}') f'{self._model_strs[-1][:20]}')
def gen_service_entity_id(self, ha_domain: str, siid: int) -> str: def gen_service_entity_id(self, ha_domain: str, siid: int,
description: str) -> str:
return ( return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_' f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_s_{siid}') f'{self._model_strs[-1][:20]}_s_{siid}_{description}')
def gen_prop_entity_id( def gen_prop_entity_id(
self, ha_domain: str, spec_name: str, siid: int, piid: int self, ha_domain: str, spec_name: str, siid: int, piid: int
@ -591,13 +592,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,7 +895,8 @@ class MIoTServiceEntity(Entity):
self._attr_name = f' {self.entity_data.spec.description_trans}' self._attr_name = f' {self.entity_data.spec.description_trans}'
elif isinstance(self.entity_data.spec, MIoTSpecService): elif isinstance(self.entity_data.spec, MIoTSpecService):
self.entity_id = miot_device.gen_service_entity_id( self.entity_id = miot_device.gen_service_entity_id(
DOMAIN, siid=self.entity_data.spec.iid) DOMAIN, siid=self.entity_data.spec.iid,
description=self.entity_data.spec.description)
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}')

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)

File diff suppressed because it is too large Load Diff

View File

@ -168,5 +168,20 @@
"service:016:action:001": "中键确认", "service:016:action:001": "中键确认",
"service:017: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

@ -0,0 +1,22 @@
{
"urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1": [
{
"iid": 3,
"type": "urn:miot-spec-v2:service:light:00007802:hyd-lyjpro:1",
"description": "Moon Light",
"properties": [
{
"iid": 2,
"type": "urn:miot-spec-v2:property:on:00000006:hyd-lyjpro:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
]
}

View File

@ -5,6 +5,9 @@ urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
- 15.* - 15.*
services: services:
- '10' - '10'
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:
properties:
- '3.2'
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01: urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
properties: properties:
- '5.1' - '5.1'

View File

@ -1,3 +1,70 @@
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:air-conditioner:0000A004:xiaomi-m9:6:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:
prop.2.5:
name: voc-density
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1:
prop.2.3:
name: current-position-a
prop.2.8:
name: target-position-a
prop.2.9:
name: target-position-b
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1:
prop.2.3:
value-range:
- 0
- 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:mrbond-m33a:1:
prop.2.3:
name: current-position-a
prop.2.11:
name: current-position-b
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: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:xiaomi-s1:1:
prop.4.4:
name: fan-level-ventilation
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:gateway:0000A019:lumi-mcn001:1:
prop.2.1:
access:
- read
- notify
prop.2.2:
icon: mdi:ip
prop.2.3:
access:
- read
- notify
prop.2.5:
access:
- read
- notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
prop.2.1: prop.2.1:
name: access-mode name: access-mode
@ -14,25 +81,63 @@ 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:light:0000A001:shhf-sfla12:1:
prop.8.11:
name: on-a
urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-acn001:1:
prop.3.2:
access:
- read
- notify
unit: mV
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:izq-24:2:0000C824:
prop.2.6:
unit: cm
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:linp-hb01:2:0000C824:
prop.3.3:
unit: m
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:chuangmi-212a01:3:
prop.5.1: prop.5.1:
name: power-consumption expr: round(src_value*6/1000000, 3)
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: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
expr: round(src_value/1000, 3) expr: round(src_value/1000, 3)
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: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-v3:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1:
prop.11.1: prop.11.1:
name: power-consumption name: power-consumption
expr: round(src_value/100, 2) expr: round(src_value/100, 2)
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1 urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1
urn:miot-spec-v2:device:outlet:0000A002:qmi-psv3:1:0000C816:
prop.3.3:
unit: mV
prop.3.4:
unit: mA
urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816: 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: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:relay:0000A03D:lumi-c2acn01:1:
prop.4.1:
unit: kWh
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
@ -42,19 +147,48 @@ urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
name: upload-speed name: upload-speed
icon: mdi:upload icon: mdi:upload
unit: B/s unit: B/s
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1: urn:miot-spec-v2:device:thermostat:0000A031:suittc-wk168:1:
prop.2.3: prop.2.3:
value-range:
- 0
- 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-list:
- value: 1 - value: 1
description: low description: '1'
- value: 128 - value: 2
description: medium description: '2'
- value: 255 - value: 3
description: high 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: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:
prop.4.1:
unit: ppm
prop.4.2:
unit: ppm

View File

@ -48,6 +48,7 @@ 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,
@ -138,7 +139,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': { 'optional': {
'properties': {'mode', 'target-humidity'} 'properties': {'mode', 'target-humidity'}
} }
}, }
}, },
'optional': { 'optional': {
'environment': { 'environment': {
@ -164,8 +165,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'continue-sweep', 'continue-sweep',
'stop-and-gocharge' 'stop-and-gocharge'
} }
}, }
} }
}, },
'optional': { 'optional': {
@ -178,9 +178,9 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'required': { 'required': {
'properties': { 'properties': {
'battery-level': {'read'} 'battery-level': {'read'}
},
} }
}, }
}
}, },
'entity': 'vacuum' 'entity': 'vacuum'
}, },
@ -196,7 +196,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'target-humidity'} 'properties': {'target-humidity'}
}, }
} }
}, },
'optional': { 'optional': {
@ -237,7 +237,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'properties': { 'properties': {
'target-temperature', 'mode', 'fan-level', 'target-temperature', 'mode', 'fan-level',
'temperature'} 'temperature'}
}, }
} }
}, },
'optional': { 'optional': {
@ -246,7 +246,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': { 'optional': {
'properties': {'temperature', 'relative-humidity'} 'properties': {'temperature', 'relative-humidity'}
} }
}, }
}, },
'entity': 'thermostat' 'entity': 'thermostat'
}, },
@ -260,7 +260,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'target-temperature', 'heat-level'} 'properties': {'target-temperature', 'heat-level'}
}, }
} }
}, },
'optional': { 'optional': {
@ -269,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': {
@ -292,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',
@ -308,7 +315,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'mode', 'temperature'} 'properties': {'mode', 'temperature'}
}, }
} }
}, },
'optional': {}, 'optional': {},
@ -381,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': {
@ -400,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
@ -434,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',
@ -486,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',
@ -540,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

View File

@ -15,14 +15,13 @@ TRANS_RELATIVE_PATH: str = path.join(
MIOT_I18N_RELATIVE_PATH: str = path.join( MIOT_I18N_RELATIVE_PATH: str = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n') ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
SPEC_BOOL_TRANS_FILE = path.join( SPEC_BOOL_TRANS_FILE = path.join(
ROOT_PATH, ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
'../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
SPEC_FILTER_FILE = path.join( SPEC_FILTER_FILE = path.join(
ROOT_PATH, ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml') SPEC_ADD_FILE = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_add.json')
SPEC_MODIFY_FILE = path.join( SPEC_MODIFY_FILE = path.join(
ROOT_PATH, ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
'../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
def load_json_file(file_path: str) -> Optional[dict]: def load_json_file(file_path: str) -> Optional[dict]:
@ -30,7 +29,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
with open(file_path, 'r', encoding='utf-8') as file: with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file) return json.load(file)
except FileNotFoundError: except FileNotFoundError:
_LOGGER.info('%s is not found.', file_path,) _LOGGER.info('%s is not found.', file_path)
return None return None
except json.JSONDecodeError: except json.JSONDecodeError:
_LOGGER.info('%s is not a valid JSON file.', file_path) _LOGGER.info('%s is not a valid JSON file.', file_path)
@ -56,9 +55,12 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
def save_yaml_file(file_path: str, data: dict) -> None: def save_yaml_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file: with open(file_path, 'w', encoding='utf-8') as file:
yaml.safe_dump( yaml.safe_dump(data,
data, file, default_flow_style=False, file,
allow_unicode=True, indent=2, sort_keys=False) default_flow_style=False,
allow_unicode=True,
indent=2,
sort_keys=False)
def dict_str_str(d: dict) -> bool: def dict_str_str(d: dict) -> bool:
@ -132,9 +134,108 @@ def bool_trans(d: dict) -> bool:
for key, trans in d['translate'].items(): for key, trans in d['translate'].items():
trans_keys: set[str] = set(trans.keys()) trans_keys: set[str] = set(trans.keys())
if set(trans.keys()) != default_keys: if set(trans.keys()) != default_keys:
_LOGGER.info( _LOGGER.info('bool trans inconsistent, %s, %s, %s', key,
'bool trans inconsistent, %s, %s, %s', default_keys, trans_keys)
key, default_keys, trans_keys) return False
return True
def spec_add(data: dict) -> bool:
"""dict[str, list[dict[str, int| str | list]]]"""
if not isinstance(data, dict):
return False
for urn, content in data.items():
if not isinstance(urn, str) or not isinstance(content, (list, str)):
return False
if isinstance(content, str):
continue
for service in content:
if ('iid' not in service) or ('type' not in service) or (
'description'
not in service) or (('properties' not in service) and
('actions' not in service) and
('events' not in service)):
return False
type_strs: list[str] = service['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
if 'properties' in service:
if not isinstance(service['properties'], list):
return False
for prop in service['properties']:
if ('iid' not in prop) or ('type' not in prop) or (
'description' not in prop) or (
'format' not in prop) or ('access' not in prop):
return False
if not isinstance(prop['iid'], int) or not isinstance(
prop['type'], str) or not isinstance(
prop['description'], str) or not isinstance(
prop['format'], str) or not isinstance(
prop['access'], list):
return False
type_strs = prop['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for access in prop['access']:
if access not in ['read', 'write', 'notify']:
return False
if 'value-range' in prop:
if not isinstance(prop['value-range'], list):
return False
for value in prop['value-range']:
if not isinstance(value, (int, float)):
return False
if 'value-list' in prop:
if not isinstance(prop['value-list'], list):
return False
for item in prop['value-list']:
if 'value' not in item or 'description' not in item:
return False
if not isinstance(item['value'],
int) or not isinstance(
item['description'], str):
return False
if 'actions' in service:
if not isinstance(service['actions'], list):
return False
for action in service['actions']:
if ('iid' not in action) or ('type' not in action) or (
'description' not in action) or (
'in' not in action) or ('out' not in action):
return False
if not isinstance(action['iid'], int) or not isinstance(
action['type'], str) or not isinstance(
action['description'], str) or not isinstance(
action['in'], list) or not isinstance(
action['out'], list):
return False
type_strs = action['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for param in action['in']:
if not isinstance(param, int):
return False
for param in action['out']:
if not isinstance(param, int):
return False
if 'events' in service:
if not isinstance(service['events'], list):
return False
for event in service['events']:
if ('iid' not in event) or ('type' not in event) or (
'description' not in event) or ('arguments'
not in event):
return False
if not isinstance(event['iid'], int) or not isinstance(
event['type'], str) or not isinstance(
event['description'], str) or not isinstance(
event['arguments'], list):
return False
type_strs = event['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for param in event['arguments']:
if not isinstance(param, int):
return False return False
return True return True
@ -159,25 +260,22 @@ def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
_LOGGER.info('invalid type') _LOGGER.info('invalid type')
return False return False
if dict1.keys() != dict2.keys(): if dict1.keys() != dict2.keys():
_LOGGER.info( _LOGGER.info('inconsistent key values, %s, %s', dict1.keys(),
'inconsistent key values, %s, %s', dict1.keys(), dict2.keys()) dict2.keys())
return False return False
for key in dict1: for key in dict1:
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict): if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
if not compare_dict_structure(dict1[key], dict2[key]): if not compare_dict_structure(dict1[key], dict2[key]):
_LOGGER.info( _LOGGER.info('inconsistent key values, dict, %s', key)
'inconsistent key values, dict, %s', key)
return False return False
elif isinstance(dict1[key], list) and isinstance(dict2[key], list): elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
if not all( if not all(
isinstance(i, type(j)) isinstance(i, type(j))
for i, j in zip(dict1[key], dict2[key])): for i, j in zip(dict1[key], dict2[key])):
_LOGGER.info( _LOGGER.info('inconsistent key values, list, %s', key)
'inconsistent key values, list, %s', key)
return False return False
elif not isinstance(dict1[key], type(dict2[key])): elif not isinstance(dict1[key], type(dict2[key])):
_LOGGER.info( _LOGGER.info('inconsistent key values, type, %s', key)
'inconsistent key values, type, %s', key)
return False return False
return True return True
@ -200,6 +298,12 @@ def sort_spec_filter(file_path: str):
return filter_data return filter_data
def sort_spec_add(file_path: str):
filter_data = load_json_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error'
return dict(sorted(filter_data.items()))
def sort_spec_modify(file_path: str): def sort_spec_modify(file_path: str):
filter_data = load_yaml_file(file_path=file_path) filter_data = load_yaml_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error' assert isinstance(filter_data, dict), f'{file_path} format error'
@ -222,6 +326,14 @@ def test_spec_filter():
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error' assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
@pytest.mark.github
def test_spec_add():
data = load_json_file(SPEC_ADD_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_ADD_FILE} failed'
assert spec_add(data), f'{SPEC_ADD_FILE} format error'
@pytest.mark.github @pytest.mark.github
def test_spec_modify(): def test_spec_modify():
data = load_yaml_file(SPEC_MODIFY_FILE) data = load_yaml_file(SPEC_MODIFY_FILE)
@ -255,7 +367,8 @@ def test_miot_lang_integrity():
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from miot.const import INTEGRATION_LANGUAGES from miot.const import INTEGRATION_LANGUAGES
integration_lang_list: list[str] = [ integration_lang_list: list[str] = [
f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())] f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())
]
translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH)) translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH))
assert len(translations_names) == len(integration_lang_list) assert len(translations_names) == len(integration_lang_list)
assert translations_names == set(integration_lang_list) assert translations_names == set(integration_lang_list)
@ -271,21 +384,18 @@ def test_miot_lang_integrity():
default_dict = load_json_file( default_dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0])) path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]: for name in list(integration_lang_list)[1:]:
compare_dict = load_json_file( compare_dict = load_json_file(path.join(TRANS_RELATIVE_PATH, name))
path.join(TRANS_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict): if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info( _LOGGER.info('compare_dict_structure failed /translations, %s',
'compare_dict_structure failed /translations, %s', name) name)
assert False assert False
# Check i18n files structure # Check i18n files structure
default_dict = load_json_file( default_dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0])) path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]: for name in list(integration_lang_list)[1:]:
compare_dict = load_json_file( compare_dict = load_json_file(path.join(MIOT_I18N_RELATIVE_PATH, name))
path.join(MIOT_I18N_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict): if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info( _LOGGER.info('compare_dict_structure failed /miot/i18n, %s', name)
'compare_dict_structure failed /miot/i18n, %s', name)
assert False assert False
@ -303,12 +413,21 @@ def test_miot_data_sort():
f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path' f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'
' and run the following command sorting, ', ' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py') 'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps( assert json.dumps(load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
sort_spec_filter(file_path=SPEC_FILTER_FILE)), ( sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
f'{SPEC_FILTER_FILE} not sorted, goto project root path' f'{SPEC_FILTER_FILE} not sorted, goto project root path'
' and run the following command sorting, ', ' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py') 'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_json_file(file_path=SPEC_ADD_FILE)) == json.dumps(
sort_spec_add(file_path=SPEC_ADD_FILE)), (
f'{SPEC_ADD_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_yaml_file(file_path=SPEC_MODIFY_FILE)) == json.dumps(
sort_spec_modify(file_path=SPEC_MODIFY_FILE)), (
f'{SPEC_MODIFY_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
@pytest.mark.update @pytest.mark.update
@ -319,6 +438,9 @@ def test_sort_spec_data():
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE) sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data) save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE) _LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
sort_data = sort_spec_add(file_path=SPEC_ADD_FILE)
save_json_file(file_path=SPEC_ADD_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_ADD_FILE)
sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE) sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE)
save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data) save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_MODIFY_FILE) _LOGGER.info('%s formatted.', SPEC_MODIFY_FILE)