Skip to content

Cover position control#408

Open
LorandBiro wants to merge 6 commits into
romasku:mainfrom
LorandBiro:cover-position
Open

Cover position control#408
LorandBiro wants to merge 6 commits into
romasku:mainfrom
LorandBiro:cover-position

Conversation

@LorandBiro
Copy link
Copy Markdown
Contributor

@LorandBiro LorandBiro commented Apr 21, 2026

This PR is the 3rd phase of the window covering feature started in issue #270. It contains position control and tracking.

The branch needs a little more work so it's created as a draft, but builds are available for testing:

@LorandBiro
Copy link
Copy Markdown
Contributor Author

I added an "overrun" feature, which lets you press open or close again at the end position to run the motor for another 3 seconds. I did this because calibration was frustrating when the controller thought it had reached the end position, but the cover hadn’t actually reached it physically. The only way to fix that was to send the cover all the way in the opposite direction and then back again. This helped a lot while I was calibrating the dead zones.

I’m also considering adding a fixed 1-second overrun whenever we’re at an end position, to improve accuracy when the cover only moves between 0% and 50% without ever reaching 100%. A fixed overrun would ensure that 0% truly hits the physical end position.

Any feedback/idea on how to improve this is welcome.

@LorandBiro
Copy link
Copy Markdown
Contributor Author

Ok, so the question about the "overrun" feature seems to be clarified. I added a 3-second overrun each time the cover is sent to an end position. This allows for realignment in cases where the cover is only used at one end of its range. If both the open and close times are set to 0, manual mode is activated. In this mode, there is no position tracking or control. Based on testing so far, this provides an easy way to set up and calibrate the covers.

@LorandBiro LorandBiro marked this pull request as ready for review April 27, 2026 12:33
@LorandBiro
Copy link
Copy Markdown
Contributor Author

I’ve finished the PR, but there are two points where I need help:

  • I updated MODULE_TUYA_NOVATO_TS130F to enable OTA updates. @DawidLBD, @andrei-lazarov - could you test and verify that OTA is working and that my changes are correct?
  • I had to implement a workaround to retrieve the argument from the GoToLiftPercentage command. It doesn’t look great. Does anyone have an idea why cmd_incoming_from_addr_info is not working here? See zigbee_zcl.c

@DawidLBD
Copy link
Copy Markdown

I performed an OTA test with the custom FW, and it loaded without any issues. The config string is correct.

@Haydend
Copy link
Copy Markdown
Contributor

Haydend commented May 2, 2026

I did some investigation into why cmd_incoming_from_addr_info isn't working for you.

I believe it's because of zcl_window_covering_custom.c. This memory stuff is a bit beyond me at the moment, so I don't really understand what's happening there. But I used zcl_level.c from the Telink library as an example and reimplemented it. That appears to have fixed it.

I have a branch with the changes you can take a look at. I've run it on a device and it worked.
The key files:
https://github.com/Haydend/tuya-zigbee-switch/blob/dunnicliffeh/cover-position-investigation/src/telink/custom_zcl/zcl_window_covering_custom.c
https://github.com/Haydend/tuya-zigbee-switch/blob/dunnicliffeh/cover-position-investigation/src/telink/hal/zigbee_zcl.c

It feels like Telink are doing something weird/wrong here, but it might be best to follow their lead to avoid confusion about why this one callback works differently.
Hopefully this helps someone who's better at C to understand what's happening here.

Also, have you done all this using only OTA? That's hardcore! I would of bricked so many devices.

@Haydend
Copy link
Copy Markdown
Contributor

Haydend commented May 3, 2026

TL;DR In zcl_window_covering_custom.c pass a pointer to zclIncoming_t->addrInfo, instead of building your own zclIncomingAddrInfo_t.
return(incoming->clusterAppCb(&(incoming->addrInfo), incoming->hdr.cmd, payload));

Slept on this, and I think I understand it a bit more now. The Telink cmd handlers all pass a pointer to the addrInfo (zclIncomingAddrInfo_t ) member of the zclIncoming_t struct they receive. cmd_incoming_from_addr_info is some C magic which gets you a pointer to the whole zclIncoming_t struct, from a pointer to a member of that struct. In your custom cmd handler, you were building a fresh zclIncomingAddrInfo_t struct. The pointer to this fresh struct is in no way connected to the incoming zclIncoming_t, thus cmd_incoming_from_addr_info doesn't work.

