Skip to content

Conversation

candeodevelopment
Copy link
Contributor

Proposed change

Adds support for the following Candeo devices:

C-ZB-LC20-Dim / C-ZB-LC20-DIM
C-ZB-LC20-CCT
C-ZB-LC20-RGB
C-ZB-LC20-RGBW
C-ZB-LC20-RGBCCT

C-ZB-RD1
C-ZB-RD1P-DIM
C-ZB-RD1P-REM
C-ZB-RD1P-DPM

Additional information

@TheJulianJES would you mind giving us some pointers should there be any need for unit tests for these?

Checklist

  • The changes are tested and work correctly
  • pre-commit checks pass / the code has been formatted using Black
  • Tests have been added to verify that the new code works

candeodevelopment and others added 2 commits August 5, 2025 10:55
…ry dimmer switches

Adds support for the following Candeo devices:

C-ZB-LC20-Dim / C-ZB-LC20-DIM
C-ZB-LC20-CCT
C-ZB-LC20-RGB
C-ZB-LC20-RGBW
C-ZB-LC20-RGBCCT

C-ZB-RD1
C-ZB-RD1P-DIM
C-ZB-RD1P-REM
C-ZB-RD1P-DPM

@TheJulianJES would you mind giving us some pointers should there be any need for unit tests for these?
Copy link

codecov bot commented Aug 5, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.16%. Comparing base (ad0dc81) to head (d400eab).
⚠️ Report is 31 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #4234      +/-   ##
==========================================
+ Coverage   92.05%   92.16%   +0.10%     
==========================================
  Files         350      360      +10     
  Lines       11602    11966     +364     
==========================================
+ Hits        10680    11028     +348     
- Misses        922      938      +16     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@puddly
Copy link
Contributor

puddly commented Aug 5, 2025

You can remove direction= from all of the command definitions, it's not needed.

@candeodevelopment
Copy link
Contributor Author

@puddly I thought direction was a required field, If I remove it then I get an error:

2025-08-06 09:54:53.961 DEBUG (bellows.thread_0) [bellows.ash] Received data 3747b1a97d312a15b658924a27ab1593499c03e767c1a99874fec7438af8127d
2025-08-06 09:54:53.969 DEBUG (bellows.thread_0) [bellows.ash] Received data 387e
2025-08-06 09:54:53.970 DEBUG (bellows.thread_0) [bellows.ash] Received frame DataFrame(frm_num=3, re_tx=0, ack_num=7, ezsp_frame=b'\x05\x90\x01E\x00\x00\x04\x01\x06\x00\x02\x01@\x01\x00\x00M\xc0\xcc,g\xff\xff\x03\x01 \x03\x04')
2025-08-06 09:54:53.971 DEBUG (bellows.thread_0) [bellows.ash] Sending frame AckFrame(res=0, ncp_ready=0, ack_num=4) + FLAG
2025-08-06 09:54:53.971 DEBUG (bellows.thread_0) [bellows.ash] Sending data 8430fc7e
2025-08-06 09:54:53.979 DEBUG (MainThread) [bellows.ezsp.protocol] Received command incomingMessageHandler: {'type': <EmberIncomingMessageType.INCOMING_UNICAST: 0>, 'apsFrame': EmberApsFrame(profileId=260, clusterId=6, sourceEndpoint=2, destinationEndpoint=1, options=<EmberApsOption.APS_OPTION_RETRY|APS_OPTION_ENABLE_ROUTE_DISCOVERY: 320>, groupId=0, sequence=77), 'lastHopLqi': 192, 'lastHopRssi': -52, 'sender': 0x672C, 'bindingIndex': 255, 'addressIndex': 255, 'messageContents': b'\x01 \x03'}
2025-08-06 09:54:53.980 DEBUG (MainThread) [bellows.ezsp.protocol] Frame contains trailing data: b'\x04'
2025-08-06 09:54:53.981 DEBUG (MainThread) [bellows.zigbee.application] Received incomingMessageHandler frame with [<EmberIncomingMessageType.INCOMING_UNICAST: 0>, EmberApsFrame(profileId=260, clusterId=6, sourceEndpoint=2, destinationEndpoint=1, options=<EmberApsOption.APS_OPTION_RETRY|APS_OPTION_ENABLE_ROUTE_DISCOVERY: 320>, groupId=0, sequence=77), 192, -52, 0x672C, 255, 255, b'\x01 \x03']
2025-08-06 09:54:53.983 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2025, 8, 6, 8, 54, 53, 983650, tzinfo=datetime.timezone.utc), priority=0, src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x672C), src_ep=2, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=77, profile_id=260, cluster_id=6, data=Serialized[b'\x01 \x03'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=192, rssi=-52)
2025-08-06 09:54:53.991 DEBUG (MainThread) [zigpy.zcl] [0x672C:2:0x0006] Received ZCL frame: b'\x01 \x03'
2025-08-06 09:54:53.997 DEBUG (MainThread) [zigpy.zcl] [0x672C:2:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x01>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=32, command_id=3, *direction=<Direction.Client_to_Server: 0>)
2025-08-06 09:54:54.005 DEBUG (MainThread) [zigpy.device] [0x672c] Failed to parse packet ZigbeePacket(timestamp=datetime.datetime(2025, 8, 6, 8, 54, 53, 983650, tzinfo=datetime.timezone.utc), priority=0, src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x672C), src_ep=2, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=77, profile_id=260, cluster_id=6, data=Serialized[b'\x01 \x03'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=192, rssi=-52)
Traceback (most recent call last):
File "/usr/local/lib/python3.13/site-packages/zigpy/device.py", line 488, in packet_received
hdr, args = endpoint.deserialize(packet.cluster_id, data)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/zigpy/endpoint.py", line 228, in deserialize
return cluster.deserialize(data)
~~~~~~~~~~~~~~~~~~~^^^^^^
File "/usr/local/lib/python3.13/site-packages/zigpy/zcl/init.py", line 292, in deserialize
hdr.frame_control = hdr.frame_control.replace(direction=command.direction)
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/zigpy/types/struct.py", line 344, in replace
instance = type(self)(**d)
File "/usr/local/lib/python3.13/site-packages/zigpy/types/struct.py", line 511, in new
underlying_int, rest = cls._int_type.deserialize(temp_instance.serialize())
~~~~~~~~~~~~~~~~~~~~~~~^^
File "/usr/local/lib/python3.13/site-packages/zigpy/types/struct.py", line 236, in serialize
for field, value in self.assigned_fields(strict=True):
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/zigpy/types/struct.py", line 182, in assigned_fields
raise ValueError(
f"Value for field {field.name!r} is required: {self!r}"
)
ValueError: Value for field 'direction' is required: FrameControl<0x00>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False)

The above exception was the direct cause of the following exception:

zigpy.exceptions.ParsingError

Apologies if I've misunderstood what you meant!

@puddly
Copy link
Contributor

puddly commented Aug 6, 2025

Make sure you're using the latest version of zigpy and all of the other packages.

@candeodevelopment
Copy link
Contributor Author

Make sure you're using the latest version of zigpy and all of the other packages.

Ah right, we're just on the latest HA release (2025.7.4), we'll switch to the beta channel and recheck.

@candeodevelopment
Copy link
Contributor Author

@puddly we've updated the code, please let us know if anything further is required.

Thanks for your help!

@TheJulianJES TheJulianJES changed the title Add support for Candeo LC20 smart LED controllers and RD1 / RD1P rotary dimmer switches Add Candeo LC20 smart LED controllers, RD1/RD1P rotary dimmers Aug 11, 2025
@TheJulianJES TheJulianJES added needs review This PR should be reviewed soon, as it generally looks good. needs reviewer answer An answer from a reviewer is needed (e.g. why a PR isn't acceptable in the current state). labels Aug 11, 2025
Use actual data value for device automation triggers rather than enum.

Using an enum throws an "unsupported schema data type" error in HA itself when creating an automation.
@TheJulianJES TheJulianJES added manufacturer This request was made by the device's manufacturer smash This PR is close to be merged soon labels Sep 2, 2025
@dhc25
Copy link
Contributor

dhc25 commented Sep 19, 2025

Hi @TheJulianJES - will this merge make into the next release? I see the smash label there for a couple of weeks - I'm just double checking that there's nothing more you need from us? Thanks

Copy link
Collaborator

@TheJulianJES TheJulianJES left a comment

Choose a reason for hiding this comment

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

I'll come back to this shortly.
Except for the English strings in the device triggers for the dimmer switches, everything looks good.

Since the smart LED controller quirks are also in this PR, I can't just merge that part, unfortunately. It's always nicer to have separate PRs if possible.

)
.device_automation_triggers(
{
("Pressed", "Rotary knob"): {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'll come back to you on this, but I believe we should not have English strings in here. Instead, we should use translation keys (with the translations in ZHA).

Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @TheJulianJES - is this feedback confirmed?
Waiting for your feedback so we can do what's required (if anything). - Thanks

Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar to other device triggers, use something like that here (e.g. see const.py and other quirks). We'll then use that string as the translation key in HA's ZHA integration with a translation there.

So, we'll need to add the trigger strings to HA's ZHA integration here:
https://github.com/home-assistant/core/blob/00955b8e6af26d5a73386a2773a69486e50154dd/homeassistant/components/zha/strings.json#L269-L294

I should be able to do that in the ZHA bump if you provide me with the translations we want to go with.

@dhc25
Copy link
Contributor

dhc25 commented Sep 25, 2025

Thanks @TheJulianJES - what is the update on this please?

It's always nicer to have separate PRs if possible.

Yes - we'd like to do that, and we do still have several products to submit. We batched a few together this time, just due to the time it takes to get things through.

If we can get this through as soon as possible, we'll look to do the PRs product-by-product going forwards. Thanks

@TheJulianJES TheJulianJES added the priority: medium This should be addressed or looked at soon label Oct 8, 2025
@TheJulianJES TheJulianJES self-assigned this Oct 8, 2025
@candeodevelopment
Copy link
Contributor Author

@TheJulianJES we've updated the code to replace the hard coded strings.

We used existing consts where available, but think we'd need the following created on the HA's ZHA integration to match up with the actual device events:

"trigger_type": {
    "rotary_knob_started_rotating": "Rotary knob started rotating \"{subtype}\"",
    "rotary_knob_continued_rotating": "Rotary knob continued rotating \"{subtype}\"",
    "rotary_knob_stopped_rotating": "\"{subtype}\" stopped rotating",
}

"trigger_subtype": {
    "rotary_knob": "Rotary knob"
}

Hope that makes sense?

Please let us know if there's anything else that we need to do.

Thanks for your help!

@TheJulianJES
Copy link
Collaborator

That looks good!

Comment on lines +22 to +33
COMMAND_PRESS = "press"
COMMAND_DOUBLE_PRESS = "double_press"
COMMAND_HOLD = "hold"
COMMAND_RELEASE = "release"
COMMAND_STARTED_ROTATING = "started_rotating"
COMMAND_CONTINUED_ROTATING = "continued_rotating"
COMMAND_STOPPED_ROTATING = "stopped_rotating"

ROTARY_KNOB = "rotary_knob"
STARTED_ROTATING = "rotary_knob_started_rotating"
CONTINUED_ROTATING = "rotary_knob_continued_rotating"
STOPPED_ROTATING = "rotary_knob_stopped_rotating"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we just move all of them into the zhaquirks const.py file? We're gonna add translations for this in ZHA either way, so that way, other quirks can also use them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I don't see why not, as long as they don't conflict with or overwrite anything that's already defined?

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

Labels

manufacturer This request was made by the device's manufacturer needs review This PR should be reviewed soon, as it generally looks good. needs reviewer answer An answer from a reviewer is needed (e.g. why a PR isn't acceptable in the current state). priority: medium This should be addressed or looked at soon smash This PR is close to be merged soon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants