Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: refactor miot device and spec #592

Merged
merged 38 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
eddeafa
fix: fix miot_device type error
topsworld Jan 7, 2025
d9d8433
fix: fix type error
topsworld Jan 7, 2025
e4dfdf6
feat: remove spec cache storage
topsworld Jan 8, 2025
078adfb
feat: update std_lib and multi_lang logic
topsworld Jan 8, 2025
3399e3b
feat: update entity value-range
topsworld Jan 8, 2025
d25d3f6
feat: update value-list logic
topsworld Jan 9, 2025
93f04b1
feat: update prop.format_ logic
topsworld Jan 9, 2025
fca97e0
fix: fix miot cloud log error
topsworld Jan 9, 2025
792d70f
merge: merge main to this branch
topsworld Jan 10, 2025
60e57e8
fix: fix fan entity
topsworld Jan 10, 2025
8d5feba
Merge branch 'main' into refactor-miot-spec-device
topsworld Jan 10, 2025
018b213
Merge branch 'main' into refactor-miot-spec-device
topsworld Jan 13, 2025
7c1cbea
Merge branch 'main' into refactor-miot-spec-device
topsworld Jan 15, 2025
470215b
style: ignore type error
topsworld Jan 15, 2025
45b4a2b
style: rename spec_filter func name
topsworld Jan 15, 2025
51d76a3
feat: move bool_trans from storage to spec
topsworld Jan 15, 2025
9c84425
feat: move sepc_filter from storage to spec, use the YAML format file
topsworld Jan 15, 2025
da4cdc4
feat: same prop supports multiple sub
topsworld Jan 16, 2025
69f1d38
feat: same event supports multiple sub
topsworld Jan 16, 2025
8a1b38e
fix: fix device remove error
topsworld Jan 16, 2025
e07b90d
feat: add func slugify_did
topsworld Jan 16, 2025
6b49d07
fix: fix multi lang error
topsworld Jan 16, 2025
93905a1
feat: update action debug logic
topsworld Jan 16, 2025
37a446f
feat: ignore normal disconnect log
topsworld Jan 16, 2025
1476646
feat: support binary mode
topsworld Jan 16, 2025
64d5ff4
feat: change miot spec name type define
topsworld Jan 16, 2025
bc62891
style: ignore i18n tranlate type error
topsworld Jan 16, 2025
3749243
fix: fix pylint warning
topsworld Jan 16, 2025
cebd483
fix: miot storage type error
topsworld Jan 17, 2025
d5e243c
feat: support binary display mode configure
topsworld Jan 17, 2025
e7564b0
feat: set default sensor state_class
topsworld Jan 17, 2025
22f843d
fix: fix sensor entity type error
topsworld Jan 17, 2025
b16d158
fix: fix __init__ type error
topsworld Jan 17, 2025
a79fe14
merge: merge main
topsworld Jan 17, 2025
ee1a910
feat: update test case logic
topsworld Jan 17, 2025
af3c457
fix: github actions add dependencies lib
topsworld Jan 17, 2025
bfb6a96
fix: fix some type error
topsworld Jan 17, 2025
8cc9178
feat: update device list changed notify logic
topsworld Jan 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography
pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography slugify

- name: Check rule format with pytest
run: |
Expand Down
52 changes: 26 additions & 26 deletions custom_components/xiaomi_home/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from homeassistant.components import persistent_notification
from homeassistant.helpers import device_registry, entity_registry