Still not 100% clear on how cmd_incoming_from_addr_info works. It appears to also be known as the container_of macro and is used a lot in the linux kernel. Something to do with a struct being contiguous memory, therefore if you have a pointer to something in that struct you can travel along the memory to find the pointer to the whole thing. Very clever.

See https://github.com/Haydend/tuya-zigbee-switch/blob/dunnicliffeh/cover-position-investigation/src/telink/custom_zcl/zcl_window_covering_custom.c

@LorandBiro
Copy link
Copy Markdown
Contributor Author

@Haydend Thank you so much for looking into this, it felt impossible to figure out without serial access or access to the Telink source code. I tested the changes locally, and everything seems to work correctly.

Also, have you done all this using only OTA? That's hardcore! I would have bricked so many devices.

Yeah, I did everything over OTA, the first update was nerve-wracking :D But I got lucky and didn’t brick any devices.

@FrancescoTalotta
Copy link
Copy Markdown

Hello,

Thank you very much for your help.

I have six of these curtain modules purchased from AliExpress: https://alishort.com/IebOn

I was wondering whether it would be possible to include the corresponding firmware support directly in the main build.

I noticed that there are two links available: one for the OTA firmware and one for the Zigbee2MQTT converter. Could you please confirm whether these are already updated to the latest version?

Thank you in advance for your support.

Francesco

LorandBiro and others added 5 commits May 7, 2026 13:13
Using deviceAddCustomCluster for the cover switch and cover clusters
caused the Zigbee2MQTT UI to crash when opening the binding and
reporting tabs, and made the reconfigure process fail. The issue was
reproducible on the main branch as well, so it was not a regression
introduced here.

Replacing the custom cluster registrations with direct numeric cluster
IDs and inline {ID, type} attribute definitions resolves the crashes
and restores reliable reconfiguration. The trade-off is reduced
visibility of custom attributes in the Z2M reporting UI.
Update stock_converter_manufacturer from Tuya to Lonsonho and
stock_converter_model from TS130F to QS-Zigbee-C03 to match the
actual Z2M converter. Add previously null stock_manufacturer_id
(4417) and stock_image_type (54179) for correct OTA identification.
- Split travel_time into separate open_time and close_time attributes
  (close_time falls back to open_time for symmetric covers)
- Rename open/closed slack to deadzone with percentage-based config
- Swap open/closed deadzone attribute order (0xff04=open, 0xff05=closed)
- Add 3-second overrun when motor reaches an end position for
  reliable calibration
- Add manual mode when both travel times are zero: position tracking
  is disabled, motor runs until stopped or 5-minute safety timeout
- Configurable deadzones as percentage of total travel
  (previously fixed at 5%)
- Remove minimum travel time constraint; always apply overrun at ends
- Remove COVER_ACTION_OPEN and COVER_ACTION_CLOSE; use position-based
  control exclusively
- Housekeeping: remove forward declarations, rename update task,
  simplify cover_dispatch_action, improve documentation
- Add custom ZCL cluster handler for Telink window covering
- Expand test coverage for all new position tracking behavior
- Update changelog, supported devices doc, and devcontainer
- Mark GIRIER_TS130F and TUYA_NOVATO_TS130F as fully_supported
The custom zcl_windowCovering_custom_cmdHandler was building a fresh
stack-local zclIncomingAddrInfo_t and passing its address to the app
callback. The callback (cmd_callback_window_covering) uses
cmd_incoming_from_addr_info — a container_of / offsetof macro — to
recover the enclosing zclIncoming_t* from the addrInfo pointer. Since
the stack-local copy has no enclosing struct, container_of computed a
garbage pointer, making cmd_incoming_from_addr_info fail silently.

Fix this by passing &(incoming->addrInfo) — the actual member of the
real zclIncoming_t struct — matching how all other Telink cluster
handlers invoke their app callbacks. With the correct pointer,
cmd_callback_window_covering can use cmd_incoming_from_addr_info to
recover pInMsg->pData and pInMsg->dataLen, removing the old workaround
that passed cmdPayload with a hardcoded length of 1.

Co-authored-by: Hayden <haydendunnicliffe@gmail.com>
The cover moving property was not being reported to Home Assistant
because the property name was prefixed (e.g. "cover_left__moving") and
Zigbee2MQTT looks it up by the exact name "moving". Also change the
access mode from STATE_GET to STATE so the value is properly exposed as
a state attribute.

See https://github.com/Koenkk/zigbee2mqtt/blob/master/lib/extension/homeassistant.ts#L844

This fixes cover.toggle behaviour: previously HA would toggle between
open and closed based on position > 0, and could not detect or stop
an in-progress movement.

Co-authored-by: DawidLBD <112850302+DawidLBD@users.noreply.github.com>
@LorandBiro
Copy link
Copy Markdown
Contributor Author

@FrancescoTalotta

Hello,

Thank you very much for your help.

I have six of these curtain modules purchased from AliExpress: https://alishort.com/IebOn

I was wondering whether it would be possible to include the corresponding firmware support directly in the main build.

I noticed that there are two links available: one for the OTA firmware and one for the Zigbee2MQTT converter. Could you please confirm whether these are already updated to the latest version?

Thank you in advance for your support.

Francesco

Hey,

I also use those curtain modules, it should be working perfectly.

Some functionality is already merged to main. If you can live without the position control, you can already update your devices by following the instructions in the documentation. This PR will be merged soon, then an official firmware upgrade will add position control.

The converter and OTA files are not updated in this branch. If you want to test the changes before the merge, you can get the binaries and the converter from my build branch: cover-position-build

You can get the binaries directly from there, or you can follow the documentation and change the URLs. Please note that you'll have to wait for this build before the files are updated on the build branch: https://github.com/LorandBiro/tuya-zigbee-switch/actions/runs/25492620252

@FrancescoTalotta
Copy link
Copy Markdown

FrancescoTalotta commented May 7, 2026

Hello,
Thank you very much for the binaries, and really nice work indeed!
I flashed the following firmware: Firmware ID: 1.1.2-0564bee3 20260507 Firmware version: 285356037

I have not tested all the functions yet, but the switch already works much better with this new firmware.
However, I noticed two issues. It seems that I cannot invert the motor direction, meaning the UP/DOWN functions cannot be swapped. In order to see the cover position percentage, I need to manually click the small refresh button on the right to update the position. With the stock firmware, the position was updated automatically by sending MQTT messages. This was very useful, especially to monitor remotely what was happening with the cover when I was away from home.

Thank you again for your work and support.

EDIT: Probably it was me or something wrong with Z2M. This morning I opened Z2M again and the motor direction and cover position percentage were working very well. The position is updated automatically.

EDIT2: Ok found the issue. I forgot to Reconfigure the devices after firmware upgrade. Now it works very well.

Francesco

@Todelec
Copy link
Copy Markdown

Todelec commented May 9, 2026

Hi,
great work you're doing here.

I first updated my 2-gang switches and then came across your roller shutter branch, which I flashed immediately.
So far it’s working excellently!

I’m already looking forward to the position display in the Home Assistant frontend.

Are you planning to implement direct binding?

@LorandBiro
Copy link
Copy Markdown
Contributor Author

@FrancescoTalotta Thank you very much for testing the branch and sharing your solution.

@Todelec Happy to hear that! Could you clarify which firmware version you deployed to your shutter devices? Home Assistant should already display the position properly if you're using the build from my branch. If the position is available in the Z2M UI but not in Home Assistant, please share the details so I can investigate further.

Direct binding is already available in the official build. You can set up the switches of a cover module to control other cover modules. Is this what you meant by direct binding?

@Todelec
Copy link
Copy Markdown

Todelec commented May 11, 2026

Hi,

Thank you for your quick reply!

I’m using version 1.1.2-0564bee3. At first, there were no entries in the Z2M reporting tab, but after adding them, the position is now displayed correctly in the frontend. That was my mistake.

Regarding binding:
I would like to bind a TS0002-GIR-zmy4lslw device to a cover module. However, when selecting the cover module as the target, no clusters are available to choose from. I only see endpoints 1 and 2, plus cover_switch and cover.

I was able to successfully bind two TS0002-GIR-zmy4lslw devices together.
However, I’m still new to this topic and learning. Any advice would be appreciated.

Thanks in advance!

@LorandBiro
Copy link
Copy Markdown
Contributor Author

LorandBiro commented May 14, 2026

@Todelec if there were no entries in the reporting tab, you should probably re-interview and reconfigure the device, that usually helps. Also, binding has a really bad user interface in Z2M. I just tried binding between two Girier modules, and when I chose the second endpoint, I could choose the cluster as well. After that, binding worked properly.

image

@yarfalksol
Copy link
Copy Markdown

@LorandBiro thank you very much for your effort, great work!
I'm currently using a few Wi-Fi shutter modules, but I'm planning to switch to Zigbee, as Zigbee is my primary choice for switches and dimmers
I've tested your latest build 0564bee3 on GIRIER_TS130F_1GANG/dwytrmda with great success. I have two comments:

  • my coverOpenTime is 66 seconds and coverCloseTime is 62 seconds, but it seems the maximum for both in the current build is 60 seconds. 60 seconds works fine, but 66 only lasts a few seconds. I found your statement: Add coverOpenTime and coverCloseTime converter helpers (numeric, 0–60000 ms, config category) - could the upper limit be raised?
  • my Wi-Fi shutter modules support slat angle positioning. Do you plan to introduce this feature as well?

@Todelec
Copy link
Copy Markdown

Todelec commented May 17, 2026

@Todelec if there were no entries in the reporting tab, you should probably re-interview and reconfigure the device, that usually helps. Also, binding has a really bad user interface in Z2M. I just tried binding between two Girier modules, and when I chose the second endpoint, I could choose the cluster as well. After that, binding worked properly.

@LorandBiro

Thank you very much for your support.

What I was trying to do apparently cannot work.
I wanted to bind a regular switch with the cover module.

In the meantime, I found out that this cannot work because the switch does not have a closuresWindowCovering client cluster.

If I use a cover module instead, I can select the closuresWindowCovering cluster as well, just like in your screenshot.

So that’s how I’m going to rebuild it now — of course with your firmware ;)

BR and thanks a lot!
Tod

@LorandBiro
Copy link
Copy Markdown
Contributor Author

LorandBiro commented May 23, 2026

@yarfalksol Sorry I missed your message last week. The open and close time is currently limited to 300 seconds:
https://github.com/romasku/tuya-zigbee-switch/pull/408/changes#diff-a5fccb8a1b662749470d1d475d4735323a99640a3796826a68375da1a1c8d0baR372

Where did you see that the open and close time is limited to 60 seconds? I looked through the git history and the code, but I couldn't find it.

I don't plan to add support for slat angle. This "small" contribution already turned out to be much bigger than I anticipated :)
But I do recommend creating an issue, since others have already mentioned this requirement.

@yarfalksol
Copy link
Copy Markdown

@LorandBiro seems cover_travel_time() returns uint16_t, causing overflow for travel times > 65 s

cover_travel_time() in src/zigbee/cover_cluster.c (line 54) stores the result of open_time * 100 (or close_time * 100) in a uint16_t return value. Since open_time/close_time are stored in tenths of a second (zigbee2mqtt scale: 10), a 66 s travel time is stored as 660, and 660 × 100 = 66 000 silently wraps around to 464 (66000 mod 65536). This makes the motor stop after ~3 s instead of running the full travel time.

The overflow threshold is 65535 / 100 = 655 tenths = 65.5 s. Values at or below 65 s work correctly; 66 s and above are broken. The same issue affects both open_time and close_time.

Seams cover_travel_time() on line 54 have to be changed from uint16_t to uint32_t. All call sites (lines 113, 152, 284) already assign the result to uint32_t, so no other changes are needed.

Once again thank you very much for your effort.

cover_travel_time() returned uint16_t but multiplied open_time/close_time
(which are stored in tenths of a second) by 100 to convert to milliseconds.
For travel times above 65.5 s (655 tenths), the result exceeded 65535 and
silently wrapped. A 66 s travel time (660 tenths) would produce 464 ms
instead of 66000 ms.

Co-authored-by: yarfalksol <285298507+yarfalksol@users.noreply.github.com>
@LorandBiro
Copy link
Copy Markdown
Contributor Author

@yarfalksol Oh, I understand now. Thank you so much for investigating this issue in so much detail. I applied the fix and I marked you as co-author. The new build should be available soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants