local device = {} device.model = "TS0301_dooya_dm35" device.vendor = "Dooya" device.description = "DM35EQ/SP tubular curtain motor" -- Control path verified from live logs on a CALIBRATED motor: -- open/close/stop -> standard window-covering cluster 0x0102 (cmd 0x00/0x01/0x02) -- position -> 0x0102 goToLiftPercentage (cmd 0x05); device returns -- Default Response status 0x00 and the position tracks the -- setpoint (commanded 30 -> feedback settled at ~33). -- Note: EF00 dp2 does NOT act as a setpoint on this motor (it only mirrors the -- current position), and goToLift is a no-op until the motor's travel limits are -- calibrated -- which is why the earlier EF00-datapoint attempts all failed. -- EF00 dp3 is the position feedback and the ONLY signal for remote/manual -- operation (this firmware never reports a state datapoint). Following z2m -- (tuya.valueConverter.coverPosition), every dp3 report also derives the cover -- `state` (0=CLOSE / else=OPEN) via coverDeriveState, so state stays consistent -- with position -- fixing the "shows previous state" lag and reflecting remote -- ops. No gating/optimistic window: the report always carries a matching state. device.fingerprint = { { model = "TS0301", manufacturer = "_TZE210_cc35xtzk" }, } device.exposes = { { type = "cover", features = { { type = "enum", name = "state", property = "state", access = 3, values = { "OPEN", "CLOSE", "STOP" }, }, { type = "numeric", name = "position", property = "position", access = 7, unit = "%", min = 0, max = 100, description = "Position of this cover", }, }, }, { type = "numeric", name = "battery", property = "battery", access = 5, unit = "%", min = 0, max = 100, description = "Remaining battery in %", category = "diagnostic", }, } -- open/close/stop + position both go over the standard 0x0102 cluster. device.fromZigbee = { "battery" } device.toZigbee = { "window_covering", "window_covering_position" } device.tuyaDatapoints = { { dp = 1, name = "state", converter = "lookup", lookup = { OPEN = 0, STOP = 1, CLOSE = 2 } }, -- dp3 = position feedback. The motor reports it inverted (raw = % closed: -- 0=open..100=closed) while goToLift already drives the physical position -- correctly, so decode with coverPositionInverted (100-raw) to match the -- commanded value. coverDeriveState then derives state from the decoded -- (open%) value: 0 -> CLOSE, else -> OPEN. { dp = 3, name = "position", converter = "coverPositionInverted", access = "r", coverDeriveState = true }, } -- Wake the Tuya MCU so it streams its datapoints. device.tuya = { magicPacket = true } device.configure = { bind = { "genPowerCfg" }, reporting = { { cluster = "genPowerCfg", attribute = "batteryPercentageRemaining", min = 3600, max = 65000, change = 0, }, }, } return device