Skip to content

Add full support for Tuya BAC-002-ALZB (schedule_text, improved off behavior, calibration -9/+9)#11004

Merged
Koenkk merged 8 commits intoKoenkk:masterfrom
kuposcar:master
Dec 18, 2025
Merged

Add full support for Tuya BAC-002-ALZB (schedule_text, improved off behavior, calibration -9/+9)#11004
Koenkk merged 8 commits intoKoenkk:masterfrom
kuposcar:master

Conversation

@kuposcar
Copy link
Copy Markdown
Contributor

What does this PR do?

This PR adds full native support for the Tuya BAC-002-ALZB FCU thermostat.

Improvements included

Adds single-line unified weekly schedule_text (12 segments), readable and writable from Home Assistant or MQTT.

Fixes the long-standing issue where the thermostat would not turn off on first attempt.

Adds correct temperature calibration range −9 to +9°C, useful when using an external sensor.

Uses the modernExtend tuyaBase with forceTimeUpdates to ensure proper time sync.

Provides stable system_mode → Tuya DP mapping and state consolidation.

Adds whiteLabel and full fingerprint support for _TZE200_dzuqwsyg and _TZE204_dzuqwsyg.

Compatibility requirements

Works on Zigbee2MQTT 2.7.1 or newer (uses modernExtend and unified Tuya datapoints).

Notes

Tested extensively on the actual device.
Provides schedule parsing identical to older versions but in a cleaner, safer, more automation-friendly format.

@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Dec 13, 2025

Could you take a look at the merge conflicts?

@Koenkk Koenkk merged commit b719b92 into Koenkk:master Dec 18, 2025
3 checks passed
@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Dec 18, 2025

Thanks!

@kuposcar
Copy link
Copy Markdown
Contributor Author

Thanks for the review!

@fapgomes
Copy link
Copy Markdown

fapgomes commented Jan 4, 2026

After this update, I can't turn off my BAC device. It changes to cool instead of off.

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

robvanoostenrijk commented Jan 5, 2026

Was this tested in a 2-pipe cooling only configuration as well?

Confirming the same. These changes make Zigbee2MQTT unable to turn off the airconditioning entirely.

@Koenkk Can this be reverted until the author fixes these changes?

@fapgomes
Copy link
Copy Markdown

fapgomes commented Jan 5, 2026

Mine is also a 2-pipe, for both hot and cold use.

@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Jan 5, 2026

Reverted in #11209

Changes will be available in the dev branch in a few hours from now and in the next release which is every 1st of the month.

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 5, 2026

I’ve already identified and fixed the issue.
It seems to originate from Home Assistant.

I’ll open a PR as soon as I can. In the meantime, I’m sharing the external converter in case anyone wants to use it until the next release.

I’ve tested the external converter myself and also shared it with a few other users, and so far it appears to be working well.

I’ve also updated the deadzone range to go from 0 to 5; previously it started at 1.

The file is provided in .mjs format so it can be merged directly later on.

custom_bac002_2.7.1.zip

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

robvanoostenrijk commented Jan 6, 2026

@kuposcar,

Thanks for improving the BAC-002-AZLB / BAC-003 integration and providing the external converter for validation.

I've reviewed your changes:

  • Off Modifications

    I can see you are now turning the device on, in order to turn it off 120ms later.
    On my devices this leads to an unnecessary backlight flash (screen lighting up) when turning off.
    Direct off works fine on both BAC-003 and BAC-002-ALZB.

    Are you sure the change you made is not working around another automation in a downstream system conflicting with this mode change? The best way to test is to disconnect Zigbee2MQTT from downstream and ensure you don't have something like Zigbee2MQTT automations add-on loaded.

  • Device Names

    From the information I can see on the internet:
    _TZE204_dzuqwsyg: BAC-003
    _TZE200_dzuqwsyg: BAC-002-ALZB

    Action: Updated the whitelabel for BAC-003.

  • Schedule Text

    Good change, it will indeed made automation values easier to set, which is the most likely interaction method.

  • Deadzone Temperature

    It was previously limited to -3..3 because because these are the maximum values selectable in the on device maintenance menu (tested on _TZE204_dzuqwsyg and _TZE200_dzuqwsyg). The manual for the WiFi one I found online does indeed say -9..+9.
    I've confirmed however that -9..+9 works, however if you set -9 from Zigbee2MQTT, in the on-device menu you can then continue past the built-in block at -3 to -A, -B, -C (hex values). I'm not sure if thats healthy and could corrupt the device memory, particularly if continuing into double byte values.

    Hidden menu settings for reference:
    image

  • System Mode (Cooling Only)

    I run these devices in a hot climate. As such they are 2 pipe models (in cold, out cold), compared to 4 pipe models which have (in hot, in cold, out cold, out hot). When using your converter and integrating it with Google Home, it gets confused and shows only heat mode.

    image

    I can see that the device specific configuration for it is still in place, but the code handling the setting has maybe been accidentally removed.

    Action: Added back in the system mode modification in expose for 2-pipe mode.

  • System Mode (Add Off)
    On these devices, the system mode datapoint does not include off. Thats why previously I created a virtual off by adding it in the system mode and sending the right data point commands.
    However, some using downstream integrations wanted the off in system mode to be optional, thats why there was a configuration for it. I can see you left the code for it in datapoint 1 handling, so maybe this was an oversight.

    The option splits the off function back into the original state property.

    Action: I've added it back in expose for now. However if the community does not require this anymore it could be removed.