from .miot.common import slugify_did
from .miot.miot_storage import (
DeviceManufacturer, MIoTStorage, MIoTCert)
from .miot.miot_spec import (
Expand Down Expand Up @@ -92,7 +93,7 @@ def ha_persistent_notify(
"""Send messages in Notifications dialog box."""
if title:
persistent_notification.async_create(
hass=hass, message=message,
hass=hass, message=message or '',
title=title, notification_id=notify_id)
else:
persistent_notification.async_dismiss(
Expand Down Expand Up @@ -125,9 +126,8 @@ def ha_persistent_notify(
miot_devices: list[MIoTDevice] = []
er = entity_registry.async_get(hass=hass)
for did, info in miot_client.device_list.items():
spec_instance: MIoTSpecInstance = await spec_parser.parse(
urn=info['urn'])
if spec_instance is None:
spec_instance = await spec_parser.parse(urn=info['urn'])
if not isinstance(spec_instance, MIoTSpecInstance):
_LOGGER.error('spec content is None, %s, %s', did, info)
continue
device: MIoTDevice = MIoTDevice(
Expand Down Expand Up @@ -155,7 +155,8 @@ def ha_persistent_notify(
for entity in filter_entities:
device.entity_list[platform].remove(entity)
entity_id = device.gen_service_entity_id(
ha_domain=platform, siid=entity.spec.iid)
ha_domain=platform,
siid=entity.spec.iid) # type: ignore
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
if platform in device.prop_list:
Expand Down Expand Up @@ -208,19 +209,29 @@ def ha_persistent_notify(
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
# Action debug
if miot_client.action_debug:
if 'notify' in device.action_list:
# Add text entity for debug action
device.action_list['action_text'] = (
device.action_list['notify'])
else:
if not miot_client.action_debug:
# Remove text entity for debug action
for action in device.action_list.get('notify', []):
entity_id = device.gen_action_entity_id(
ha_domain='text', spec_name=action.name,
siid=action.service.iid, aiid=action.iid)
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
# Binary sensor display
if not miot_client.display_binary_bool:
for prop in device.prop_list.get('binary_sensor', []):
entity_id = device.gen_prop_entity_id(
ha_domain='binary_sensor', spec_name=prop.name,
siid=prop.service.iid, piid=prop.iid)
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
if not miot_client.display_binary_text:
for prop in device.prop_list.get('binary_sensor', []):
entity_id = device.gen_prop_entity_id(
ha_domain='sensor', spec_name=prop.name,
siid=prop.service.iid, piid=prop.iid)
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)

hass.data[DOMAIN]['devices'][config_entry.entry_id] = miot_devices
await hass.config_entries.async_forward_entry_setups(
Expand All @@ -237,7 +248,7 @@ def ha_persistent_notify(
device_entry = dr.async_get_device(
identifiers={(
DOMAIN,
MIoTDevice.gen_did_tag(
slugify_did(
cloud_server=config_entry.data['cloud_server'],
did=did))},
connections=None)
Expand Down Expand Up @@ -330,21 +341,10 @@ async def async_remove_config_entry_device(
'remove device failed, invalid domain, %s, %s',
device_entry.id, device_entry.identifiers)
return False
device_info = identifiers[1].split('_')
if len(device_info) != 2:
_LOGGER.error(
'remove device failed, invalid device info, %s, %s',
device_entry.id, device_entry.identifiers)
return False
did = device_info[1]
if did not in miot_client.device_list:
_LOGGER.error(
'remove device failed, device not found, %s, %s',
device_entry.id, device_entry.identifiers)
return False

# Remove device
await miot_client.remove_device_async(did)
await miot_client.remove_device2_async(did_tag=identifiers[1])
device_registry.async_get(hass).async_remove_device(device_entry.id)
_LOGGER.info(
'remove device, %s, %s, %s', device_info[0], did, device_entry.id)
'remove device, %s, %s', identifiers[1], device_entry.id)
return True
7 changes: 4 additions & 3 deletions custom_components/xiaomi_home/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ async def async_setup_entry(

new_entities = []
for miot_device in device_list:
for prop in miot_device.prop_list.get('binary_sensor', []):
new_entities.append(BinarySensor(
miot_device=miot_device, spec=prop))
if miot_device.miot_client.display_binary_bool:
for prop in miot_device.prop_list.get('binary_sensor', []):
new_entities.append(BinarySensor(
miot_device=miot_device, spec=prop))

if new_entities:
async_add_entities(new_entities)
Expand Down
95 changes: 41 additions & 54 deletions custom_components/xiaomi_home/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,64 +156,56 @@ def __init__(
_LOGGER.error(
'unknown on property, %s', self.entity_id)
elif prop.name == 'mode':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
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:
if item['name'].lower() in {'off', 'idle'}:
self._hvac_mode_map[item['value']] = HVACMode.OFF
elif item['name'].lower() in {'auto'}:
self._hvac_mode_map[item['value']] = HVACMode.AUTO
elif item['name'].lower() in {'cool'}:
self._hvac_mode_map[item['value']] = HVACMode.COOL
elif item['name'].lower() in {'heat'}:
self._hvac_mode_map[item['value']] = HVACMode.HEAT
elif item['name'].lower() in {'dry'}:
self._hvac_mode_map[item['value']] = HVACMode.DRY
elif item['name'].lower() in {'fan'}:
self._hvac_mode_map[item['value']] = HVACMode.FAN_ONLY
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
elif prop.name == 'target-temperature':
if not isinstance(prop.value_range, dict):
if not prop.value_range:
_LOGGER.error(
'invalid target-temperature value_range format, %s',
self.entity_id)
continue
self._attr_min_temp = prop.value_range['min']
self._attr_max_temp = prop.value_range['max']
self._attr_target_temperature_step = prop.value_range['step']
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
self._attr_target_temperature_step = prop.value_range.step
self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE)
self._prop_target_temp = prop
elif prop.name == 'target-humidity':
if not isinstance(prop.value_range, dict):
if not prop.value_range:
_LOGGER.error(
'invalid target-humidity value_range format, %s',
self.entity_id)
continue
self._attr_min_humidity = prop.value_range['min']
self._attr_max_humidity = prop.value_range['max']
self._attr_min_humidity = prop.value_range.min_
self._attr_max_humidity = prop.value_range.max_
self._attr_supported_features |= (
ClimateEntityFeature.TARGET_HUMIDITY)
self._prop_target_humi = prop
elif prop.name == 'fan-level':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id)
continue
self._fan_mode_map = {
item['value']: item['description']
for item in prop.value_list}
self._fan_mode_map = prop.value_list.to_map()
self._attr_fan_modes = list(self._fan_mode_map.values())
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
self._prop_fan_level = prop
Expand Down Expand Up @@ -269,8 +261,8 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
elif self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async(prop=self._prop_on, value=True)
# set mode
mode_value = self.get_map_value(
map_=self._hvac_mode_map, description=hvac_mode)
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(
Expand Down Expand Up @@ -339,8 +331,8 @@ async def async_set_swing_mode(self, swing_mode):

async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
mode_value = self.get_map_value(
map_=self._fan_mode_map, description=fan_mode)
mode_value = self.get_map_key(
map_=self._fan_mode_map, value=fan_mode)
if mode_value is None or not await self.set_property_async(
prop=self._prop_fan_level, value=mode_value):
raise RuntimeError(
Expand Down Expand Up @@ -376,17 +368,17 @@ def hvac_mode(self) -> Optional[HVACMode]:
"""Return the hvac mode. e.g., heat, cool mode."""
if self.get_prop_value(prop=self._prop_on) is False:
return HVACMode.OFF
return self.get_map_description(
return self.get_map_key(
map_=self._hvac_mode_map,
key=self.get_prop_value(prop=self._prop_mode))
value=self.get_prop_value(prop=self._prop_mode))

@property
def fan_mode(self) -> Optional[str]:
"""Return the fan mode.

Requires ClimateEntityFeature.FAN_MODE.
"""
return self.get_map_description(
return self.get_map_value(
map_=self._fan_mode_map,
key=self.get_prop_value(prop=self._prop_fan_level))

Expand Down Expand Up @@ -446,8 +438,8 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None:
}.get(v_ac_state['M'], None)
if mode:
self.set_prop_value(
prop=self._prop_mode, value=self.get_map_value(
map_=self._hvac_mode_map, description=mode))
prop=self._prop_mode, value=self.get_map_key(
map_=self._hvac_mode_map, value=mode))
# T: target temperature
if 'T' in v_ac_state and self._prop_target_temp:
self.set_prop_value(prop=self._prop_target_temp,
Expand Down Expand Up @@ -517,29 +509,24 @@ def __init__(
ClimateEntityFeature.TURN_OFF)
self._prop_on = prop
elif prop.name == 'target-temperature':
if not isinstance(prop.value_range, dict):
if not prop.value_range:
_LOGGER.error(
'invalid target-temperature value_range format, %s',
self.entity_id)
continue
self._attr_min_temp = prop.value_range['min']
self._attr_max_temp = prop.value_range['max']
self._attr_target_temperature_step = prop.value_range['step']
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
self._attr_target_temperature_step = prop.value_range.step
self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE)
self._prop_target_temp = prop
elif prop.name == 'heat-level':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'invalid heat-level value_list, %s', self.entity_id)
continue
self._heat_level_map = {
item['value']: item['description']
for item in prop.value_list}
self._heat_level_map = prop.value_list.to_map()
self._attr_preset_modes = list(self._heat_level_map.values())
self._attr_supported_features |= (
ClimateEntityFeature.PRESET_MODE)
Expand Down Expand Up @@ -582,8 +569,8 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode."""
await self.set_property_async(
self._prop_heat_level,
value=self.get_map_value(
map_=self._heat_level_map, description=preset_mode))
value=self.get_map_key(
map_=self._heat_level_map, value=preset_mode))

@property
def target_temperature(self) -> Optional[float]:
Expand Down Expand Up @@ -613,7 +600,7 @@ def hvac_mode(self) -> Optional[HVACMode]:
@property
def preset_mode(self) -> Optional[str]:
return (
self.get_map_description(
self.get_map_value(
map_=self._heat_level_map,
key=self.get_prop_value(prop=self._prop_heat_level))
if self._prop_heat_level else None)
Loading
Loading