Compare commits

...

20 Commits

Author SHA1 Message Date
ted
59c35be221
Merge 4e1185f4e5 into 5c46504d0e 2025-03-07 18:16:48 +08:00
Li Shuzhen
5c46504d0e
docs: update changelog and version to v0.2.1 (#848)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-07 14:20:17 +08:00
Li Shuzhen
97d89b3a04
feat: thermostat preset mode (#833)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-07 08:48:35 +08:00
Li Shuzhen
4482d257dc
revert: multi_lang.json (#834) 2025-03-07 08:48:12 +08:00
wilds
d0387be15b
fix #838 (#839) 2025-03-07 08:47:52 +08:00
Necroneco
27cf1085bd
fix: opening and closing for linp.wopener.wd1lb (#826) 2025-03-07 08:46:17 +08:00
ted
4e1185f4e5
Merge branch 'XiaoMi:main' into main 2025-02-28 20:20:55 +08:00
ted
6ad8da42be
Merge branch 'XiaoMi:main' into main 2025-02-25 10:45:50 +08:00
ted
4aa6bb579f
Merge branch 'XiaoMi:main' into main 2025-02-19 14:05:50 +08:00
ted
e83aa9367e
Merge branch 'XiaoMi:main' into main 2025-01-31 15:25:58 +08:00
tedwang
04c44f36b1 Merge branch 'main' of https://github.com/zghnwsq/fork_ha_xiaomi_home into fix_vacuum_warn 2025-01-23 11:01:23 +08:00
ted
a40363d3e6
Merge branch 'XiaoMi:main' into main 2025-01-23 10:17:55 +08:00
tedwang
13e6863678 fix: Fix the HA warning in the logs related to vacuum state setting
Adapt to new vacuum state property, set the activity property instead of directly setting the state property.
2025-01-23 10:09:15 +08:00
ted
b0d0d6b107
Merge branch 'XiaoMi:main' into main 2025-01-21 16:27:32 +08:00
ted
085caff660
Merge branch 'XiaoMi:main' into main 2025-01-18 16:34:16 +08:00
ted
fe3756db9b
Merge branch 'XiaoMi:main' into main 2025-01-16 09:18:40 +08:00
ted
bf33f0c60d
Merge branch 'XiaoMi:main' into main 2025-01-13 13:58:58 +08:00
ted
72f3a5df3e
Merge branch 'XiaoMi:main' into main 2025-01-11 20:35:24 +08:00
ted
f452611b87
Merge branch 'XiaoMi:main' into main 2025-01-08 11:43:20 +08:00
tedwang
36d5a3e4de docs: fix table header misplacement
fix table header misplacement
2025-01-03 10:54:41 +08:00
8 changed files with 332 additions and 93 deletions

View File

@ -1,5 +1,18 @@
# CHANGELOG
## v0.2.1
### Added
- Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833)
### Changed
- Change paho-mqtt version to adapt Home Assistant 2025.03. [#839](https://github.com/XiaoMi/ha_xiaomi_home/pull/839)
- Revert to use multi_lang.json. [#834](https://github.com/XiaoMi/ha_xiaomi_home/pull/834)
### Fixed
- Fix the opening and the closing status of linp.wopener.wd1lb. [#826](https://github.com/XiaoMi/ha_xiaomi_home/pull/826)
- Fix the format type of the wind-reverse property. [#810](https://github.com/XiaoMi/ha_xiaomi_home/pull/810)
- Fix the fan-level property without value-list but with value-range. [#808](https://github.com/XiaoMi/ha_xiaomi_home/pull/808)
## v0.2.0
This version has modified some default units of sensors. After updating, it may cause Home Assistant to pop up some compatibility warnings. You can re-add the integration to resolve it.

View File

@ -189,7 +189,7 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity):
for prop in self.entity_data.props:
if prop.name == prop_name and prop.service.name == service_name:
if not prop.value_list:
_LOGGER.error('invalid %s %s value_list, %s',service_name,
_LOGGER.error('invalid %s %s value_list, %s', service_name,
prop_name, self.entity_id)
continue
self._mode_map = prop.value_list.to_map()
@ -229,7 +229,9 @@ class FeatureFanMode(MIoTServiceEntity, ClimateEntity):
super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props:
if prop.name == 'fan-level' and prop.service.name == 'fan-control':
if (prop.name == 'fan-level' and
(prop.service.name == 'fan-control' or
prop.service.name == 'thermostat')):
if not prop.value_list:
_LOGGER.error('invalid fan-level value_list, %s',
self.entity_id)
@ -665,78 +667,31 @@ class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature,
class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
FeatureHumidity, FeatureFanMode):
FeatureHumidity, FeatureFanMode, FeaturePresetMode):
"""Thermostat"""
_prop_mode: Optional[MIoTSpecProperty]
_hvac_mode_map: Optional[dict[int, HVACMode]]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the thermostat."""
self._prop_mode = None
self._hvac_mode_map = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_icon = 'mdi:thermostat'
# hvac modes
self._attr_hvac_modes = None
for prop in entity_data.props:
if prop.name == 'mode':
if not prop.value_list:
_LOGGER.error('invalid mode value_list, %s', self.entity_id)
continue
self._hvac_mode_map = {}
for item in prop.value_list.items:
if item.name in {'off', 'idle'}:
self._hvac_mode_map[item.value] = HVACMode.OFF
elif item.name in {'auto'}:
self._hvac_mode_map[item.value] = HVACMode.AUTO
elif item.name in {'cool'}:
self._hvac_mode_map[item.value] = HVACMode.COOL
elif item.name in {'heat'}:
self._hvac_mode_map[item.value] = HVACMode.HEAT
elif item.name in {'dry'}:
self._hvac_mode_map[item.value] = HVACMode.DRY
elif item.name in {'fan'}:
self._hvac_mode_map[item.value] = HVACMode.FAN_ONLY
self._attr_hvac_modes = list(self._hvac_mode_map.values())
self._prop_mode = prop
if self._attr_hvac_modes is None:
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO]
elif HVACMode.OFF not in self._attr_hvac_modes:
self._attr_hvac_modes.insert(0, HVACMode.OFF)
self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]
# preset modes
self._init_preset_modes('thermostat', 'mode')
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the target hvac mode."""
# set the device off
if hvac_mode == HVACMode.OFF:
if not await self.set_property_async(prop=self._prop_on,
value=False):
raise RuntimeError(f'set climate prop.on failed, {hvac_mode}, '
f'{self.entity_id}')
return
# set the device on
elif self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async(prop=self._prop_on, value=True)
# set mode
if self._prop_mode is None:
return
mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode)
if mode_value is None or not await self.set_property_async(
prop=self._prop_mode, value=mode_value):
raise RuntimeError(
f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}')
await self.set_property_async(
prop=self._prop_on,
value=False if hvac_mode == HVACMode.OFF else True)
@property
def hvac_mode(self) -> Optional[HVACMode]:
"""The current hvac mode."""
if self.get_prop_value(prop=self._prop_on) is False:
return HVACMode.OFF
return (self.get_map_value(map_=self._hvac_mode_map,
key=self.get_prop_value(
prop=self._prop_mode))
if self._prop_mode else None)
return (HVACMode.AUTO if self.get_prop_value(
prop=self._prop_on) else HVACMode.OFF)
class ElectricBlanket(FeatureOnOff, FeatureTargetTemperature,

View File

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

View File

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

View File

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

View File

@ -56,7 +56,7 @@ from slugify import slugify
# pylint: disable=relative-beyond-top-level
from .const import DEFAULT_INTEGRATION_LANGUAGE, SPEC_STD_LIB_EFFECTIVE_TIME
from .common import MIoTHttp, load_yaml_file
from .common import MIoTHttp, load_yaml_file, load_json_file
from .miot_error import MIoTSpecError
from .miot_storage import MIoTStorage
@ -213,6 +213,12 @@ class MIoTSpecValueList:
return item.description
return None
def get_name_by_value(self, value: Any) -> Optional[str]:
for item in self.items:
if item.value == value:
return item.name
return None
def dump(self) -> list:
return [item.dump() for item in self.items]
@ -839,6 +845,7 @@ class _MIoTSpecMultiLang:
"""MIoT SPEC multi lang class."""
# pylint: disable=broad-exception-caught
_DOMAIN: str = 'miot_specs_multi_lang'
_MULTI_LANG_FILE = 'specs/multi_lang.json'
_lang: str
_storage: MIoTStorage
_main_loop: asyncio.AbstractEventLoop
@ -894,6 +901,25 @@ class _MIoTSpecMultiLang:
except Exception as err:
trans_local = {}
_LOGGER.info('get multi lang from local failed, %s, %s', urn, err)
# Revert: load multi_lang.json
try:
trans_local_json = await self._main_loop.run_in_executor(
None, load_json_file,
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
self._MULTI_LANG_FILE))
urn_strs: list[str] = urn.split(':')
urn_key: str = ':'.join(urn_strs[:6])
if (
isinstance(trans_local_json, dict)
and urn_key in trans_local_json
and self._lang in trans_local_json[urn_key]
):
trans_cache.update(trans_local_json[urn_key][self._lang])
trans_local = trans_local_json[urn_key]
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('multi lang, load json file error, %s', err)
# Revert end
# Default language
if not trans_cache:
if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud:
@ -1453,10 +1479,12 @@ class MIoTSpecParser:
key=':'.join(p_type_strs[:5]))
or property_['description']
or spec_prop.name)
if 'value-range' in property_:
spec_prop.value_range = property_['value-range']
elif 'value-list' in property_:
v_list: list[dict] = property_['value-list']
# Modify value-list before translation
v_list: list[dict] = self._spec_modify.get_prop_value_list(
siid=service['iid'], piid=property_['iid'])
if (v_list is None) and ('value-list' in property_):
v_list = property_['value-list']
if v_list is not None:
for index, v in enumerate(v_list):
if v['description'].strip() == '':
v['description'] = f'v_{v["value"]}'
@ -1470,6 +1498,8 @@ class MIoTSpecParser:
f'{v["description"]}')
or v['name'])
spec_prop.value_list = MIoTSpecValueList.from_spec(v_list)
if 'value-range' in property_:
spec_prop.value_range = property_['value-range']
elif property_['format'] == 'bool':
v_tag = ':'.join(p_type_strs[:5])
v_descriptions = (
@ -1494,10 +1524,6 @@ 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

@ -0,0 +1,172 @@
{
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"de": {
"service:001": "Geräteinformationen",
"service:001:property:003": "Geräte-ID",
"service:001:property:005": "Seriennummer (SN)",
"service:002": "Gateway",
"service:002:event:001": "Netzwerk geändert",
"service:002:event:002": "Netzwerk geändert",
"service:002:property:001": "Zugriffsmethode",
"service:002:property:001:valuelist:000": "Kabelgebunden",
"service:002:property:001:valuelist:001": "5G Drahtlos",
"service:002:property:001:valuelist:002": "2.4G Drahtlos",
"service:002:property:002": "IP-Adresse",
"service:002:property:003": "WiFi-Netzwerkname",
"service:002:property:004": "Aktuelle Zeit",
"service:002:property:005": "DHCP-Server-MAC-Adresse",
"service:003": "Anzeigelampe",
"service:003:property:001": "Schalter",
"service:004": "Virtueller Dienst",
"service:004:action:001": "Virtuelles Ereignis erzeugen",
"service:004:event:001": "Virtuelles Ereignis aufgetreten",
"service:004:property:001": "Ereignisname"
},
"en": {
"service:001": "Device Information",
"service:001:property:003": "Device ID",
"service:001:property:005": "Serial Number (SN)",
"service:002": "Gateway",
"service:002:event:001": "Network Changed",
"service:002:event:002": "Network Changed",
"service:002:property:001": "Access Method",
"service:002:property:001:valuelist:000": "Wired",
"service:002:property:001:valuelist:001": "5G Wireless",
"service:002:property:001:valuelist:002": "2.4G Wireless",
"service:002:property:002": "IP Address",
"service:002:property:003": "WiFi Network Name",
"service:002:property:004": "Current Time",
"service:002:property:005": "DHCP Server MAC Address",
"service:003": "Indicator Light",
"service:003:property:001": "Switch",
"service:004": "Virtual Service",
"service:004:action:001": "Generate Virtual Event",
"service:004:event:001": "Virtual Event Occurred",
"service:004:property:001": "Event Name"
},
"es": {
"service:001": "Información del dispositivo",
"service:001:property:003": "ID del dispositivo",
"service:001:property:005": "Número de serie (SN)",
"service:002": "Puerta de enlace",
"service:002:event:001": "Cambio de red",
"service:002:event:002": "Cambio de red",
"service:002:property:001": "Método de acceso",
"service:002:property:001:valuelist:000": "Cableado",
"service:002:property:001:valuelist:001": "5G inalámbrico",
"service:002:property:001:valuelist:002": "2.4G inalámbrico",
"service:002:property:002": "Dirección IP",
"service:002:property:003": "Nombre de red WiFi",
"service:002:property:004": "Hora actual",
"service:002:property:005": "Dirección MAC del servidor DHCP",
"service:003": "Luz indicadora",
"service:003:property:001": "Interruptor",
"service:004": "Servicio virtual",
"service:004:action:001": "Generar evento virtual",
"service:004:event:001": "Ocurrió un evento virtual",
"service:004:property:001": "Nombre del evento"
},
"fr": {
"service:001": "Informations sur l'appareil",
"service:001:property:003": "ID de l'appareil",
"service:001:property:005": "Numéro de série (SN)",
"service:002": "Passerelle",
"service:002:event:001": "Changement de réseau",
"service:002:event:002": "Changement de réseau",
"service:002:property:001": "Méthode d'accès",
"service:002:property:001:valuelist:000": "Câblé",
"service:002:property:001:valuelist:001": "Sans fil 5G",
"service:002:property:001:valuelist:002": "Sans fil 2.4G",
"service:002:property:002": "Adresse IP",
"service:002:property:003": "Nom du réseau WiFi",
"service:002:property:004": "Heure actuelle",
"service:002:property:005": "Adresse MAC du serveur DHCP",
"service:003": "Voyant lumineux",
"service:003:property:001": "Interrupteur",
"service:004": "Service virtuel",
"service:004:action:001": "Générer un événement virtuel",
"service:004:event:001": "Événement virtuel survenu",
"service:004:property:001": "Nom de l'événement"
},
"ja": {
"service:001": "デバイス情報",
"service:001:property:003": "デバイスID",
"service:001:property:005": "シリアル番号 (SN)",
"service:002": "ゲートウェイ",
"service:002:event:001": "ネットワークが変更されました",
"service:002:event:002": "ネットワークが変更されました",
"service:002:property:001": "アクセス方法",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G ワイヤレス",
"service:002:property:001:valuelist:002": "2.4G ワイヤレス",
"service:002:property:002": "IPアドレス",
"service:002:property:003": "WiFiネットワーク名",
"service:002:property:004": "現在の時間",
"service:002:property:005": "DHCPサーバーMACアドレス",
"service:003": "インジケータライト",
"service:003:property:001": "スイッチ",
"service:004": "バーチャルサービス",
"service:004:action:001": "バーチャルイベントを生成",
"service:004:event:001": "バーチャルイベントが発生しました",
"service:004:property:001": "イベント名"
},
"ru": {
"service:001": "Информация об устройстве",
"service:001:property:003": "ID устройства",
"service:001:property:005": "Серийный номер (SN)",
"service:002": "Шлюз",
"service:002:event:001": "Сеть изменена",
"service:002:event:002": "Сеть изменена",
"service:002:property:001": "Метод доступа",
"service:002:property:001:valuelist:000": "Проводной",
"service:002:property:001:valuelist:001": "5G Беспроводной",
"service:002:property:001:valuelist:002": "2.4G Беспроводной",
"service:002:property:002": "IP Адрес",
"service:002:property:003": "Название WiFi сети",
"service:002:property:004": "Текущее время",
"service:002:property:005": "MAC адрес DHCP сервера",
"service:003": "Световой индикатор",
"service:003:property:001": "Переключатель",
"service:004": "Виртуальная служба",
"service:004:action:001": "Создать виртуальное событие",
"service:004:event:001": "Произошло виртуальное событие",
"service:004:property:001": "Название события"
},
"zh-Hant": {
"service:001": "設備信息",
"service:001:property:003": "設備ID",
"service:001:property:005": "序號 (SN)",
"service:002": "網關",
"service:002:event:001": "網路發生變化",
"service:002:event:002": "網路發生變化",
"service:002:property:001": "接入方式",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G 無線",
"service:002:property:001:valuelist:002": "2.4G 無線",
"service:002:property:002": "IP地址",
"service:002:property:003": "WiFi網路名稱",
"service:002:property:004": "當前時間",
"service:002:property:005": "DHCP伺服器MAC地址",
"service:003": "指示燈",
"service:003:property:001": "開關",
"service:004": "虛擬服務",
"service:004:action:001": "產生虛擬事件",
"service:004:event:001": "虛擬事件發生",
"service:004:property:001": "事件名稱"
}
},
"urn:miot-spec-v2:device:switch:0000A003:lumi-acn040": {
"en": {
"service:011": "Right Button On and Off",
"service:011:property:001": "Right Button On and Off",
"service:015:action:001": "Left Button Identify",
"service:016:action:001": "Middle Button Identify",
"service:017:action:001": "Right Button Identify"
},
"zh-Hans": {
"service:015:action:001": "左键确认",
"service:016:action:001": "中键确认",
"service:017:action:001": "右键确认"
}
}
}

View File

@ -54,7 +54,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumEntityFeature
VacuumEntityFeature,
VacuumActivity
)
from .miot.const import DOMAIN
@ -191,10 +192,47 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property
def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner."""
"""Return the current state of the vacuum cleaner.
To fix the HA warning below:
Detected that custom integration 'xiaomi_home' is setting state
directly.Entity XXX(<class 'custom_components.xiaomi_home.vacuum
.Vacuum'>)should implement the 'activity' property and return
its state using the VacuumActivity enum.This will stop working in
Home Assistant 2026.1.
Refer to
https://developers.home-assistant.io/blog/2024/12/08/new-vacuum-state-property
There are only 6 states in VacuumActivity enum. To be compatible with
more constants, try get matching VacuumActivity enum first, return state
string as before if there is no match. In Home Assistant 2026.1, every
state should map to a VacuumActivity enum.
"""
if (activity := self.activity) is not None:
return activity
return self.get_map_value(
map_=self._status_map,
key=self.get_prop_value(prop=self._prop_status))
key=self.get_prop_value(prop=self._prop_status)
)
@property
def activity(self) -> VacuumActivity | None:
"""Return the current vacuum activity."""
state_trans_map = {
'Sleep': VacuumActivity.IDLE,
'Idle': VacuumActivity.IDLE,
'Paused': VacuumActivity.PAUSED,
'Go Charging': VacuumActivity.RETURNING,
'Charging': VacuumActivity.DOCKED,
'Sweeping': VacuumActivity.CLEANING,
'Sweeping and Mopping': VacuumActivity.CLEANING,
'Mopping': VacuumActivity.CLEANING,
'Error': VacuumActivity.ERROR,
}
prop_value = self.get_prop_value(prop=self._prop_status)
state_name = self._prop_status.value_list.get_name_by_value(prop_value)
return state_trans_map.get(state_name)
@property
def battery_level(self) -> Optional[int]: