29 Commits

Author SHA1 Message Date
00f24bd3e1 docs: update changelog and version to v0.3.1 (#1049)
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-29 09:27:24 +08:00
f384034854 Fix specs (#1037) 2025-04-29 09:11:17 +08:00
b0204ad9b7 fix: the humidifier property (#1035)
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-28 09:35:47 +08:00
b4ece958ac fix: set fan on/off state before set the percentage (#1031)
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-27 20:13:34 +08:00
db77af8a13 feat: make air-purifier as fan (#987)
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-27 16:11:55 +08:00
a9f1fc630d fix: zhimi.fan.v3 fan level (#1033) 2025-04-27 15:40:29 +08:00
51a27a1e30 Merge pull request #1032 from XiaoMi/revert-1027-zhimi.fan.v3
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
Revert "add device zhimi.fan.v3  prop.2.2"
2025-04-27 10:18:24 +08:00
2e0ea642a4 Revert "add device zhimi.fan.v3 prop.2.2" 2025-04-27 10:16:27 +08:00
80d962897a Merge pull request #1027 from XiaoMi/zhimi.fan.v3
add device zhimi.fan.v3  prop.2.2
2025-04-27 10:15:55 +08:00
d17784070d add device zhimi.fan.v3 prop.2.6 2025-04-27 09:56:05 +08:00
218c96e5e6 add device zhimi.fan.v3 prop.2.2 2025-04-25 14:33:07 +08:00
eacc0d02da fix: update device list error when there is no shared devices (#1024)
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-25 12:23:04 +08:00
23f0a2d360 docs: update changelog and version to v0.3.0 (#1022)
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-25 08:40:14 +08:00
3abccc2491 feat: import shared devices (#1021) 2025-04-25 08:29:11 +08:00
7a459de766 fix: ignore 'Event loop is closed' when unsub a closed event loop (#991)
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-24 17:55:15 +08:00
2f619ff51d fix: contact-state for linp.magnet.m1 and loock.safe.v1 (#977)
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-10 10:19:50 +08:00
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
d0a7940c59 feat: support _attr_hvac_action of the climate entity (#956) 2025-04-05 11:38:03 +08:00
899d616da4 feat: custom defined MIoT-Spec-V2 instance (#953) 2025-04-05 11:37:17 +08:00
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
77b0a4531b fix: fix some specs (#949) 2025-04-02 08:53:15 +08:00
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
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
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
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
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
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
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
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
21 changed files with 1067 additions and 594 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,4 +1,49 @@
# CHANGELOG # CHANGELOG
## v0.3.1
### Changed
- Setting the fan speed level when the fan is off will turning the fan on first. [#1031](https://github.com/XiaoMi/ha_xiaomi_home/pull/1031)
### Fixed
- Fix update device list error when there is no shared devices. [#1024](https://github.com/XiaoMi/ha_xiaomi_home/pull/1024)
- Fix the humidifier get_prop_value error when the property is None. [#1035](https://github.com/XiaoMi/ha_xiaomi_home/pull/1035)
- Fix the MIoT-Spec-V2 of zhimi.fan.v3 fan-level, cuco.plug.cp1md voltage and current, zimi.plug.zncz01 electric-power, giot.plug.v8icm power-consumption unit, yunmi.kettle.r3 tds unit, and dmaker.fan.p5 fan-level. [#1037](https://github.com/XiaoMi/ha_xiaomi_home/pull/1037)
## v0.3.0
注意v0.3.0 变更了部分实体 unique_id 的生成规则,如果勾选 xiaomi_home > 配置 > 更新实体转换规则,会导致部分实体已配置的自动化失效。如果想要避免重新配置大量自动化,可使用这个[补丁](https://github.com/XiaoMi/ha_xiaomi_home/pull/972)。
CAUTION: v0.3.0 changes the unique_id of some entities. If you check the option `xiaomi_home > CONFIGURE > Update entity conversion rules`, it may cause the automation settings for these entities to fail. To avoid having to reconfigure a large number of automation settings, you can use this [patch](https://github.com/XiaoMi/ha_xiaomi_home/pull/972).
### Added
- Import the devices in the shared homes and the separated shared devices. [#1021](https://github.com/XiaoMi/ha_xiaomi_home/pull/1021)
- Support _attr_hvac_action of the climate entity. [#956](https://github.com/XiaoMi/ha_xiaomi_home/pull/956)
- Add custom defined MIoT-Spec-V2 instance via spec_add.json. [#953](https://github.com/XiaoMi/ha_xiaomi_home/pull/953)
### Fixed
- Ignore 'Event loop is closed' when unsub a closed event loop. [#991](https://github.com/XiaoMi/ha_xiaomi_home/pull/991)
- Fix contact-state for linp.magnet.m1 and loock.safe.v1. [#977](https://github.com/XiaoMi/ha_xiaomi_home/pull/977)
- Fix the mode initialization error of aupu.bhf_light.s368m. [#955](https://github.com/XiaoMi/ha_xiaomi_home/pull/955)
- Fix the MIoT-Spec-V2 of lumi.gateway.mcn001, qmi.plug.psv3, lumi.motion.acn001, izq.sensor_occupy.24, linp.sensor_occupy.hb01 and yunmi.waterpuri.s20. [#949](https://github.com/XiaoMi/ha_xiaomi_home/pull/949)
## 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 ## 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 > 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 >

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._prop_on = prop
self._attr_supported_features |= ( break
ClimateEntityFeature.TURN_OFF)
self._prop_on = prop
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn on.""" """Turn on."""
@ -134,6 +133,7 @@ class FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity):
entity_data: MIoTEntityData) -> None: entity_data: MIoTEntityData) -> None:
"""Initialize the feature class.""" """Initialize the feature class."""
self._prop_target_temp = None self._prop_target_temp = None
self._attr_temperature_unit = None
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties # properties
@ -151,6 +151,10 @@ class FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity):
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE) ClimateEntityFeature.TARGET_TEMPERATURE)
self._prop_target_temp = prop self._prop_target_temp = prop
break
# temperature_unit is required by the climate entity
if not self._attr_temperature_unit:
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set the target temperature.""" """Set the target temperature."""
@ -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):
@ -620,22 +657,17 @@ 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'}:
if (HVACMode.OFF self._hvac_mode_map[item.value] = HVACMode.OFF
not in list(self._hvac_mode_map.values())): break
self._hvac_mode_map[item.value] = HVACMode.OFF if self._hvac_mode_map:
elif (HVACMode.AUTO self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]
not in list(self._hvac_mode_map.values())):
self._hvac_mode_map[item.value] = HVACMode.AUTO
self._attr_hvac_modes = list(self._hvac_mode_map.values())
if HVACMode.OFF in self._attr_hvac_modes:
self._prop_mode = prop
else: else:
_LOGGER.error('no idle mode, %s', self.entity_id) _LOGGER.error('no idle mode, %s', self.entity_id)
# preset modes # preset modes
@ -643,7 +675,7 @@ class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature,
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(
@ -656,13 +688,12 @@ class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature,
"""The current hvac mode.""" """The current hvac mode."""
if self._prop_mode is None: if self._prop_mode is None:
return None return None
mode_value = self.get_map_value( current_mode = self.get_prop_value(prop=self._prop_mode)
map_=self._hvac_mode_map, if current_mode is None:
key=self.get_prop_value(prop=self._prop_mode)) return None
if mode_value == HVACMode.OFF or mode_value is None: mode_value = self.get_map_value(map_=self._hvac_mode_map,
return mode_value key=current_mode)
return HVACMode.AUTO if (HVACMode.AUTO return HVACMode.OFF if mode_value == HVACMode.OFF else HVACMode.AUTO
in self._attr_hvac_modes) else None
class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature, class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
@ -677,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')
@ -705,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')
@ -719,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

@ -565,27 +565,32 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
home_list = {} home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices') tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list # home list
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
# i18n if device_source not in self._cc_home_info['homes']:
tip_central = '' continue
group_id = home_info.get('group_id', None) for home_id, home_info in self._cc_home_info[
dev_list = { 'homes'][device_source].items():
device['did']: device
for device in list(self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
mips_list
and group_id in mips_list
and mips_list[group_id].get('did', None) in dev_list
):
# i18n # i18n
tip_central = self._miot_i18n.translate( tip_central = ''
key='config.other.found_central_gateway') group_id = home_info.get('group_id', None)
home_info['central_did'] = mips_list[group_id].get('did', None) dev_list = {
home_list[home_id] = ( device['did']: device
f'{home_info["home_name"]} ' for device in list(self._cc_home_info['devices'].values())
f'[ {len(dev_list)} {tip_devices} {tip_central} ]') if device.get('home_id', None) == home_id}
if (
mips_list
and group_id in mips_list
and mips_list[group_id].get('did', None) in dev_list
):
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
self._cc_home_list_show = dict(sorted(home_list.items())) self._cc_home_list_show = dict(sorted(home_list.items()))
@ -660,10 +665,14 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not home_selected: if not home_selected:
return await self.__show_homes_select_form( return await self.__show_homes_select_form(
'no_family_selected') 'no_family_selected')
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
if home_id in home_selected: if device_source not in self._cc_home_info['homes']:
self._home_selected[home_id] = home_info continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
if home_id in home_selected:
self._home_selected[home_id] = home_info
self._area_name_rule = user_input.get( self._area_name_rule = user_input.get(
'area_name_rule', self._area_name_rule) 'area_name_rule', self._area_name_rule)
# Storage device list # Storage device list
@ -1420,27 +1429,31 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
home_list = {} home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices') tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list # home list
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
# i18n if device_source not in self._cc_home_info['homes']:
tip_central = '' continue
group_id = home_info.get('group_id', None) for home_id, home_info in self._cc_home_info[
did_list = { 'homes'][device_source].items():
device['did']: device for device in list(
self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
group_id in mips_list
and mips_list[group_id].get('did', None) in did_list
):
# i18n # i18n
tip_central = self._miot_i18n.translate( tip_central = ''
key='config.other.found_central_gateway') group_id = home_info.get('group_id', None)
home_info['central_did'] = mips_list[group_id].get( did_list = {
'did', None) device['did']: device for device in list(
home_list[home_id] = ( self._cc_home_info['devices'].values())
f'{home_info["home_name"]} ' if device.get('home_id', None) == home_id}
f'[ {len(did_list)} {tip_devices} {tip_central} ]') if (
group_id in mips_list
and mips_list[group_id].get('did', None) in did_list
):
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(did_list)} {tip_devices} {tip_central} ]')
# Remove deleted item # Remove deleted item
self._home_selected_list = [ self._home_selected_list = [
home_id for home_id in self._home_selected_list home_id for home_id in self._home_selected_list
@ -1460,10 +1473,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return await self.__show_homes_select_form('no_family_selected') return await self.__show_homes_select_form('no_family_selected')
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode) self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
self._home_selected = {} self._home_selected = {}
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
if home_id in self._home_selected_list: if device_source not in self._cc_home_info['homes']:
self._home_selected[home_id] = home_info continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
if home_id in self._home_selected_list:
self._home_selected[home_id] = home_info
# Get device list # Get device list
device_list: dict = { device_list: dict = {
did: dev_info did: dev_info

View File

@ -236,6 +236,9 @@ class Fan(MIoTServiceEntity, FanEntity):
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage of the fan speed.""" """Set the percentage of the fan speed."""
if percentage > 0: if percentage > 0:
if not self.is_on:
# If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True)
if self._speed_names: if self._speed_names:
await self.set_property_async( await self.set_property_async(
prop=self._prop_fan_level, prop=self._prop_fan_level,
@ -249,9 +252,6 @@ class Fan(MIoTServiceEntity, FanEntity):
value=int(percentage_to_ranged_value( value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max), low_high_range=(self._speed_min, self._speed_max),
percentage=percentage))) percentage=percentage)))
if not self.is_on:
# If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True)
else: else:
await self.set_property_async(prop=self._prop_on, value=False) await self.set_property_async(prop=self._prop_on, value=False)

View File

@ -52,23 +52,22 @@ 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.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.humidifier import ( from homeassistant.components.humidifier import (HumidifierEntity,
HumidifierEntity, HumidifierDeviceClass,
HumidifierDeviceClass, HumidifierEntityFeature,
HumidifierEntityFeature HumidifierAction)
)
from .miot.miot_spec import MIoTSpecProperty from .miot.miot_spec import MIoTSpecProperty
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
from .miot.const import DOMAIN from .miot.const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
@ -82,8 +81,8 @@ async def async_setup_entry(
Humidifier(miot_device=miot_device, entity_data=data)) Humidifier(miot_device=miot_device, entity_data=data))
for data in miot_device.entity_list.get('dehumidifier', []): for data in miot_device.entity_list.get('dehumidifier', []):
data.device_class = HumidifierDeviceClass.DEHUMIDIFIER data.device_class = HumidifierDeviceClass.DEHUMIDIFIER
new_entities.append(Humidifier( new_entities.append(
miot_device=miot_device, entity_data=data)) Humidifier(miot_device=miot_device, entity_data=data))
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -99,9 +98,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
_mode_map: dict[Any, Any] _mode_map: dict[Any, Any]
def __init__( def __init__(self, miot_device: MIoTDevice,
self, miot_device: MIoTDevice, entity_data: MIoTEntityData entity_data: MIoTEntityData) -> None:
) -> None:
"""Initialize the Humidifier.""" """Initialize the Humidifier."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_device_class = entity_data.device_class self._attr_device_class = entity_data.device_class
@ -130,12 +128,10 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
# mode # mode
elif prop.name == 'mode': elif prop.name == 'mode':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('mode value_list is None, %s', self.entity_id)
'mode value_list is None, %s', self.entity_id)
continue continue
self._mode_map = prop.value_list.to_map() self._mode_map = prop.value_list.to_map()
self._attr_available_modes = list( self._attr_available_modes = list(self._mode_map.values())
self._mode_map.values())
self._attr_supported_features |= HumidifierEntityFeature.MODES self._attr_supported_features |= HumidifierEntityFeature.MODES
self._prop_mode = prop self._prop_mode = prop
# relative-humidity # relative-humidity
@ -152,33 +148,45 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
async def async_set_humidity(self, humidity: int) -> None: async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity.""" """Set new target humidity."""
await self.set_property_async( if self._prop_target_humidity is None:
prop=self._prop_target_humidity, value=humidity) return
await self.set_property_async(prop=self._prop_target_humidity,
value=humidity)
async def async_set_mode(self, mode: str) -> None: async def async_set_mode(self, mode: str) -> None:
"""Set new target preset mode.""" """Set new target preset mode."""
await self.set_property_async( await self.set_property_async(prop=self._prop_mode,
prop=self._prop_mode, value=self.get_map_key(
value=self.get_map_key(map_=self._mode_map, value=mode)) map_=self._mode_map, value=mode))
@property @property
def is_on(self) -> Optional[bool]: def is_on(self) -> Optional[bool]:
"""Return if the humidifier is on.""" """Return if the humidifier is on."""
return self.get_prop_value(prop=self._prop_on) return self.get_prop_value(prop=self._prop_on)
@property
def action(self) -> Optional[HumidifierAction]:
"""Return the current status of the device."""
if not self.is_on:
return HumidifierAction.OFF
if self._attr_device_class == HumidifierDeviceClass.HUMIDIFIER:
return HumidifierAction.HUMIDIFYING
return HumidifierAction.DRYING
@property @property
def current_humidity(self) -> Optional[int]: def current_humidity(self) -> Optional[int]:
"""Return the current humidity.""" """Return the current humidity."""
return self.get_prop_value(prop=self._prop_humidity) return (self.get_prop_value(
prop=self._prop_humidity) if self._prop_humidity else None)
@property @property
def target_humidity(self) -> Optional[int]: def target_humidity(self) -> Optional[int]:
"""Return the target humidity.""" """Return the target humidity."""
return self.get_prop_value(prop=self._prop_target_humidity) return (self.get_prop_value(prop=self._prop_target_humidity)
if self._prop_target_humidity else None)
@property @property
def mode(self) -> Optional[str]: def mode(self) -> Optional[str]:
"""Return the current preset mode.""" """Return the current preset mode."""
return self.get_map_value( return self.get_map_value(map_=self._mode_map,
map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode))
key=self.get_prop_value(prop=self._prop_mode))

View File

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

View File

@ -879,16 +879,7 @@ class MIoTClient:
sub_from = self._sub_source_list.pop(did, None) sub_from = self._sub_source_list.pop(did, None)
# Unsub # Unsub
if sub_from: if sub_from:
if sub_from == 'cloud': self.__unsub_from(sub_from, did)
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif sub_from == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
# Storage # Storage
await self._storage.save_async( await self._storage.save_async(
domain='miot_devices', domain='miot_devices',
@ -936,6 +927,39 @@ class MIoTClient:
delay_sec, lambda: self._main_loop.create_task( delay_sec, lambda: self._main_loop.create_task(
self.refresh_user_cert_async())) self.refresh_user_cert_async()))
@final
def __unsub_from(self, sub_from: str, did: str) -> None:
mips: Any = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
try:
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
except RuntimeError as e:
if 'Event loop is closed' in str(e):
# Ignore unsub exception when loop is closed
pass
else:
raise
@final
def __sub_from(self, sub_from: str, did: str) -> None:
mips = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
@final @final
def __update_device_msg_sub(self, did: str) -> None: def __update_device_msg_sub(self, did: str) -> None:
if did not in self._device_list_cache: if did not in self._device_list_cache:
@ -967,27 +991,9 @@ class MIoTClient:
return return
# Unsub old # Unsub old
if from_old: if from_old:
if from_old == 'cloud': self.__unsub_from(from_old, did)
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif from_old == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif from_old in self._mips_local:
mips = self._mips_local[from_old]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
# Sub new # Sub new
if from_new == 'cloud': self.__sub_from(from_new, did)
self._mips_cloud.sub_prop(did=did, handler=self.__on_prop_msg)
self._mips_cloud.sub_event(did=did, handler=self.__on_event_msg)
elif from_new == 'lan':
self._miot_lan.sub_prop(did=did, handler=self.__on_prop_msg)
self._miot_lan.sub_event(did=did, handler=self.__on_event_msg)
elif from_new in self._mips_local:
mips = self._mips_local[from_new]
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
self._sub_source_list[did] = from_new self._sub_source_list[did] = from_new
_LOGGER.info( _LOGGER.info(
'device sub changed, %s, from %s to %s', did, from_old, from_new) 'device sub changed, %s, from %s to %s', did, from_old, from_new)

View File

@ -444,6 +444,17 @@ class MIoTHttpClient:
return home_list return home_list
async def get_separated_shared_devices_async(self) -> dict[str, dict]:
separated_shared_devices: dict = {}
device_list: dict[str, dict] = await self.__get_device_list_page_async(
dids=[], start_did=None)
for did, value in device_list.items():
if value['owner'] is not None and ('userid' in value['owner']) and (
'nickname' in value['owner']
):
separated_shared_devices.setdefault(did, value['owner'])
return separated_shared_devices
async def get_homeinfos_async(self) -> dict: async def get_homeinfos_async(self) -> dict:
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/homeroom/gethome', url_path='/app/v2/homeroom/gethome',
@ -499,19 +510,22 @@ class MIoTHttpClient:
): ):
more_list = await self.__get_dev_room_page_async( more_list = await self.__get_dev_room_page_async(
max_id=res_obj['result']['max_id']) max_id=res_obj['result']['max_id'])
for home_id, info in more_list.items(): for device_source in ['homelist', 'share_home_list']:
if home_id not in home_infos['homelist']: for home_id, info in more_list.items():
_LOGGER.info('unknown home, %s, %s', home_id, info) if home_id not in home_infos[device_source]:
continue _LOGGER.info('unknown home, %s, %s', home_id, info)
home_infos['homelist'][home_id]['dids'].extend(info['dids']) continue
for room_id, info in info['room_info'].items(): home_infos[device_source][home_id]['dids'].extend(
home_infos['homelist'][home_id]['room_info'].setdefault( info['dids'])
room_id, { for room_id, info in info['room_info'].items():
'room_id': room_id, home_infos[device_source][home_id][
'room_name': '', 'room_info'].setdefault(
'dids': []}) room_id, {
home_infos['homelist'][home_id]['room_info'][ 'room_id': room_id,
room_id]['dids'].extend(info['dids']) 'room_name': '',
'dids': []})
home_infos[device_source][home_id]['room_info'][
room_id]['dids'].extend(info['dids'])
return { return {
'uid': uid, 'uid': uid,
@ -651,6 +665,25 @@ class MIoTHttpClient:
'room_name': room_name, 'room_name': room_name,
'group_id': group_id 'group_id': group_id
} for did in room_info.get('dids', [])}) } for did in room_info.get('dids', [])})
separated_shared_devices: dict = (
await self.get_separated_shared_devices_async())
if separated_shared_devices:
homes.setdefault('separated_shared_list', {})
for did, owner in separated_shared_devices.items():
owner_id = str(owner['userid'])
homes['separated_shared_list'].setdefault(owner_id,{
'home_name': owner['nickname'],
'uid': owner_id,
'group_id': 'NotSupport',
'room_info': {'shared_device': 'shared_device'}
})
devices.update({did: {
'home_id': owner_id,
'home_name': owner['nickname'],
'room_id': 'shared_device',
'room_name': 'shared_device',
'group_id': 'NotSupport'
}})
dids = sorted(list(devices.keys())) dids = sorted(list(devices.keys()))
results = await self.get_devices_with_dids_async(dids=dids) results = await self.get_devices_with_dids_async(dids=dids)
if results is None: if results is None:

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
@ -778,8 +779,10 @@ class MIoTDevice:
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from homeassistant.const import UnitOfConductivity # type: ignore from homeassistant.const import UnitOfConductivity # type: ignore
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
unit_map['mWh'] = UnitOfEnergy.MILLIWATT_HOUR
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
unit_map['μS/cm'] = 'μS/cm' unit_map['μS/cm'] = 'μS/cm'
unit_map['mWh'] = 'mWh'
return unit_map.get(spec_unit, None) return unit_map.get(spec_unit, None)
@ -894,7 +897,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,7 +1215,7 @@ 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('unknown event msg, %s', 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) self.log_info('wrong event msg, %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,76 @@
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:dmaker-p5:1:
prop.2.6:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:zhimi-v3:3:
prop.2.6:
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 +87,78 @@ 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:kettle:0000A009:yunmi-r3:1:
prop.3.1:
unit: ppm
urn:miot-spec-v2:device:light:0000A001:shhf-sfla12:1:
prop.8.11:
name: on-a
urn:miot-spec-v2:device:magnet-sensor:0000A016:linp-m1:1:
prop.2.1004:
name: contact-state
expr: src_value!=1
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)
prop.2.3:
expr: round(src_value/10, 1)
prop.2.4:
unit: mA
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:giot-v8icm:1:0000C816:
prop.4.1:
unit: mWh
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:1:0000C816: urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816
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,37 +168,52 @@ 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:safe-box:0000A042:loock-v1:1:
prop.5.1:
name: contact-state
expr: src_value!=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'
urn:miot-spec-v2:device:bath-heater:0000A028:mike-2:1: - value: 4
prop.3.1: description: '4'
name: mode-a - value: 5
prop.3.11: description: '5'
name: mode-b - value: 6
prop.3.12: description: '6'
name: mode-c - value: 7
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1: description: '7'
prop.2.2: - value: 8
name: fan-level-a description: '8'
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6: - value: 9
prop.10.6: description: '9'
unit: none - value: 10
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6 description: '10'
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6 - value: 11
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6 description: '11'
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6 - value: 12
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:5: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6 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,
@ -388,6 +389,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
'fan-control': 'fan', 'fan-control': 'fan',
'ceiling-fan': 'fan', 'ceiling-fan': 'fan',
'air-fresh': 'fan', 'air-fresh': 'fan',
'air-purifier': 'fan',
'water-heater': { 'water-heater': {
'required': { 'required': {
'properties': { 'properties': {
@ -454,12 +456,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',
@ -506,7 +524,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',
@ -560,12 +582,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

@ -110,7 +110,7 @@ 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:

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)
@ -39,7 +38,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
def save_json_file(file_path: str, data: dict) -> None: def save_json_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:
json.dump(data, file, ensure_ascii=False, indent=4) json.dump(data, file, ensure_ascii=False, indent=2)
def load_yaml_file(file_path: str) -> Optional[dict]: def load_yaml_file(file_path: str) -> Optional[dict]:
@ -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,13 +134,112 @@ 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 False
return True 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 True
def spec_modify(data: dict) -> bool: def spec_modify(data: dict) -> bool:
"""dict[str, str | dict[str, dict]]""" """dict[str, str | dict[str, dict]]"""
if not isinstance(data, dict): if not isinstance(data, dict):
@ -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)