zha-device-handlers
zha-device-handlers copied to clipboard
Add NodOn SIN-4-FP-21 quirk
Proposed change
This PR adds a quirk for SIN-4-FP-21 (PilotWire). The device reports itself as a SmartPlug but it has special manufacturer-specific cluster which allows setting the following modes:
Off = 0x00
Comfort = 0x01
Eco = 0x02
FrostProtection = 0x03
ComfortMinus1 = 0x04
ComfortMinus2 = 0x05
Additional information
The result look like this in Home Assitant:
Checklist
- [x] The changes are tested and work correctly
- [x]
pre-commitchecks pass / the code has been formatted using Black - [x] Tests have been added to verify that the new code works
Codecov Report
All modified and coverable lines are covered by tests :white_check_mark:
Project coverage is 89.74%. Comparing base (
a5a00d9) to head (c0552b9). Report is 2 commits behind head on dev.
Additional details and impacted files
@@ Coverage Diff @@
## dev #3364 +/- ##
==========================================
+ Coverage 89.72% 89.74% +0.02%
==========================================
Files 319 320 +1
Lines 10348 10370 +22
==========================================
+ Hits 9285 9307 +22
Misses 1063 1063
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
@TheJulianJES I've re-pushed commits to use the v2 interface of QuirkBuilder().
Unfortunately, I didn't find an example of how to write tests for it. The tests that I wrote for the previous version didn't work due to compatibility issues, I believe.
If you think tests are necessary here, please give me an example of any test that uses the v2 interface.
Any news on this? I would love this to be merged, I myself own a bunch of those modules.
Please validate. I need this quirk as well
@TheJulianJES, I have a question. I didn't find a better place to ask it.
There is a slight problem with this quirk. An entity added to HA gets the '_none' suffix (see attached picture). I would expect it to get the '_pilot_wire_mode' suffix. Of course, one can rename the HA entity, but getting it right from the start is better.
I did a lot of debugging on my side but got lost, to be honest. I see that the ZHA objects receive the correct _unique_id_suffix value (which was my initial suspicion). But HA's entity still has '_none'.
This problem is not only with an enum but also with a configuration number. I'm preparing a PR to allow configuring impulse mode on NodOn switches (similar to this). My entity also has the _none suffix.
I'd appreciate your help or guidance.
P.S. With no success, I've tried setting translation_key and fallback_name attributes in quirks.
The none suffix is caused by the fact that HA doesn't have a value for the translation key. In future versions, HA will fall back to `fallback_name.
When your PR is merged, the quirks bump merged to ZHA, the ZHA bump merged to HA, we'd need to add the translation key to HA then.
The
nonesuffix is caused by the fact that HA doesn't have a value for the translation key. In future versions, HA will fall back to `fallback_name.When your PR is merged, the quirks bump merged to ZHA, the ZHA bump merged to HA, we'd need to add the translation key to HA then.
Thank you for the answer. Can you point out where I should add it in HA? I want to try it locally. I will change HA's code in my installation.
Hi @ikruglov thank you so much for this merge request. I'm waiting for it !
@TheJulianJES do you have any idea where this translation key is located, so that @ikruglov could test his code ?
Hi @ikruglov,
Tried to use the quirk, but got this in my home-assistant journal:
nov. 08 18:02:05 <host> hass[370001]: 2024-11-08 18:02:05.296 ERROR (SyncWorker_5) [zhaquirks] Unexpected exception importing custom quirk 'nodon.pilot_wire'
nov. 08 18:02:05 <host> hass[370001]: Traceback (most recent call last):
nov. 08 18:02:05 <host> hass[370001]: File "/var/lib/hass/.venv/lib/python3.12/site-packages/zhaquirks/__init__.py", line 479, in setup
nov. 08 18:02:05 <host> hass[370001]: spec.loader.exec_module(module)
nov. 08 18:02:05 <host> hass[370001]: File "<frozen importlib._bootstrap_external>", line 995, in exec_module
nov. 08 18:02:05 <host> hass[370001]: File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
nov. 08 18:02:05 <host> hass[370001]: File "/var/lib/private/hass/zhaquirks/nodon/pilot_wire.py", line 5, in <module>
nov. 08 18:02:05 <host> hass[370001]: from zhaquirks.nodon import (
nov. 08 18:02:05 <host> hass[370001]: ImportError: cannot import name 'NodOnPilotWireCluster' from 'zhaquirks.nodon' (/var/lib/hass/.venv/lib/python3.12/site-packages/zhaquirks/nodon/__init__.py)
Any idea what I might be doing wrong?
Hi @ikruglov, Tried to use the quirk, but got this in my home-assistant journal:
nov. 08 18:02:05 <host> hass[370001]: 2024-11-08 18:02:05.296 ERROR (SyncWorker_5) [zhaquirks] Unexpected exception importing custom quirk 'nodon.pilot_wire' nov. 08 18:02:05 <host> hass[370001]: Traceback (most recent call last): nov. 08 18:02:05 <host> hass[370001]: File "/var/lib/hass/.venv/lib/python3.12/site-packages/zhaquirks/__init__.py", line 479, in setup nov. 08 18:02:05 <host> hass[370001]: spec.loader.exec_module(module) nov. 08 18:02:05 <host> hass[370001]: File "<frozen importlib._bootstrap_external>", line 995, in exec_module nov. 08 18:02:05 <host> hass[370001]: File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed nov. 08 18:02:05 <host> hass[370001]: File "/var/lib/private/hass/zhaquirks/nodon/pilot_wire.py", line 5, in <module> nov. 08 18:02:05 <host> hass[370001]: from zhaquirks.nodon import ( nov. 08 18:02:05 <host> hass[370001]: ImportError: cannot import name 'NodOnPilotWireCluster' from 'zhaquirks.nodon' (/var/lib/hass/.venv/lib/python3.12/site-packages/zhaquirks/nodon/__init__.py)Any idea what I might be doing wrong?
Try this :
"""NODON module for custom device handlers."""
from zigpy.quirks import CustomCluster
from zigpy.quirks.v2 import EntityType, QuirkBuilder
import zigpy.types as t
from zigpy.zcl.foundation import (
BaseAttributeDefs,
BaseCommandDefs,
DataTypeId,
Direction,
ZCLAttributeDef,
ZCLCommandDef,
)
class NodOnPilotWireMode(t.enum8):
"""Pilot wire mode."""
OFF = 0x00
COMFORT = 0x01
ECO = 0x02
FROST_PROTECTION = 0x03
COMFORT_MINUS_1 = 0x04
COMFORT_MINUS_2 = 0x05
NODON = "NodOn"
NODON_PILOT_WIRE_CLUSTER_ID = 0xFC00 # 64512
class NodOnPilotWireCluster(CustomCluster):
"""NodOn manufacturer specific cluster to set Pilot Wire mode."""
name: str = "NodOnPilotWireCluster"
cluster_id: t.uint16_t = NODON_PILOT_WIRE_CLUSTER_ID
ep_attribute: str = "nodon_pilot_wire_cluster"
class AttributeDefs(BaseAttributeDefs):
"""Attribute definitions."""
pilot_wire_mode = ZCLAttributeDef(
id=0x0000,
type=NodOnPilotWireMode,
zcl_type=DataTypeId.uint8,
is_manufacturer_specific=True,
)
class ServerCommandDefs(BaseCommandDefs):
"""Server command definitions."""
set_pilot_wire_mode = ZCLCommandDef(
id=0x00,
schema={"mode": NodOnPilotWireMode},
direction=Direction.Client_to_Server,
is_manufacturer_specific=True,
)
(
QuirkBuilder(NODON, "SIN-4-FP-21")
.replaces(NodOnPilotWireCluster)
.enum(
attribute_name=NodOnPilotWireCluster.AttributeDefs.pilot_wire_mode.name,
enum_class=NodOnPilotWireMode,
cluster_id=NodOnPilotWireCluster.cluster_id,
entity_type=EntityType.STANDARD,
translation_key="pilot_wire",
fallback_name="Pilot Wire",
)
.add_to_registry()
)
@Mookunicorn this time, I get no error in the logs, but my SIN-4-FP-21 module still shows a on/off switch, no modes alternative.
@Mookunicorn Naming the python file __init__.py seems to have corrected the issue, thanks!
But it looks like my heater does not support other modes, or the module fails to interpret the commands sent :(
Hi @ikruglov, Tried to use the quirk, but got this in my home-assistant journal:
nov. 08 18:02:05 <host> hass[370001]: 2024-11-08 18:02:05.296 ERROR (SyncWorker_5) [zhaquirks] Unexpected exception importing custom quirk 'nodon.pilot_wire'nov. 08 18:02:05 <host> hass[370001]: Traceback (most recent call last):nov. 08 18:02:05 <host> hass[370001]: File "/var/lib/hass/.venv/lib/python3.12/site-packages/zhaquirks/__init__.py", line 479, in setupnov. 08 18:02:05 <host> hass[370001]: spec.loader.exec_module(module)nov. 08 18:02:05 <host> hass[370001]: File "<frozen importlib._bootstrap_external>", line 995, in exec_modulenov. 08 18:02:05 <host> hass[370001]: File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removednov. 08 18:02:05 <host> hass[370001]: File "/var/lib/private/hass/zhaquirks/nodon/pilot_wire.py", line 5, in <module>nov. 08 18:02:05 <host> hass[370001]: from zhaquirks.nodon import (nov. 08 18:02:05 <host> hass[370001]: ImportError: cannot import name 'NodOnPilotWireCluster' from 'zhaquirks.nodon' (/var/lib/hass/.venv/lib/python3.12/site-packages/zhaquirks/nodon/__init__.py)Any idea what I might be doing wrong?
Make sure that you got file names right. pilot_wire.py can't import classes from init.py (note there is double underscore on each size of 'init').
Hello @ikruglov, After a thorough reading on the heater I'm using, it actually supports 6 modes of pilot wire, so every mode should work... But I only get Comfort/Frost protection, though HA shows all the modes... Don't know what is messing up with the mode... I believe the quirk as you provide it should be placed in /var/lib/hass/.venv/lib/python3.12/site-packages/zhaquirks/nodon/ (on my machine), right? Because placing it in my custom_quirks_path produces the error I shown earlier...
@ikruglov In fact, placing the quirk in the correct path made everything work as expected. I'm now a happy 6 modes pilot wire user :)
Hello, I tried this https://github.com/zigpy/zha-device-handlers/pull/3364#issuecomment-2465425127 with a small customization for the Adeo device
QuirkBuilder(NODON, "SIN-4-FP-21")
+ .also_applies_to("Adeo","SIN-4-FP-21_EQU")
.replaces(NodOnPilotWireCluster)
Modes are showing up in the device, and react correctly when toggling the main switch (on => comfort & off=> frost protection)
But I get an error when trying to set a mode directly (comfort & frost protection included)
home-assistant | File "/usr/src/homeassistant/homeassistant/components/select/__init__.py", line 188, in async_handle_select_option
home-assistant | await self.async_select_option(option)
home-assistant | File "/usr/src/homeassistant/homeassistant/components/zha/helpers.py", line 1288, in handler
home-assistant | raise HomeAssistantError(err) from err
home-assistant | homeassistant.exceptions.HomeAssistantError: Failed to write attribute pilot_wire_mode=<NodOnPilotWireMode.COMFORT: 1>: <Status.UNSUPPORTED_ATTRIBUTE: 134>
Running HA 2024.10.2, and I used "Reconfigure" on the device instead of removing / enrolling again as the nodes are already on the wall and I'd prefer not to take the heater off again ;) I have some modules not mounted so I will probably test with a fresh enrolling to see if it makes any difference.
Hello, I tried this #3364 (comment) with a small customization for the Adeo device
QuirkBuilder(NODON, "SIN-4-FP-21") + .also_applies_to("Adeo","SIN-4-FP-21_EQU") .replaces(NodOnPilotWireCluster)Modes are showing up in the device, and react correctly when toggling the main switch (on => comfort & off=> frost protection)
But I get an error when trying to set a mode directly (comfort & frost protection included)
home-assistant | File "/usr/src/homeassistant/homeassistant/components/select/__init__.py", line 188, in async_handle_select_option home-assistant | await self.async_select_option(option) home-assistant | File "/usr/src/homeassistant/homeassistant/components/zha/helpers.py", line 1288, in handler home-assistant | raise HomeAssistantError(err) from err home-assistant | homeassistant.exceptions.HomeAssistantError: Failed to write attribute pilot_wire_mode=<NodOnPilotWireMode.COMFORT: 1>: <Status.UNSUPPORTED_ATTRIBUTE: 134>Running HA 2024.10.2, and I used "Reconfigure" on the device instead of removing / enrolling again as the nodes are already on the wall and I'd prefer not to take the heater off again ;) I have some modules not mounted so I will probably test with a fresh enrolling to see if it makes any difference.
Hi. Based on the error' Status. UNSUPPORTED_ATTRIBUTE: 134`, it seems that Adeo has different codes or attributes. I can't comment here or test because I don't have an Adeo device, only NodOn. But I don't think that "Reconfiguring" or re-enroling will help. One needs to find the correct attributes.
Hi. Based on the error' Status. UNSUPPORTED_ATTRIBUTE: 134`, it seems that Adeo has different codes or attributes. I can't comment here or test because I don't have an Adeo device, only NodOn. But I don't think that "Reconfiguring" or re-enroling will help. One needs to find the correct attributes.
I got it working "manually" using a manufacturer_id_override as seen here https://github.com/zigpy/zha-device-handlers/issues/2777#issuecomment-2358979497 Is it possible to use the field without overriding write_attributes / read_attributes ?
SIN-4-FP-21_EQU
I have a possible solution that should satisfy both NodOn and Adeo devices. But I can't test the latter, so I need to rely on you (or others) who have Adeo.
You can find the code for __init__.py and pilot_wire.py files below:
https://github.com/ikruglov/zha-device-handlers/blob/adeo-pilot-wire/zhaquirks/nodon/init.py
https://github.com/ikruglov/zha-device-handlers/blob/adeo-pilot-wire/zhaquirks/nodon/pilot_wire.py
Let me know if this works. If yes, I'll merge it with this PR.
SIN-4-FP-21_EQUI have a possible solution that should satisfy both NodOn and Adeo devices. But I can't test the latter, so I need to rely on you (or others) who have Adeo.
You can find the code for
__init__.pyandpilot_wire.pyfiles below: https://github.com/ikruglov/zha-device-handlers/blob/adeo-pilot-wire/zhaquirks/nodon/init.py https://github.com/ikruglov/zha-device-handlers/blob/adeo-pilot-wire/zhaquirks/nodon/pilot_wire.pyLet me know if this works. If yes, I'll merge it with this PR.
Same error as before homeassistant.exceptions.HomeAssistantError: Failed to write attribute pilot_wire_mode=<NodOnPilotWireMode.ComfortMinus1: 4>: <Status.UNSUPPORTED_ATTRIBUTE: 134>
Looking at the code in zigpy, it seems the override is only used if the manufacturer parameter is not set https://github.com/zigpy/zigpy/blob/d0fa17bfcb5bfbda47146ec7dd6ef49dbedc298b/zigpy/quirks/init.py#L249
The device also sends bad information for the manufacturer_id (4727 here, but it responds only with 4747), it may explain why the override is not used ?
scan:
ieee: 08:dd:eb:ff:fe:d7:fd:51
nwk: "0x784e"
model: SIN-4-FP-21_EQU
manufacturer: Adeo
manufacturer_id: "0x4727"
I'm willing to debug this but python is not my strong suit, is it possible to edit the files in /usr/local/lib/python3.12/site-packages/ directly to dump some variable values in the HA logs ? If so, how ?
Hi, Made kind of mix from this thread and #2777 and seems to work without the <Status.UNSUPPORTED_ATTRIBUTE: 134>. Here is my code, hope this will help :
#20241116 - Quirk zha
#Adeo/NodOn with correction after Python/Quirk regression
from typing import Any, Final
from zigpy.quirks import CustomCluster
from zigpy.quirks.v2 import QuirkBuilder, EntityType, EntityPlatform
from zigpy.zcl.foundation import (
BaseAttributeDefs,
BaseCommandDefs,
ZCLAttributeDef,
ZCLCommandDef,
WriteAttributesStatusRecord,
Status
)
from zigpy.zcl import ClusterType
import zigpy.types as t
import logging
_LOGGER = logging.getLogger(__name__)
class NodonPilotWireMode(t.enum16):
OFF = 0x00
COMFORT = 0x01
ECO = 0x02
FROST_PROTECTION = 0x03
COMFORT_MINUS_1 = 0x04
COMFORT_MINUS_2 = 0x05
NODON_PILOT_WIRE_CLUSTER_ID = 0xfc00 #64512
NODON = "NodOn"
ADEO = "Adeo"
class NodonPilotWireCluster(CustomCluster):
"""NodOn pilot wire manufacturer-specific cluster."""
cluster_id: Final = NODON_PILOT_WIRE_CLUSTER_ID
name: Final = "Pilot Wire"
ep_attribute: Final = "pilot_wire_cluster"
manufacturer_id_override: Final = 4747
class AttributeDefs(BaseAttributeDefs):
pilot_wire_mode: Final = ZCLAttributeDef(
id=0x0000,
type=NodonPilotWireMode,
access="r"
)
class ClientCommandDefs(BaseCommandDefs):
setMode: Final = ZCLCommandDef(
id=0x0000,
schema={"mode": NodonPilotWireMode},
direction=True,
is_manufacturer_specific=True
)
async def write_attributes(
self, attributes: dict[str | int, Any], manufacturer: int | None = None
) -> list:
"""Override writes to the pilot_wire_mode attribute."""
if "pilot_wire_mode" in attributes:
await self.client_command(0, attributes.get("pilot_wire_mode"), manufacturer=4747)
await self.read_attributes(attributes)
return [[WriteAttributesStatusRecord(Status.SUCCESS)]]
return await super().write_attributes(attributes, manufacturer=4747)
async def read_attributes(
self,
attributes: list[int | str],
allow_cache: bool = False,
only_cache: bool = False,
manufacturer: int | t.uint16_t | None = None,
) -> Any:
return await super().read_attributes(attributes, allow_cache=False, only_cache=False, manufacturer=4747)
(
QuirkBuilder(ADEO, "SIN-4-FP-21_EQU")
.also_applies_to(NODON,"SIN-4-FP-21")
.replaces(NodonPilotWireCluster)
.enum(
attribute_name="pilot_wire_mode",
enum_class=NodonPilotWireMode,
cluster_id=NODON_PILOT_WIRE_CLUSTER_ID,
entity_type=EntityType.STANDARD,
translation_key="pilot_wire",
fallback_name="Pilot Wire",
)
.add_to_registry()
)
I'm willing to debug this but python is not my strong suit, is it possible to edit the files in
/usr/local/lib/python3.12/site-packages/directly to dump some variable values in the HA logs ? If so, how ?
Thanks for the info. I usually mount specific files directly from the host system to freely change them in a comfortable environment. But you can also try modifing files directly inside the container. Just restart HA after each change. It should work.
Made kind of mix from this thread and #2777 and seems to work without the <Status.UNSUPPORTED_ATTRIBUTE: 134>. Here is my code, hope this will help :
This works because you explicitly specify manufacturer=4747 for each send and receive command. I'm pretty sure that the entire write_attributes() and read_attributes() code is unnecessary as long as we find a way to make zigpy to pass manufacturer=4747 for Adeo and don't pass it for NodOn (or pass a NodOn-specific one). I hoped to override it in the class (via manufacturer_id_override), but it was not enough.
The device also sends bad information for the manufacturer_id (4727 here, but it responds only with 4747), it may explain why the override is not used ?
scan: ieee: 08:dd:eb:ff:fe:d7:fd:51 nwk: "0x784e" model: SIN-4-FP-21_EQU manufacturer: Adeo manufacturer_id: "0x4727"
@papylhomme, so you say that Adeo device has manufacturer_id: "0x4727" but only replies to commands and setting/getting attributes when manufacturer_id=0x4747?
Also, can you show what's the output of this page for Adeo?
The device also sends bad information for the manufacturer_id (4727 here, but it responds only with 4747), it may explain why the override is not used ?
scan: ieee: 08:dd:eb:ff:fe:d7:fd:51 nwk: "0x784e" model: SIN-4-FP-21_EQU manufacturer: Adeo manufacturer_id: "0x4727"@papylhomme, so you say that Adeo device has
manufacturer_id: "0x4727"but only replies to commands and setting/getting attributes whenmanufacturer_id=0x4747?
Yes exactly
{
"node_descriptor": {
"logical_type": 1,
"complex_descriptor_available": 0,
"user_descriptor_available": 0,
"reserved": 0,
"aps_flags": 0,
"frequency_band": 8,
"mac_capability_flags": 142,
"manufacturer_code": 4727,
"maximum_buffer_size": 82,
"maximum_incoming_transfer_size": 500,
"server_mask": 11264,
"maximum_outgoing_transfer_size": 500,
"descriptor_capability_field": 0
},
I played with the code, and the manufacturer_id_override is not used where we want. It is used in the Device class when present, but the CustomCluster class reads directly from the value provided in the endpoint, so I end up using 4727 when sending zigbee messages
played with the code, and the
manufacturer_id_overrideis not used where we want it to be. It is used in the Device class when present, but the CustomCluster class reads directly from the value provided in the endpoint, so I end up using 4727 when sending zigbee messages
Okay. This is what I suspected. We can override node_descriptor completely and set NodOn's 0x4747 manufacturer_code there. QuirkBuilder luckely allows this via .node_descriptor(). I'll push a patch later today when I'm done with work.
@papylhomme, I've pushed the updated fix. Try it out: https://github.com/ikruglov/zha-device-handlers/blob/adeo-pilot-wire/zhaquirks/nodon/pilot_wire.py
Note two things:
- There is no longer
__init__.py. All Pilot Wire-related code is inpilot_wire.py.I will also update this PR later so it has all the code in a single file, but after settling on the Adeo code. - I'm using features that were added relatively recently. So, you need the most up-to-date version of HA. I'm running 2024.11.2
Updated: If this doesn't work, then I see two ways out:
- Fallback to the suggested code where
read/write_attributesandcommandsare overwritten in the cluster class to passmanufacturer_id.In this case, I believe this should be a separate quirk (and PR) because this code is unnecessary for NodOn. - Patch zigpy (quite possibly, it's a bug), so
manufacturer_id_overrideis used. I'm still deciding whether to invest time in this.
@papylhomme, I've pushed the updated fix. Try it out: https://github.com/ikruglov/zha-device-handlers/blob/adeo-pilot-wire/zhaquirks/nodon/pilot_wire.py
Note two things:
- There is no longer
__init__.py. All Pilot Wire-related code is inpilot_wire.py.I will also update this PR later so it has all the code in a single file, but after settling on the Adeo code.- I'm using features that were added relatively recently. So, you need the most up-to-date version of HA. I'm running 2024.11.2
Just tested it quickly and still doesn't work :-/ I'll try to debug this further when I have some time, maybe this evening, definitely in the week. I'm running 2024.11.2 too, could you point to the new feature you are using in zigpy/zha, it would help the process.
Updated: If this doesn't work, then I see two ways out:
- Fallback to the suggested code where
read/write_attributesandcommandsare overwritten in the cluster class to passmanufacturer_id.In this case, I believe this should be a separate quirk (and PR) because this code is unnecessary for NodOn.
:+1: Agree for the separate PR, overriding the internal methods will be a mess to maintain
- Patch zigpy (quite possibly, it's a bug), so
manufacturer_id_overrideis used. I'm still deciding whether to invest time in this.
I will open an issue there and see what they think about it.
could you point to the new feature you are using in zigpy/zha, it would help the process.
clone() method in QuirkBuilder
