Hi,
for everyone who need integrate Zigbee curtain motor ( MI-P82 Zigbee ) from Aliexpress
Here is my working quirk for TS0601 TZE204_tgl8i2np
/zhaquirk/ts0601_curtain.py
from typing import Dict, Optional, Union
from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.closures import WindowCovering
from zigpy.zcl.clusters.general import (
Basic,
GreenPowerProxy,
Groups,
Ota,
Scenes,
Time,
)
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
)
from zhaquirks.tuya import (
NoManufacturerCluster,
TUYA_MCU_COMMAND,
TuyaLocalCluster,
TuyaWindowCover,
)
from zhaquirks.tuya.mcu import (
DPToAttributeMapping,
TuyaClusterData,
TuyaMCUCluster,
)
class TuyaCC(t.enum8):
OPEN = 0x00
STOP = 0x01
CLOSE = 0x02
class ZclCC(t.enum8):
OPEN = 0x00
CLOSE = 0x01
STOP = 0x02
TUYA2ZB_COMMANDS = {
ZclCC.OPEN: TuyaCC.OPEN,
ZclCC.CLOSE: TuyaCC.CLOSE,
ZclCC.STOP: TuyaCC.STOP,
}
class TuyaWindowCovering(NoManufacturerCluster, WindowCovering, TuyaLocalCluster):
attributes = WindowCovering.attributes.copy()
attributes.update(
{
0xF000: ("curtain_switch", t.enum8, True),
0xF001: ("accurate_calibration", t.enum8, True),
0xF002: ("motor_steering", t.enum8, True),
0xF003: ("travel", t.uint16_t, True),
# Explicitly define moving_state attribute to parse DP 7 cleanly
0xF004: ("moving_state", t.enum8, True),
}
)
async def command(
self,
command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
*args,
manufacturer: Optional[Union[int, t.uint16_t]] = None,
expect_reply: bool = True,
tsn: Optional[Union[int, t.uint8_t]] = None,
):
if command_id in (0x0002, 0x0000, 0x0001):
cluster_data = TuyaClusterData(
endpoint_id=self.endpoint.endpoint_id,
cluster_name=self.ep_attribute,
cluster_attr="curtain_switch",
attr_value=TUYA2ZB_COMMANDS[command_id],
expect_reply=expect_reply,
manufacturer=None,
)
self.endpoint.device.command_bus.listener_event(
TUYA_MCU_COMMAND,
cluster_data,
)
return foundation.GENERAL_COMMANDS[
foundation.GeneralCommand.Default_Response
].schema(command_id=command_id, status=foundation.Status.SUCCESS)
elif command_id == 0x0005:
lift_value = args[0]
cluster_data = TuyaClusterData(
endpoint_id=self.endpoint.endpoint_id,
cluster_name=self.ep_attribute,
cluster_attr="current_position_lift_percentage",
attr_value=lift_value,
expect_reply=expect_reply,
manufacturer=None,
)
self.endpoint.device.command_bus.listener_event(
TUYA_MCU_COMMAND,
cluster_data,
)
return foundation.GENERAL_COMMANDS[
foundation.GeneralCommand.Default_Response
].schema(command_id=command_id, status=foundation.Status.SUCCESS)
return foundation.GENERAL_COMMANDS[
foundation.GeneralCommand.Default_Response
].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)
class TuyaWindowCoverManufCluster(TuyaMCUCluster):
attributes = TuyaMCUCluster.attributes.copy()
attributes.update(
{
0x5000: ("backlight_mode", t.enum8, True),
0x8001: ("indicator_status", t.enum8, True),
}
)
dp_to_attribute: Dict[int, DPToAttributeMapping] = {
1: DPToAttributeMapping(
TuyaWindowCovering.ep_attribute,
"curtain_switch",
),
2: DPToAttributeMapping(
TuyaWindowCovering.ep_attribute,
"current_position_lift_percentage",
),
3: DPToAttributeMapping(
TuyaWindowCovering.ep_attribute,
"current_position_lift_percentage",
),
7: DPToAttributeMapping(
TuyaWindowCovering.ep_attribute,
"moving_state",
),
}
# Merged handlers together properly so they map to the correct update parser
data_point_handlers: Dict[int, str] = {
1: "_dp_2_attr_update",
2: "_dp_2_attr_update",
3: "_dp_2_attr_update",
7: "_dp_2_attr_update",
}
class TuyaCover0601_GP(TuyaWindowCover):
signature = {
MODELS_INFO: [
("_TZE204_r0jdjrvi", "TS0601"),
("_TZE204_tgl8i2np", "TS0601"),
],
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaWindowCoverManufCluster.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242: {
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}
replacement = {
ENDPOINTS: {
1: {
DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaWindowCoverManufCluster,
TuyaWindowCovering,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242: {
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
}
}
curtain custom card code:
type: custom:button-card
entity: cover.zaves_zz
name: Curtain winter garden
show_name: false
show_icon: false
grid_options:
columns: 12
rows: 3
styles:
card:
- border-radius: 15px
- padding: 12px
- background-color: var(--card-background-color)
grid:
- grid-template-areas: |
"i title status status"
"visual visual visual visual"
"btn1 btn2 btn3 btn4"
- grid-template-columns: repeat(4, 1fr)
- grid-template-rows: auto auto auto
- row-gap: 12px
- column-gap: 8px
custom_fields:
title:
- grid-column: span 2
- justify-self: start
- font-weight: bold
- font-size: 14px
- padding-left: 8px
status:
- grid-column: span 2
- justify-self: end
- font-size: 13px
- color: var(--secondary-text-color)
visual:
- grid-column: span 4
- background: rgba(0, 0, 0, 0.2)
- border-radius: 6px
- height: 40px
- width: 100%
- position: relative
- overflow: hidden
- border: 1px solid rgba(255, 255, 255, 0.1)
custom_fields:
title: |
[[[ return "Curtain winter garden"; ]]]
status: |
[[[
if (entity.state === 'closed') return "Closed";
const pos = entity.attributes.current_position;
if (pos === 100) return "Open";
return pos + "% Open";
]]]
visual: |
[[[
const pos = entity.attributes.current_position || 0;
const panelWidth = (50 - (pos / 2));
return `
<div style="
position: absolute; left: 0; top: 0; bottom: 0;
width: ${panelWidth}%; background: var(--accent-color);
transition: width 0.5s ease-in-out; border-right: 1px solid rgba(0,0,0,0.3);
"></div>
<div style="
position: absolute; right: 0; top: 0; bottom: 0;
width: ${panelWidth}%; background: var(--accent-color);
transition: width 0.5s ease-in-out; border-left: 1px solid rgba(0,0,0,0.3);
"></div>
`;
]]]
btn1:
card:
type: custom:button-card
name: Closed
tap_action:
action: call-service
service: cover.set_cover_position
service_data:
entity_id: cover.zaves_zz
position: 0
styles:
card:
- background-color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 0 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
- color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 0 ? 'white' : 'var(--primary-text-color)' ]]]
- padding: 8px 2px
- border-radius: 6px
- font-weight: bold
- font-size: 11px
btn2:
card:
type: custom:button-card
name: 25%
tap_action:
action: call-service
service: cover.set_cover_position
service_data:
entity_id: cover.zaves_zz
position: 25
styles:
card:
- background-color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 25 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
- color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 25 ? 'white' : 'var(--primary-text-color)' ]]]
- padding: 8px 2px
- border-radius: 6px
- font-weight: bold
- font-size: 11px
btn3:
card:
type: custom:button-card
name: 75%
tap_action:
action: call-service
service: cover.set_cover_position
service_data:
entity_id: cover.zaves_zz
position: 75
styles:
card:
- background-color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 75 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
- color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 75 ? 'white' : 'var(--primary-text-color)' ]]]
- padding: 8px 2px
- border-radius: 6px
- font-weight: bold
- font-size: 11px
btn4:
card:
type: custom:button-card
name: Open
tap_action:
action: call-service
service: cover.set_cover_position
service_data:
entity_id: cover.zaves_zz
position: 100
styles:
card:
- background-color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 100 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
- color: >
[[[ return states['cover.zaves_zz'].attributes.current_position
=== 100 ? 'white' : 'var(--primary-text-color)' ]]]
- padding: 8px 2px
- border-radius: 6px
- font-weight: bold
- font-size: 11px