custom_bac002_2.7.1_mod.zip

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 6, 2026

Thanks for the detailed review, I’ll take a look at it.

The reason for the power-on before power-off is that on the BAC-002 units I tested, the device would not turn off when sending an OFF command directly, as it did not “wake up” first.

Regarding OFF handling, it’s a bit tricky because by default ON/OFF is independent from the system mode, and including it inside the mode causes issues. In the previous external converter I made, the main complaints were precisely that ON/OFF was independent from the mode, matching the original Tuya behavior.

Just to confirm: when you mention deadzone, you are referring to the temperature calibration/compensation, correct?

For reference, I’ve been using calibration values in the -9 to +9 range on my devices for about a year without observing any issues or unexpected behavior.

If your converter works well for everyone, we can base the merge on your implementation.

Thanks a lot.

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

Thanks for the detailed review, I’ll take a look at it.

The reason for the power-on before power-off is that on the BAC-002 units I tested, the device would not turn off when sending an OFF command directly, as it did not “wake up” first.

Based on your feedback, I've added a device specific setting to do this: "Wake before power off".
When this is enabled it will send the power-on command and wait 120ms to send the power-off command as per your logic.
If disabled, it just turns the device off as before.

Regarding OFF handling, it’s a bit tricky because by default ON/OFF is independent from the system mode, and including it inside the mode causes issues.

That's why the device-specific setting is there. If you want a separate on/off switch entity, you flip that setting in Z2M.
The combined system mode is required for downstream systems such as Alexa and Google Home, these do not understand a separate on/off switch entity for an Airconditioning control unit.

Just to confirm: when you mention deadzone, you are referring to the temperature calibration/compensation, correct?
Sorry, temperate calibration indeed. Just highlighting that while -/+9 works it goes beyond what the on-device menu allows.

For reference, I’ve been using calibration values in the -9 to +9 range on my devices for about a year without observing any issues or unexpected behavior.
Anything over -/+3 seems quite a large measurement deviation. But lets keep this in then, its bound-limited by Z2M regardless.

If your converter works well for everyone, we can base the merge on your implementation.

Let's merge this all together and re-submit the pull request. All pre-existing use-cases are now covered.

Attaching the updated version with a device-setting for "Wake before poweroff":
custom_bac002_2.7.1_mod_2.zip

@fapgomes
Copy link
Copy Markdown

fapgomes commented Jan 6, 2026

I can confirm that the @robvanoostenrijk version, works.

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 7, 2026

Thanks for the detailed review, I’ll take a look at it.
The reason for the power-on before power-off is that on the BAC-002 units I tested, the device would not turn off when sending an OFF command directly, as it did not “wake up” first.

Based on your feedback, I've added a device specific setting to do this: "Wake before power off". When this is enabled it will send the power-on command and wait 120ms to send the power-off command as per your logic. If disabled, it just turns the device off as before.

Regarding OFF handling, it’s a bit tricky because by default ON/OFF is independent from the system mode, and including it inside the mode causes issues.

That's why the device-specific setting is there. If you want a separate on/off switch entity, you flip that setting in Z2M. The combined system mode is required for downstream systems such as Alexa and Google Home, these do not understand a separate on/off switch entity for an Airconditioning control unit.

Just to confirm: when you mention deadzone, you are referring to the temperature calibration/compensation, correct?
Sorry, temperate calibration indeed. Just highlighting that while -/+9 works it goes beyond what the on-device menu allows.

For reference, I’ve been using calibration values in the -9 to +9 range on my devices for about a year without observing any issues or unexpected behavior.
Anything over -/+3 seems quite a large measurement deviation. But lets keep this in then, its bound-limited by Z2M regardless.

If your converter works well for everyone, we can base the merge on your implementation.

Let's merge this all together and re-submit the pull request. All pre-existing use-cases are now covered.

Attaching the updated version with a device-setting for "Wake before poweroff": custom_bac002_2.7.1_mod_2.zip

