Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add device support for TS110E Model QS-Zigbee-D02-TRIAC-LN #1422

Merged
merged 7 commits into from
Jul 8, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions zhaquirks/tuya/ts110e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Tuya Dimmer TS110E."""
from typing import Optional, Union

from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.general import (
Basic,
GreenPowerProxy,
Groups,
LevelControl,
OnOff,
Ota,
Scenes,
Time,
)

from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
)
from zhaquirks.tuya import (
NoManufacturerCluster,
TuyaDimmerSwitch,
TuyaZBExternalSwitchTypeCluster,
)

TUYA_LEVEL_ATTRIBUTE = 0xF000
TUYA_BULB_TYPE_ATTRIBUTE = 0xFC02
TUYA_MIN_LEVEL_ATTRIBUTE = 0xFC03
TUYA_MAX_LEVEL_ATTRIBUTE = 0xFC04
TUYA_CUSTOM_LEVEL_COMMAND = 0x00F0


class TuyaLevelPayload(t.Struct):
"""Tuya Level payload."""

level: t.uint16_t
transtime: t.uint16_t
Comment on lines +39 to +43
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should probably move to tuya.__init__.py, but let's keep it here for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or first do the implementation in the quirk and getting it merged and its working OK and then making one more PR for moving it to tuya INIT then tuya INIT is little tricky with many addings and can needing rebasing many times and its PITA (i have one PR waiting for being merged to it).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the idea.
Keep here and merge. When required, move to Tuya INIT. 👍🏻



class TuyaBulbType(t.enum8):
"""Tuya bulb type."""

LED = 0x00
INCANDESCENT = 0x01
HALOGEN = 0x02


class F000LevelControlCluster(NoManufacturerCluster, LevelControl):
"""LevelControlCluster that reports to attrid 0xF000."""

server_commands = LevelControl.server_commands.copy()
server_commands[TUYA_CUSTOM_LEVEL_COMMAND] = foundation.ZCLCommandDef(
"moveToLevelTuya",
(TuyaLevelPayload,),
is_manufacturer_specific=False,
)

attributes = LevelControl.attributes.copy()
attributes.update(
{
# 0xF000
TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t),
# 0xFC02
TUYA_BULB_TYPE_ATTRIBUTE: ("bulb_type", TuyaBulbType),
# 0xFC03
TUYA_MIN_LEVEL_ATTRIBUTE: ("manufacturer_min_level", t.uint16_t),
# 0xFC04
TUYA_MAX_LEVEL_ATTRIBUTE: ("manufacturer_max_level", t.uint16_t),
}
)

# 0xF000 reported values are 10-1000, convert to 0-254
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conversion is 1-255 (which I think is the range that HA handles).

And the values 10 & 1000 I think correspond to the manufacturer_min_level and manufacturer_max_level values.
If someone modifies the values that conversion can do strange things.
The truth is that I don't know what could be the best way to handle this situation, but it is probably something quite common in dimmers that would be addressed in a generic way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've experimented with this quite a while and this is what worked best for me. Would you agree to merging this as is (because it seems to work with the default manufacturer_min_level and manufacturer_max_level) and if someone in the future comes in and finds a better approach to calculate the values we just update the formula? At least this PR supports the device in the first place 😊

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. We can go ahead with the version as is.

def _update_attribute(self, attrid, value):
if attrid == TUYA_LEVEL_ATTRIBUTE:
self.debug(
"Getting brightness %s",
value,
)
value = (value + 4 - 10) * 254 // (1000 - 10)
attrid = 0x0000

super()._update_attribute(attrid, value)

async def command(
self,
command_id: Union[foundation.Command, 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,
):
"""Override the default Cluster command."""
self.debug(
"Sending Cluster Command. Cluster Command is %x, Arguments are %s",
command_id,
args,
)
# move_to_level, move, move_to_level_with_on_off
if command_id in (0x0000, 0x0001, 0x0004):
# convert dim values to 10-1000
brightness = args[0] * (1000 - 10) // 254 + 10
self.debug(
"Setting brightness to %s",
brightness,
)
return await super().command(
TUYA_CUSTOM_LEVEL_COMMAND,
TuyaLevelPayload(level=brightness, transtime=0),
manufacturer=manufacturer,
expect_reply=expect_reply,
tsn=tsn,
)
Comment on lines +112 to +118
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine to me, but probably you can get the same result without extending the NoManufacturerCluster and putting here:

manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm that doesn't work because then this Class would also show up as cluster in the dimmers of other devices. F000LevelControlCluster has to inherit in some way from CustomCluster and NoManufacturerCluster does that for us (including setting manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID,).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this Class would also show up as cluster in the dimmers of other devices

That sentence not make sense to me. A quirk shouldn't modify other devices that don't use the quirk.
Trying to figure out how can be done I believe that some code like this could make the described behaviour:

    # attributes = LevelControl.attributes.copy()
    attributes.update(
        {
            # 0xF000
            TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t),
        }
    )

If not making a copy of parent class attributes, maybe the code is modifying the parent attributes and changes for any device that uses the parent class 🤔

In any case, your proposed code is fine to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah I don’t know, but it did show up as level control cluster on this new dimmer I bought…


return super().command(command_id, *args, manufacturer, expect_reply, tsn)


class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch):
"""Tuya Dimmer Switch Module With Neutral 1 Gang."""

signature = {
MODELS_INFO: [("_TZ3210_ngqk6jia", "TS110E")],
ENDPOINTS: {
# <SimpleDescriptor endpoint=1 profile=260 device_type=257
# input_clusters=[0, 4, 5, 6, 8, 57345]
# output_clusters=[10, 25]>
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
OnOff.cluster_id,
LevelControl.cluster_id,
TuyaZBExternalSwitchTypeCluster.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242: {
# <SimpleDescriptor endpoint=242 profile=41440 device_type=97
# input_clusters=[]
# output_clusters=[33]
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
OnOff.cluster_id,
F000LevelControlCluster,
TuyaZBExternalSwitchTypeCluster,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242: {
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}