From 29573bb3cac77b8ae19ddb6e4dcb13c387d6cd55 Mon Sep 17 00:00:00 2001 From: almirus Date: Sat, 30 May 2026 17:13:43 +0300 Subject: [PATCH 1/2] add battery level sensors for yeelink.curtain.ctmt2 --- custom_components/xiaomi_miot/core/device.py | 31 +++++++++++++++---- .../xiaomi_miot/core/device_customizes.py | 7 +++++ .../xiaomi_miot/core/miot_specs_extend.json | 26 ++++++++++++++++ .../xiaomi_miot/core/templates.py | 14 +++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/custom_components/xiaomi_miot/core/device.py b/custom_components/xiaomi_miot/core/device.py index 20ac18547..81bfe25e5 100644 --- a/custom_components/xiaomi_miot/core/device.py +++ b/custom_components/xiaomi_miot/core/device.py @@ -1306,22 +1306,41 @@ async def update_miio_props(self, props=None): return if props is None: props = self.custom_miio_properties + if not props: + return if self.miio2miot: - attrs = self.miio2miot.only_miio_props(props) + raw = self.miio2miot.only_miio_props(props) + if not isinstance(raw, dict): + raw = dict(zip(props, raw)) else: try: num = self.custom_config_integer('chunk_properties') or 15 - attrs = await self.local.async_get_properties(props, max_properties=num) + getter = self.custom_config('miio_property_getter') or 'get_properties' + if getter == 'get_prop': + raw = await self.local.async_get_prop(props, max_properties=num) + else: + raw = await self.local.async_get_properties(props, max_properties=num) except DeviceException as exc: self.log.warning('%s: Got miio properties %s failed: %s', self.name_model, props, exc) return - if len(props) != len(attrs): + if len(props) != len(raw): self.props.update({ - 'miio.props': attrs, + 'miio.props': raw, }) return - attrs = dict(zip(map(lambda x: f'miio.{x}', props), attrs)) - self.props.update(attrs) + raw = dict(zip(props, raw)) + tpl = self.custom_config('miio_properties_template') + if tpl: + tpl = template(tpl, self.hass) + attrs = tpl.async_render({'props': raw}) + if not isinstance(attrs, dict): + attrs = raw + else: + attrs = dict(zip(map(lambda x: f'miio.{x}', props), raw.values())) + if attrs: + self.props.update(attrs) + self.data['updated'] = dt.now() + self.dispatch(self.decode_attrs(attrs)) self.log.info('%s: Got miio properties: %s', self.name_model, attrs) @cached_property diff --git a/custom_components/xiaomi_miot/core/device_customizes.py b/custom_components/xiaomi_miot/core/device_customizes.py index 98bc3b575..de4cbfc1a 100644 --- a/custom_components/xiaomi_miot/core/device_customizes.py +++ b/custom_components/xiaomi_miot/core/device_customizes.py @@ -2703,6 +2703,13 @@ 'yeelink.curtain.ctmt2': { **CHUNK_1, 'chunk_coordinators': [], + 'sensor_properties': 'battery_level,battery_level_2', + 'exclude_miot_properties': 'battery_level,battery_level_2', + 'miio_properties': 'get_child,battery,battery2', + 'miio_property_getter': 'get_prop', + 'miio_properties_template': 'yeelink_curtain_ctmt2_miio_properties', + 'miio_cloud_props': 'get_child,battery,battery2', + 'miio_cloud_props_template': 'yeelink_curtain_ctmt2_cloud_props', }, 'yeelink.bhf_light.v1': { 'interval_seconds': 30, diff --git a/custom_components/xiaomi_miot/core/miot_specs_extend.json b/custom_components/xiaomi_miot/core/miot_specs_extend.json index 7302185b7..b6395e590 100644 --- a/custom_components/xiaomi_miot/core/miot_specs_extend.json +++ b/custom_components/xiaomi_miot/core/miot_specs_extend.json @@ -1945,6 +1945,32 @@ } ] } + ], + + "yeelink.curtain.ctmt2": [ + { + "iid": 3, + "properties": [ + { + "iid": 9, + "type": "urn:miot-spec-v2:property:battery-level", + "format": "uint8", + "access": ["read"], + "unit": "percentage", + "value-range": [0, 100, 1], + "description": "Left" + }, + { + "iid": 10, + "type": "urn:miot-spec-v2:property:battery-level-2", + "format": "uint8", + "access": ["read"], + "unit": "percentage", + "value-range": [0, 100, 1], + "description": "Right" + } + ] + } ] } diff --git a/custom_components/xiaomi_miot/core/templates.py b/custom_components/xiaomi_miot/core/templates.py index ab3dfa8ed..bed5f45c6 100644 --- a/custom_components/xiaomi_miot/core/templates.py +++ b/custom_components/xiaomi_miot/core/templates.py @@ -168,6 +168,20 @@ "'coolwind_gear': val[1]," "'venting_gear': val[2]," "} }}", + 'yeelink_curtain_ctmt2_miio_properties': "{%- set child = props.get_child | default(1) | int %}" + "{{ {" + "'yl_curtain.battery_level': props.battery | int(0)," + "'yl_curtain.battery_level_2': props.battery2 | int(0)" + " if child == 2 else none," + "} }}", + 'yeelink_curtain_ctmt2_cloud_props': "{%- set child = props.get('prop.get_child', 1) | int(1) %}" + "{%- set bat = props.get('prop.battery') %}" + "{%- set bat2 = props.get('prop.battery2') %}" + "{{ {" + "'yl_curtain.battery_level': bat | int(0) if bat is not none else none," + "'yl_curtain.battery_level_2': bat2 | int(0)" + " if child == 2 and bat2 is not none else none," + "} }}", 'zimi_powerstrip_v2_power_cost': "{%- set val = (result.0 | default({})).get('value','[0]') %}" "{%- set day = now().day %}" "{%- set vls = (val | from_json)[0-day:] %}" From 9a45a020c8554424eabdd75d936c1c71080d5cc8 Mon Sep 17 00:00:00 2001 From: almirus Date: Wed, 10 Jun 2026 22:37:54 +0300 Subject: [PATCH 2/2] fix based on review results --- custom_components/xiaomi_miot/core/device.py | 45 ++++++++++++------- .../xiaomi_miot/core/device_customizes.py | 4 -- .../xiaomi_miot/core/miio2miot_specs.py | 12 +++++ .../xiaomi_miot/core/miot_specs_extend.json | 4 +- .../xiaomi_miot/core/templates.py | 6 --- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/custom_components/xiaomi_miot/core/device.py b/custom_components/xiaomi_miot/core/device.py index 81bfe25e5..ebfe51d7e 100644 --- a/custom_components/xiaomi_miot/core/device.py +++ b/custom_components/xiaomi_miot/core/device.py @@ -548,7 +548,7 @@ async def init_miot_coordinators(self, interval=60): all_mapping = {**self.miot_mapping()} chunks = self.custom_config_list('chunk_coordinators') or [] - if self.miio2miot: + if self.miio2miot and not self.miio2miot.config.get('extend_miot_props'): chunks = [] def update_factory(mapping, notify=False, chunk_services=None): @@ -856,7 +856,25 @@ async def update_miot_status( if use_local: try: if self.miio2miot: - results = await self.miio2miot.async_get_miot_props(self.local, mapping) + if self.miio2miot.config.get('extend_miot_props'): + miio_specs = self.miio2miot.specs + local_mapping = { + k: v for k, v in mapping.items() + if MiotSpec.unique_prop(v, valid=True) not in miio_specs + } + if not max_properties: + max_properties = self.custom_config_integer('chunk_properties') + if not max_properties: + max_properties = self.local.get_max_properties(local_mapping) + if local_mapping: + results = await self.local.async_get_properties_for_mapping( + max_properties=max_properties, + did=self.did, + mapping=local_mapping, + ) + results.extend(await self.miio2miot.async_get_miot_props(self.local, mapping)) + else: + results = await self.miio2miot.async_get_miot_props(self.local, mapping) if attrs := self.miio2miot.entity_attrs(): self.props.update(attrs) self.dispatch(self.decode_attrs(attrs)) @@ -1049,13 +1067,19 @@ async def async_set_properties(self, params): if not self._local_state or self.cloud_only or cloud_write: cloud_params = params elif self.miio2miot: + local_params = [] for param in params: siid = param['siid'] piid = param['piid'] if not self.miio2miot.has_setter(siid, piid=piid): - cloud_params.append(param) + if self.miio2miot.config.get('extend_miot_props'): + local_params.append(param) + else: + cloud_params.append(param) continue results.append(await self.miio2miot.async_set_property(self.local, siid, piid, param['value'])) + if self.local and local_params: + results.extend(await self.local.async_send('set_properties', local_params) or []) elif self.local: results = await self.local.async_send('set_properties', params) if self.cloud and cloud_params: @@ -1315,11 +1339,7 @@ async def update_miio_props(self, props=None): else: try: num = self.custom_config_integer('chunk_properties') or 15 - getter = self.custom_config('miio_property_getter') or 'get_properties' - if getter == 'get_prop': - raw = await self.local.async_get_prop(props, max_properties=num) - else: - raw = await self.local.async_get_properties(props, max_properties=num) + raw = await self.local.async_get_properties(props, max_properties=num) except DeviceException as exc: self.log.warning('%s: Got miio properties %s failed: %s', self.name_model, props, exc) return @@ -1329,14 +1349,7 @@ async def update_miio_props(self, props=None): }) return raw = dict(zip(props, raw)) - tpl = self.custom_config('miio_properties_template') - if tpl: - tpl = template(tpl, self.hass) - attrs = tpl.async_render({'props': raw}) - if not isinstance(attrs, dict): - attrs = raw - else: - attrs = dict(zip(map(lambda x: f'miio.{x}', props), raw.values())) + attrs = dict(zip(map(lambda x: f'miio.{x}', props), raw.values())) if attrs: self.props.update(attrs) self.data['updated'] = dt.now() diff --git a/custom_components/xiaomi_miot/core/device_customizes.py b/custom_components/xiaomi_miot/core/device_customizes.py index de4cbfc1a..7d54a78ab 100644 --- a/custom_components/xiaomi_miot/core/device_customizes.py +++ b/custom_components/xiaomi_miot/core/device_customizes.py @@ -2704,10 +2704,6 @@ **CHUNK_1, 'chunk_coordinators': [], 'sensor_properties': 'battery_level,battery_level_2', - 'exclude_miot_properties': 'battery_level,battery_level_2', - 'miio_properties': 'get_child,battery,battery2', - 'miio_property_getter': 'get_prop', - 'miio_properties_template': 'yeelink_curtain_ctmt2_miio_properties', 'miio_cloud_props': 'get_child,battery,battery2', 'miio_cloud_props_template': 'yeelink_curtain_ctmt2_cloud_props', }, diff --git a/custom_components/xiaomi_miot/core/miio2miot_specs.py b/custom_components/xiaomi_miot/core/miio2miot_specs.py index f1fd6383c..36f92ccb5 100644 --- a/custom_components/xiaomi_miot/core/miio2miot_specs.py +++ b/custom_components/xiaomi_miot/core/miio2miot_specs.py @@ -2092,6 +2092,18 @@ def cbk(prop, params, props, **kwargs): }, }, + 'yeelink.curtain.ctmt2': { + 'extend_miot_props': True, + 'miio_props': ['get_child'], + 'miio_specs': { + 'prop.3.100': {'prop': 'battery'}, + 'prop.3.101': { + 'prop': 'battery2', + 'template': '{{ value|int(0) if props.get_child|int(1) == 2 else none }}', + }, + }, + }, + 'yeelink.light._base': { 'miio_specs': { 'prop.2.1': {'prop': 'power', 'setter': True, 'format': 'onoff'}, diff --git a/custom_components/xiaomi_miot/core/miot_specs_extend.json b/custom_components/xiaomi_miot/core/miot_specs_extend.json index b6395e590..c93f1f923 100644 --- a/custom_components/xiaomi_miot/core/miot_specs_extend.json +++ b/custom_components/xiaomi_miot/core/miot_specs_extend.json @@ -1952,7 +1952,7 @@ "iid": 3, "properties": [ { - "iid": 9, + "iid": 100, "type": "urn:miot-spec-v2:property:battery-level", "format": "uint8", "access": ["read"], @@ -1961,7 +1961,7 @@ "description": "Left" }, { - "iid": 10, + "iid": 101, "type": "urn:miot-spec-v2:property:battery-level-2", "format": "uint8", "access": ["read"], diff --git a/custom_components/xiaomi_miot/core/templates.py b/custom_components/xiaomi_miot/core/templates.py index bed5f45c6..3f8558ed5 100644 --- a/custom_components/xiaomi_miot/core/templates.py +++ b/custom_components/xiaomi_miot/core/templates.py @@ -168,12 +168,6 @@ "'coolwind_gear': val[1]," "'venting_gear': val[2]," "} }}", - 'yeelink_curtain_ctmt2_miio_properties': "{%- set child = props.get_child | default(1) | int %}" - "{{ {" - "'yl_curtain.battery_level': props.battery | int(0)," - "'yl_curtain.battery_level_2': props.battery2 | int(0)" - " if child == 2 else none," - "} }}", 'yeelink_curtain_ctmt2_cloud_props': "{%- set child = props.get('prop.get_child', 1) | int(1) %}" "{%- set bat = props.get('prop.battery') %}" "{%- set bat2 = props.get('prop.battery2') %}"