Thanks a lot for the work, I really appreciate the effort you’ve put into this 👍

I’ve been testing this myself and together with a few other users. Overall, it works well, but we’ve noticed a couple of practical issues.

The main one is that the wake_before_power_off on/off switch does not appear in the UI. This means users would need to touch YAML or rely on a modified external converter to control it, which could be a problem for many users.

Based on your feedback and your external implementation, I tried an alternative approach: instead of powering on before powering off, I’m now sending two consecutive power-off commands. With this approach, the thermostat consistently turns off, and it does so without the LED flashing that was mentioned in earlier comments.

Another issue I personally ran into with this converter was that, once the device was turned off, it would not always turn back on. To address this, I added a small piece of logic where, if the device is off, it briefly powers on first and then switches to the requested mode. With this change in place, the behavior seems stable.

In any case, if the device is off and you set any system mode, the LED will turn on anyway, so I don’t think this causes any real negative side effects.

If you can confirm that this external behaves correctly for you as well, I think we’re in a good position to move forward and use this as the final solution.
custom_bac002_2.7.1_mod_3.zip

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

robvanoostenrijk commented Jan 7, 2026

@kuposcar,

Its definitely in the UI, are you looking in the right place?

Windfront UI:

image

Zigbee2MQTT legacy UI:

image

On a siode-note, it just seems sending double commands is a band-aid workaround for an issue which is originating somewhere else in your setup.
I've been committed code to this converter a few times over the years and out of the numerous users you are the first one with this specific issue requiring the device to 'wake up'.

Once you confirm that you can see "Wake before power off" in "Settings (specific)" it should anyways fit your requirements.
No need for any YAML editing.

But since you are experiencing this issue with both turning it on and off, maybe it should be "Wake before power action".

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 7, 2026

image

I tried again, but I’m unable to set the state; it always ends up as undefined. I’m not sure why. In the last uploaded external, this switch is not required.

If this issue is that specific, it might make sense to open a PR with your proposed solution, and I can keep using a specific external file for my use case.

Edit: I’ll also try reinstalling Zigbee2MQTT from scratch to see if that fixes the undefined issue.

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

robvanoostenrijk commented Jan 7, 2026

Hmm, thats weird.

The moment I click either true or false on the setting, it will change to a defined state (if its the initial click) and stores the value in the YAML.

  '0xa4c138170de082f2':
    friendly_name: Airconditioning - Bedroom
    control_sequence_of_operation: cooling_only
    debounce: 3
    expose_device_state: false
    wake_before_power_off: false

cat configuration.yaml | grep -i 'wake_before_power_off'

Obviously I'd like to support your scenario if possible, so lets do some debugging. The switch should definitely work as part of default Z2M functionality.

Note when changing the option in Z2M (at least in Windfront), you'll see a green pop-up bottom right with "Options (####)"

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 7, 2026

After extensive testing on six BAC-002 thermostats, I believe the root cause of the unreliable on/off behavior is related to 2-pipe Cooling & Heating configurations, which are neither classic 4-pipe setups nor cooling-only devices.
This configuration seems to behave slightly differently at firmware level.

During testing, I also encountered inconsistent behavior in my own Zigbee2MQTT setup (after a clean reinstall, the wake_before_power_off option suddenly became selectable again), which suggests part of the issue may be related to state desynchronization rather than pure converter logic.

What this version does

Keeps the default behavior unchanged when wake_before_power_off is disabled
→ In this case, the converter behaves exactly the same as before.

Adds an optional wake-before logic when wake_before_power_off is enabled:

Ensures the device always powers off reliably

Ensures the device always powers on reliably

This includes intentionally duplicated DP1 commands in some transitions

Although duplicating commands may look sub-optimal, it is currently the only solution that works consistently across all tested devices in this specific configuration.

Why the duplicated calls exist

Some BAC-002 units ignore OFF commands when internally asleep

Some units ignore mode changes if DP1 was not asserted shortly before

The duplicated calls guarantee a state transition edge, which appears to be required by certain firmwares

This issue has been reported by multiple users (not only myself), mainly in Home Assistant + Zigbee2MQTT setups, including reports in Spanish community forums.

Renaming clarification

Renamed “Cooling and Heating 4-pipes” to “Cooling and Heating”

This avoids confusion for users with 2-pipe Cooling & Heating installations

Functionality remains identical; this is a label clarification only

Summary

Default behavior: unchanged

Optional behavior: improves reliability

No breaking changes

No impact unless the option is explicitly enabled

At this point, the exact firmware-level cause is still unclear, but this solution has proven to be stable and repeatable across all my tested devices.

