Compare commits

...

5 Commits

Author SHA1 Message Date
LiShuzhen
ace8adbf6b style: code style 2025-03-06 16:28:13 +08:00
LiShuzhen
4809c16aba Merge branch 'main' into wd1 2025-03-06 16:25:31 +08:00
Necroneco
e69448f2eb
feat: add entity_category for indicator-light (#697)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-05 16:13:17 +08:00
Li Shuzhen
7901607648
fix: fan-level without value-list but with value-range (#808) 2025-03-05 15:31:18 +08:00
Li Shuzhen
5adcb7ce00
fix: wind-reverse format type (#810) 2025-03-05 15:31:02 +08:00
6 changed files with 100 additions and 38 deletions

View File

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

View File

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

View File

@ -549,6 +549,10 @@ class MIoTDevice:
# Optional actions
# Optional events
miot_service.platform = platform
# entity_category
if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get(
'entity_category', None):
miot_service.entity_category = entity_category
return entity_data
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
@ -899,6 +903,7 @@ class MIoTServiceEntity(Entity):
self._attr_name = (
f'{"* "if self.entity_data.spec.proprietary else " "}'
f'{self.entity_data.spec.description_trans}')
self._attr_entity_category = entity_data.spec.entity_category
# Set entity attr
self._attr_unique_id = self.entity_id
self._attr_should_poll = False

View File

@ -465,7 +465,7 @@ class _MIoTSpecBase:
iid: int
type_: str
description: str
description_trans: str
description_trans: Optional[str]
proprietary: bool
need_filter: bool
name: str
@ -476,6 +476,7 @@ class _MIoTSpecBase:
device_class: Any
state_class: Any
external_unit: Any
entity_category: Optional[str]
spec_id: int
@ -494,6 +495,7 @@ class _MIoTSpecBase:
self.device_class = None
self.state_class = None
self.external_unit = None
self.entity_category = None
self.spec_id = hash(f'{self.type_}.{self.iid}')
@ -1205,6 +1207,13 @@ class _SpecModify:
return None
return value_range
def get_prop_value_list(self, siid: int, piid: int) -> Optional[list]:
value_list = self.__get_prop_item(siid=siid, piid=piid,
key='value-list')
if not isinstance(value_list, list):
return None
return value_list
def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:
if not self._selected:
return None
@ -1485,6 +1494,10 @@ class MIoTSpecParser:
siid=service['iid'], piid=property_['iid'])
if custom_range:
spec_prop.value_range = custom_range
custom_list = self._spec_modify.get_prop_value_list(
siid=service['iid'], piid=property_['iid'])
if custom_list:
spec_prop.value_list = custom_list
# Parse service event
for event in service.get('events', []):
if (

View File

@ -49,3 +49,12 @@ urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1:
- 1
- 1
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:2: urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1
urn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1:
prop.5.2:
value-list:
- value: 1
description: low
- value: 128
description: medium
- value: 255
description: high

View File

@ -51,6 +51,7 @@ from homeassistant.components.event import EventDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
EntityCategory,
LIGHT_LUX,
UnitOfEnergy,
UnitOfPower,
@ -330,7 +331,8 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'events': set<event instance name: str>,
'actions': set<action instance name: str>
},
'entity': str
'entity': str,
'entity_category'?: str
}
}
"""
@ -348,10 +350,23 @@ SPEC_SERVICE_TRANS_MAP: dict = {
},
'entity': 'light'
},
'indicator-light': 'light',
'ambient-light': 'light',
'night-light': 'light',
'white-light': 'light',
'indicator-light': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'mode', 'brightness',
}
},
'entity': 'light',
'entity_category': EntityCategory.CONFIG
},
'fan': {
'required': {
'properties': {