Compare commits

...

7 Commits

Author SHA1 Message Date
Necroneco
10dd8ffae1
Merge 506bd9f52e into 593d7d3c0a 2025-12-16 08:56:39 +08:00
Li Shuzhen
593d7d3c0a
fix: retry to get cloud device list until successfully fetching device online state when the network resumes (#1555)
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-12-16 08:29:16 +08:00
Li Shuzhen
77cebb2e15
Fix specs (#1553)
* fix: daikin.aircondition.k2 property format (#1364)

* fix: daikin.airfresh.k33 property format (#1364)

* feat: xiaomi.wifispeaker.x08e is not converted to the media player entity (#1533)

* docs: FAQ local mode in README
2025-12-16 08:28:37 +08:00
Li Shuzhen
3925e12863
fix: playing state of media player entity (#1552) 2025-12-16 08:28:16 +08:00
Li Shuzhen
500ed76971
fix: subscribe error catch (#1551) 2025-12-16 08:27:30 +08:00
caibinqing
506bd9f52e
fix pylint 2025-04-07 11:19:19 +08:00
caibinqing
7c0caa9df7
fix: add migration step for config entry 2025-04-07 10:54:09 +08:00
11 changed files with 185 additions and 43 deletions

View File

@ -91,13 +91,13 @@ Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi
Yes, it supports multiple Xiaomi accounts. Furthermore, Xiaomi Home Integration allows that devices belonging to different accounts can be added to a same area.
- Does Xiaomi Home Integration support local control?
- Does Xiaomi Home Integration support local mode?
Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.3.0_0023 and above) or Xiaomi smart devices with built-in central hub gateway (software version 0.8.9 and above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans.
Local mode is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.3.0_0023 and above) or Xiaomi smart devices with [built-in central hub gateway](https://github.com/XiaoMi/ha_xiaomi_home/wiki/Central-hub-gateway-device-models) (software version 0.8.9 and above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local mode feature has been released.
Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available.
Xiaomi Home Integration can also implement partial local control by enabling Xiaomi LAN control function. Xiaomi LAN control function can only control IP devices (devices connected to the router via WiFi or ethernet cable) in the same local area network as Home Assistant. It cannot control BLE Mesh, ZigBee, etc. devices. This function may cause some abnormalities. We recommend not to use this function. Xiaomi LAN control function is enabled by [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update LAN control configuration
Xiaomi Home Integration can also implement partial local mode by enabling Xiaomi LAN control function. Xiaomi LAN control function can only control IP devices (devices connected to the router via WiFi or ethernet cable) in the same local area network as Home Assistant. It cannot control BLE Mesh, ZigBee, etc. devices. This function may cause some abnormalities. We recommend NOT using this function. Xiaomi LAN control function is enabled by [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update LAN control configuration.
Xiaomi LAN control function is not restricted by region. It is available in all regions. However, if there is a central gateway in the local area network where Home Assistant is located, even Xiaomi LAN control function is enabled in the integration, it will not take effect.

View File

@ -349,3 +349,101 @@ async def async_remove_config_entry_device(
_LOGGER.info(
'remove device, %s, %s', identifiers[1], device_entry.id)
return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug(
'Migrating configuration from version %s.%s',
config_entry.version,
config_entry.minor_version,
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
await _migrate_v1_to_v2(hass, config_entry)
_LOGGER.debug(
'Migration to configuration version %s.%s successful',
config_entry.version,
config_entry.minor_version,
)
return True
async def _migrate_v1_to_v2(hass: HomeAssistant, config_entry: ConfigEntry):
def ha_persistent_notify(
notify_id: str, title: Optional[str] = None,
message: Optional[str] = None
) -> None:
"""Send messages in Notifications dialog box."""
if title:
persistent_notification.async_create(
hass=hass, message=message or '',
title=title, notification_id=notify_id)
else:
persistent_notification.async_dismiss(
hass=hass, notification_id=notify_id)
entry_id = config_entry.entry_id
entry_data = dict(config_entry.data)
ha_persistent_notify(
notify_id=f'{entry_id}.oauth_error', title=None, message=None)
miot_client: MIoTClient = await get_miot_instance_async(
hass=hass, entry_id=entry_id,
entry_data=entry_data,
persistent_notify=ha_persistent_notify)
# Spec parser
spec_parser = MIoTSpecParser(
lang=entry_data.get(
'integration_language', DEFAULT_INTEGRATION_LANGUAGE),
storage=miot_client.miot_storage,
loop=miot_client.main_loop
)
await spec_parser.init_async()
# Manufacturer
manufacturer: DeviceManufacturer = DeviceManufacturer(
storage=miot_client.miot_storage,
loop=miot_client.main_loop)
await manufacturer.init_async()
er = entity_registry.async_get(hass)
for _, info in miot_client.device_list.items():
spec_instance = await spec_parser.parse(urn=info['urn'])
if not isinstance(spec_instance, MIoTSpecInstance):
continue
device: MIoTDevice = MIoTDevice(
miot_client=miot_client,
device_info={
**info, 'manufacturer': manufacturer.get_name(
info.get('manufacturer', ''))},
spec_instance=spec_instance)
device.spec_transform()
# Update unique_id
for platform, entities in device.entity_list.items():
for entity in entities:
if not isinstance(entity.spec, MIoTSpecService):
continue
old_unique_id = device.gen_service_entity_id_v1(
ha_domain=DOMAIN,
siid=entity.spec.iid,
)
entity_id = er.async_get_entity_id(
platform, DOMAIN, old_unique_id
)
if entity_id is None:
continue
new_unique_id = device.gen_service_entity_id(
ha_domain=DOMAIN,
siid=entity.spec.iid,
description=entity.spec.description,
)
er.async_update_entity(entity_id, new_unique_id=new_unique_id)
hass.config_entries.async_update_entry(config_entry, version=2)

View File

@ -109,7 +109,7 @@ _LOGGER = logging.getLogger(__name__)
class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Xiaomi Home config flow."""
# pylint: disable=unused-argument, inconsistent-quotes
VERSION = 1
VERSION = 2
MINOR_VERSION = 1
DEFAULT_AREA_NAME_RULE = 'room'
_main_loop: asyncio.AbstractEventLoop

View File

@ -430,15 +430,16 @@ class FeatureState(MIoTServiceEntity, MediaPlayerEntity):
elif item.name in {'pause', 'paused'}:
self._playing_state_map[
item.value] = MediaPlayerState.PAUSED
self._prop_playing_state = prop
self._prop_playing_state = prop
@property
def state(self) -> Optional[MediaPlayerState]:
"""The current state."""
return (self.get_map_value(map_=self._playing_state_map,
key=self.get_prop_value(
prop=self._prop_playing_state))
if self._prop_playing_state else MediaPlayerState.ON)
current_state = self.get_prop_value(
prop=self._prop_playing_state) if self._prop_playing_state else None
return (MediaPlayerState.ON if
(current_state is None) else self.get_map_value(
map_=self._playing_state_map, key=current_state))
class WifiSpeaker(FeatureVolumeSet, FeatureVolumeMute, FeaturePlay,

View File

@ -79,6 +79,12 @@ from .miot_i18n import MIoTI18n
_LOGGER = logging.getLogger(__name__)
REFRESH_PROPS_DELAY = 0.2
REFRESH_PROPS_RETRY_DELAY = 3
REFRESH_CLOUD_DEVICES_DELAY = 6
REFRESH_CLOUD_DEVICES_RETRY_DELAY = 60
REFRESH_GATEWAY_DEVICES_DELAY = 3
@dataclass
class MIoTClientSub:
"""MIoT client subscription."""
@ -717,7 +723,7 @@ class MIoTClient:
if self._refresh_props_timer:
return
self._refresh_props_timer = self._main_loop.call_later(
0.2, lambda: self._main_loop.create_task(
REFRESH_PROPS_DELAY, lambda: self._main_loop.create_task(
self.__refresh_props_handler()))
async def get_prop_async(self, did: str, siid: int, piid: int) -> Any:
@ -1433,9 +1439,19 @@ class MIoTClient:
async def __refresh_cloud_devices_async(self) -> None:
_LOGGER.debug(
'refresh cloud devices, %s, %s', self._uid, self._cloud_server)
self._refresh_cloud_devices_timer = None
result = await self._http.get_devices_async(
home_ids=list(self._entry_data.get('home_selected', {}).keys()))
if self._refresh_cloud_devices_timer:
self._refresh_cloud_devices_timer.cancel()
self._refresh_cloud_devices_timer = None
try:
result = await self._http.get_devices_async(
home_ids=list(self._entry_data.get('home_selected', {}).keys()))
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('refresh cloud devices failed, %s', err)
self._refresh_cloud_devices_timer = self._main_loop.call_later(
REFRESH_CLOUD_DEVICES_RETRY_DELAY,
lambda: self._main_loop.create_task(
self.__refresh_cloud_devices_async()))
return
if not result and 'devices' not in result:
self.__show_client_error_notify(
message=self._i18n.translate(
@ -1481,17 +1497,11 @@ class MIoTClient:
_LOGGER.debug(
'request refresh cloud devices, %s, %s',
self._uid, self._cloud_server)
if immediately:
if self._refresh_cloud_devices_timer:
self._refresh_cloud_devices_timer.cancel()
self._refresh_cloud_devices_timer = self._main_loop.call_later(
0, lambda: self._main_loop.create_task(
self.__refresh_cloud_devices_async()))
return
delay_sec : int = 0 if immediately else REFRESH_CLOUD_DEVICES_DELAY
if self._refresh_cloud_devices_timer:
return
self._refresh_cloud_devices_timer.cancel()
self._refresh_cloud_devices_timer = self._main_loop.call_later(
6, lambda: self._main_loop.create_task(
delay_sec, lambda: self._main_loop.create_task(
self.__refresh_cloud_devices_async()))
@final
@ -1615,7 +1625,8 @@ class MIoTClient:
return
self._mips_local_state_changed_timers[group_id] = (
self._main_loop.call_later(
3, lambda: self._main_loop.create_task(
REFRESH_GATEWAY_DEVICES_DELAY,
lambda: self._main_loop.create_task(
self.__refresh_gw_devices_with_group_id_async(
group_id=group_id))))
@ -1769,7 +1780,7 @@ class MIoTClient:
self._refresh_props_retry_count = 0
if self._refresh_props_list:
self._refresh_props_timer = self._main_loop.call_later(
0.2, lambda: self._main_loop.create_task(
REFRESH_PROPS_DELAY, lambda: self._main_loop.create_task(
self.__refresh_props_handler()))
else:
self._refresh_props_timer = None
@ -1788,7 +1799,7 @@ class MIoTClient:
_LOGGER.info(
'refresh props failed, retry, %s', self._refresh_props_retry_count)
self._refresh_props_timer = self._main_loop.call_later(
3, lambda: self._main_loop.create_task(
REFRESH_PROPS_RETRY_DELAY, lambda: self._main_loop.create_task(
self.__refresh_props_handler()))
@final

View File

@ -345,6 +345,11 @@ class MIoTDevice:
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}')
def gen_service_entity_id_v1(self, ha_domain: str, siid: int) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_s_{siid}')
def gen_service_entity_id(self, ha_domain: str, siid: int,
description: str) -> str:
return (

View File

@ -519,15 +519,12 @@ class _MipsClient(ABC):
if not self._mqtt or not self._mqtt.is_connected():
self.log_error(f'mips sub when not connected, {topic}')
return
try:
if topic not in self._mips_sub_pending_map:
self._mips_sub_pending_map[topic] = 0
if not self._mips_sub_pending_timer:
self._mips_sub_pending_timer = self._internal_loop.call_later(
0.01, self.__mips_sub_internal_pending_handler, topic)
except Exception as err: # pylint: disable=broad-exception-caught
# Catch all exception
self.log_error(f'mips sub internal error, {topic}. {err}')
if topic not in self._mips_sub_pending_map:
self._mips_sub_pending_map[topic] = 0
if not self._mips_sub_pending_timer:
self._mips_sub_pending_timer = self._internal_loop.call_later(
0.01, self.__mips_sub_internal_pending_handler, topic)
@final
def _mips_unsub_internal(self, topic: str) -> None:
@ -736,11 +733,16 @@ class _MipsClient(ABC):
self.log_error(f'retry mips sub internal error, {topic}')
continue
subbed_count += 1
result, mid = self._mqtt.subscribe(topic, qos=self.MIPS_QOS)
if result == MQTT_ERR_SUCCESS:
self._mips_sub_pending_map.pop(topic)
self.log_debug(f'mips sub internal success, {topic}')
continue
result = mid = None
try:
result, mid = self._mqtt.subscribe(topic, qos=self.MIPS_QOS)
if result == MQTT_ERR_SUCCESS:
self._mips_sub_pending_map.pop(topic)
self.log_debug(f'mips sub internal success, {topic}')
continue
except Exception as err: # pylint: disable=broad-exception-caught
# Catch all exception
self.log_error(f'mips sub internal error, {topic}. {err}')
self._mips_sub_pending_map[topic] = count+1
self.log_error(
f'retry mips sub internal, {count}, {topic}, {result}, {mid}')

View File

@ -291,7 +291,7 @@ class MIoTNetwork:
return self._main_loop.time() - start_ts
return self._DETECT_TIMEOUT
except Exception as err: # pylint: disable=broad-exception-caught
print(err)
_LOGGER.debug('ping error, %s',err)
return self._DETECT_TIMEOUT
async def __http_async(self, url: str) -> float:

View File

@ -1,6 +1,16 @@
urn:miot-spec-v2:device:air-condition-outlet:0000A045:lumi-mcn04:1:
prop.3.4:
format: uint8
urn:miot-spec-v2:device:air-conditioner:0000A004:daikin-k2:1:
prop.2.1:
name: ac-on
format: string
prop.2.2:
name: ac-mode
format: string
prop.3.1:
name: ac-fan-level
format: string
urn:miot-spec-v2:device:air-conditioner:0000A004:qdhkl-a42:1:
prop.2.2:
value-list:
@ -79,6 +89,16 @@ urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1:
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1
urn:miot-spec-v2:device:air-fresh:0000A012:daikin-k33:1:
prop.2.1:
name: ac-on
format: string
prop.2.3:
name: ac-mode
format: string
prop.2.5:
name: ac-fan-level
format: string
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-cgd1st:1:
prop.3.7:
value-range:
@ -461,6 +481,9 @@ 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:speaker:0000A015:xiaomi-x08e:1:
prop.3.1:
name: playing-state-a
urn:miot-spec-v2:device:switch:0000A003:090615-x1tpm:1:0000D042:
prop.27.3:
name: light-on

View File

@ -325,10 +325,12 @@ SPEC_DEVICE_TRANS_MAP: dict = {
},
'play-control': {
'required': {
'properties': {
'playing-state': {'read'}
},
'actions': {'play'}
},
'optional': {
'properties': {'playing-state'},
'actions': {'pause', 'stop', 'next', 'previous'}
}
}

View File

@ -93,11 +93,11 @@ git checkout v1.0.0
- 米家集成是否支持本地化控制?
米家集成支持通过[小米中枢网关](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search)(固件版本 3.3.0_0023 及以上)或内置中枢网关(软件版本 0.8.9 及以上)的米家设备实现本地化控制。如果没有小米中枢网关或其他带中枢网关功能的设备,那么所有控制指令都会通过小米云发送。支持 Home Assistant 本地化控制的小米中枢网关(含内置中枢网关)的固件尚未发布,固件升级计划请参阅 MIoT 团队的通知
米家集成支持通过[小米中枢网关](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search)(固件版本 3.3.0_0023 及以上)或[内置中枢网关](https://github.com/XiaoMi/ha_xiaomi_home/wiki/Central-hub-gateway-device-models)(软件版本 0.8.9 及以上)的米家设备实现本地化控制。如果没有小米中枢网关或其他带中枢网关功能的设备,那么所有控制指令都会通过小米云发送。支持 Home Assistant 本地化控制的小米中枢网关(含内置中枢网关)的固件已发布
小米中枢网关仅在中国大陆可用,在其他地区不可用。
米家集成也能通过开启小米局域网控制功能实现部分本地化控制效果。小米局域网控制功能只能控制与 Home Assistant 处于同一局域网内的 IP 设备(使用 WiFi、网线连接路由器的设备无法控制蓝牙 Mesh、ZigBee 等协议接入的设备。该功能可能会引起一些异常,我们建议不要使用该功能。小米局域网控制功能开启方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新局域网控制配置
米家集成也能通过开启小米局域网控制功能实现部分本地化控制效果。小米局域网控制功能只能控制与 Home Assistant 处于同一局域网内的 IP 设备(使用 WiFi、网线连接路由器的设备无法控制蓝牙 Mesh、ZigBee 等协议接入的设备。该功能可能会引起一些异常,我们建议不要使用该功能。小米局域网控制功能开启方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新局域网控制配置
小米局域网控制功能不受地区限制,在全球范围内均可用。如果 Home Assistant 所在的局域网内存在中枢网关,那么即便米家集成开启了小米局域网控制功能,该功能也不会生效。