I would really appreciate your feedback on this approach.
It’s clear from your previous comments and reviews that you have deep knowledge of this codebase, and before continuing further, I’d like to hear your opinion on whether this solution makes sense or if you would recommend a different direction.

custom_bac002_2.7.1_mod_6.zip

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

Ok, I took mod_6 and created a branch for it here:
https://github.com/robvanoostenrijk/zigbee-herdsman-converters/tree/devices/BAC-002-ALZB

Principally the same, just some cosmetic updates to align the options with what you explained above.

image

Let me know if this resolves all of the mentioned issues and we can resubmit this upstream.

As external converter:
custom_bac002_2.7.1_mod_7.zip

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 7, 2026

Thanks, I’ve tested it and for me it looks perfect 👍

The only remaining thing I’d like to ask is about the Deadzone minimum value. Right now it’s set to 1, but it would be great if the minimum could be 0 instead.

There are quite a few users requesting a deadzone value of 0, and in fact on newer devices currently being shipped, the minimum deadzone is already 0 and it works fine in practice.

Anyone who prefers a deadzone of 1 can still explicitly set it to 1, but allowing 0 as the minimum gives more flexibility and matches current device behavior.

line 44
e.numeric("deadzone_temperature", ea.STATE_SET).withUnit("°C").withValueMax(5).withValueMin(0),

Other than that, everything looks good to me.

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

I've updated the deadzone_temperature to 0..5.

Interesting because the manual states 1..5, so I did not consider 0 to be a valid value.
I'll test at home how often this makes the unit switch on/off.

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 8, 2026

Good morning,

I just wanted to add that I have always used a deadzone value of 0 since I’ve had this device, and I’ve never experienced any issues with it.

The only real limitation, in my opinion, is that it must not be a decimal value, which is another request some users have mentioned — but integer values work perfectly fine.

On another note, while reviewing the code in more depth, I noticed something I wanted to ask about.

On line 66 you are checking meta.options?.… in the if, but on lines 72, 85 and 92 the if conditions don’t use meta.options., only use options?

Shouldn’t this be consistently using meta.options in all cases? I just wanted to double-check in case this was intentional.

[
                    1,
                    "state",
                    {
                        to: async (v, meta) => {
                            if (meta.options?.expose_device_state === true) {
                                await tuya.sendDataPointBool(meta.device.endpoints[0], 1, utils.getFromLookup(v, { on: true, off: false }), "dataRequest", 1);
                            }
                        },
                        from: (v, meta, options) => {
                            meta.state.system_mode = v === true ? (meta.state.system_mode_device ?? "cool") : "off";
                            if (options?.expose_device_state === true)
                                return v === true ? "ON" : "OFF";
                            delete meta.state.state;
                        },
                    },
                ],
                [
                    2,
                    "system_mode",
                    {
                        to: async (v, meta, options) => {
                            const ep = meta.device.endpoints[0];
                            if (v === "off") {
                                if (options?.wake_before_power_transition === true) {
                                    await tuya.sendDataPointBool(ep, 1, true, "dataRequest", 1);
                                    await new Promise((r) => setTimeout(r, 120));
                                }
                                await tuya.sendDataPointBool(ep, 1, false, "dataRequest", 1);
                                return;
                            }
                            if (options?.wake_before_power_transition === true) {
                                if (meta.state.system_mode === "off") {
                                    await tuya.sendDataPointBool(ep, 1, true, "dataRequest", 1);
                                    await new Promise((r) => setTimeout(r, 120));
                                }
                            }

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

Well spotted!

The function signature for to is (v, meta). While from is (v, meta, options).

I've now updated (and tested) this appropriately.

In the previous build, the power transition fix should not have worked for your device though, because the option would never evaluate to true. Maybe the device responses are inconsistent, or your Zigbee2MQTT reinstallation fixed something.

Updated external converter attached.

custom_bac002_2.7.1_mod_8.zip

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 8, 2026

I’m going to test this new version now.

What happened on my side is that at first it seemed to work fine, but as I kept testing I noticed that sometimes it failed, almost as if the Wake before power off option was disabled.

That inconsistency is what made me look into the code more closely and suspect this part in the first place.

I assume this should now work consistently, but I’ll run some tests to confirm.

Thanks a lot for the quick update and the explanation.

@kuposcar
Copy link
Copy Markdown
Contributor Author

kuposcar commented Jan 9, 2026

I’ve tested it quite a bit now, and it behaves consistently and correctly.

From my side it looks ready to be submitted upstream and included in 2.7.3.

@robvanoostenrijk
Copy link
Copy Markdown
Contributor

From my side it looks ready to be submitted upstream and included in 2.7.3.

Thank you for testing.

PR created:
#11254

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants