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

View File

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

View File

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

View File

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

View File

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

View File

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