Compare commits

...

6 Commits

Author SHA1 Message Date
Necroneco
07055c3281
Merge 506bd9f52e into eacc0d02da 2025-04-25 12:23:07 +08:00
Li Shuzhen
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
Li Shuzhen
23f0a2d360
docs: update changelog and version to v0.3.0 (#1022) 2025-04-25 08:40:14 +08:00
Li Shuzhen
3abccc2491
feat: import shared devices (#1021) 2025-04-25 08:29:11 +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
6 changed files with 231 additions and 64 deletions

View File

@ -1,8 +1,22 @@
# CHANGELOG
## 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)

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

@ -105,7 +105,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
@ -565,27 +565,32 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
# i18n
tip_central = ''
group_id = home_info.get('group_id', None)
dev_list = {
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
):
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
# 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} ]')
tip_central = ''
group_id = home_info.get('group_id', None)
dev_list = {
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
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()))
@ -660,10 +665,14 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not home_selected:
return await self.__show_homes_select_form(
'no_family_selected')
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
if home_id in home_selected:
self._home_selected[home_id] = home_info
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
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(
'area_name_rule', self._area_name_rule)
# Storage device list
@ -1420,27 +1429,31 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
# i18n
tip_central = ''
group_id = home_info.get('group_id', None)
did_list = {
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
):
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
# 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} ]')
tip_central = ''
group_id = home_info.get('group_id', None)
did_list = {
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
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
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')
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
self._home_selected = {}
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
if home_id in self._home_selected_list:
self._home_selected[home_id] = home_info
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
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
device_list: dict = {
did: dev_info

View File

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

View File

@ -444,6 +444,17 @@ class MIoTHttpClient:
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:
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/homeroom/gethome',
@ -499,19 +510,22 @@ class MIoTHttpClient:
):
more_list = await self.__get_dev_room_page_async(
max_id=res_obj['result']['max_id'])
for home_id, info in more_list.items():
if home_id not in home_infos['homelist']:
_LOGGER.info('unknown home, %s, %s', home_id, info)
continue
home_infos['homelist'][home_id]['dids'].extend(info['dids'])
for room_id, info in info['room_info'].items():
home_infos['homelist'][home_id]['room_info'].setdefault(
room_id, {
'room_id': room_id,
'room_name': '',
'dids': []})
home_infos['homelist'][home_id]['room_info'][
room_id]['dids'].extend(info['dids'])
for device_source in ['homelist', 'share_home_list']:
for home_id, info in more_list.items():
if home_id not in home_infos[device_source]:
_LOGGER.info('unknown home, %s, %s', home_id, info)
continue
home_infos[device_source][home_id]['dids'].extend(
info['dids'])
for room_id, info in info['room_info'].items():
home_infos[device_source][home_id][
'room_info'].setdefault(
room_id, {
'room_id': room_id,
'room_name': '',
'dids': []})
home_infos[device_source][home_id]['room_info'][
room_id]['dids'].extend(info['dids'])
return {
'uid': uid,
@ -651,6 +665,25 @@ class MIoTHttpClient:
'room_name': room_name,
'group_id': group_id
} 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()))
results = await self.get_devices_with_dids_async(dids=dids)
if results is None:

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 (