zigpy / zha-device-handlers

ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices.
Apache License 2.0
719 stars 664 forks source link

[Device Support Request] ACOVA Alcantara heater support in ZHA #2921

Open stadros83 opened 7 months ago

stadros83 commented 7 months ago

Problem description

Hello

I just bought a zigbee heater, the Acova Alcantara. I can pair it to ZHA and get entities but there is a "bug".

If I change the temperature using ZHA, the heater stop being "usable" in HA (while still in box mode - so connected to ZHA).

If I hit "heat" it goes to "Frost Free" (so out of box mode). So there is a problem of mode here, it should go (or stay) in "box mode".

If I change the temperature on the heater (while it is still in box mode), the locelace card come back by itself and I can then change the temperature again, but just for few secondes. It seems to be as soon as the heater goes to "heat_cool" (so still in box mode but not actually heating).

My heater can only be controlled when climate.NAME_thermostat is set with system_mode: "[<SystemMode.Emergency_Heating: 1>]/heat"

If I have this it is NOT working : system_mode: "[<SystemMode.Auto: 1>]/heat_cool"

I've added two videos.

I'm not the only one with this issue : https://community.home-assistant.io/t/acova-alcantara-radiator-with-install-code/469444

I'm not a developper but I can provide logs or whatever is needed :) !

Solution description

I would like to be able to change the temperature of the heater without losing connection to it.

Screenshots/Video

[Screenshots/Video] https://www.youtube.com/shorts/aE4h_aS2h_M https://youtu.be/ECUnDe4aqBM

Device signature

Device signature ```json { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", "endpoints": { "1": { "profile_id": "0x0104", "device_type": "0x0301", "input_clusters": [ "0x0000", "0x0003", "0x0201" ], "output_clusters": [ "0x0003" ] }, "2": { "profile_id": "0x0104", "device_type": "0x0107", "input_clusters": [ "0x0000", "0x0003", "0x0406" ], "output_clusters": [ "0x0003" ] }, "3": { "profile_id": "0x0104", "device_type": "0x0001", "input_clusters": [ "0x0000", "0x0003", "0x000f" ], "output_clusters": [ "0x0003" ] }, "242": { "profile_id": "0xa1e0", "device_type": "0x0061", "input_clusters": [], "output_clusters": [ "0x0021" ] } }, "manufacturer": "ZEHNDER GROUP VAUX ANDIGNY ", "model": "ALCANTARA2 D1.00P1.02Z1.00", "class": "zigpy.device.Device" } ```

Diagnostic information

Diagnostic information ```json { "home_assistant": { "installation_type": "Home Assistant OS", "version": "2024.1.3", "dev": false, "hassio": true, "virtualenv": false, "python_version": "3.11.6", "docker": true, "arch": "x86_64", "timezone": "Europe/Paris", "os_name": "Linux", "os_version": "6.1.71-haos", "supervisor": "2023.12.1", "host_os": "Home Assistant OS 11.4", "docker_version": "24.0.7", "chassis": "embedded", "run_as_root": true }, "custom_components": { "alarmo": { "version": "v1.9.13", "requirements": [] }, "sonos_cloud": { "version": "0.3.5", "requirements": [] }, "hacs": { "version": "1.33.0", "requirements": [ "aiogithubapi>=22.10.1" ] }, "frigate": { "version": "4.0.1", "requirements": [ "pytz==2022.7" ] }, "rtetempo": { "version": "1.3.2", "requirements": [ "requests-oauthlib>=1.3.1" ] } }, "integration_manifest": { "domain": "zha", "name": "Zigbee Home Automation", "after_dependencies": [ "onboarding", "usb" ], "codeowners": [ "@dmulcahey", "@adminiuga", "@puddly", "@TheJulianJES" ], "config_flow": true, "dependencies": [ "file_upload" ], "documentation": "https://www.home-assistant.io/integrations/zha", "iot_class": "local_polling", "loggers": [ "aiosqlite", "bellows", "crccheck", "pure_pcapy3", "zhaquirks", "zigpy", "zigpy_deconz", "zigpy_xbee", "zigpy_zigate", "zigpy_znp", "universal_silabs_flasher" ], "requirements": [ "bellows==0.37.6", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.109", "zigpy-deconz==0.22.4", "zigpy==0.60.4", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", "universal-silabs-flasher==0.0.15", "pyserial-asyncio-fast==0.11" ], "usb": [ { "vid": "10C4", "pid": "EA60", "description": "*2652*", "known_devices": [ "slae.sh cc2652rb stick" ] }, { "vid": "1A86", "pid": "55D4", "description": "*sonoff*plus*", "known_devices": [ "sonoff zigbee dongle plus v2" ] }, { "vid": "10C4", "pid": "EA60", "description": "*sonoff*plus*", "known_devices": [ "sonoff zigbee dongle plus" ] }, { "vid": "10C4", "pid": "EA60", "description": "*tubeszb*", "known_devices": [ "TubesZB Coordinator" ] }, { "vid": "1A86", "pid": "7523", "description": "*tubeszb*", "known_devices": [ "TubesZB Coordinator" ] }, { "vid": "1A86", "pid": "7523", "description": "*zigstar*", "known_devices": [ "ZigStar Coordinators" ] }, { "vid": "1CF1", "pid": "0030", "description": "*conbee*", "known_devices": [ "Conbee II" ] }, { "vid": "0403", "pid": "6015", "description": "*conbee*", "known_devices": [ "Conbee III" ] }, { "vid": "10C4", "pid": "8A2A", "description": "*zigbee*", "known_devices": [ "Nortek HUSBZB-1" ] }, { "vid": "0403", "pid": "6015", "description": "*zigate*", "known_devices": [ "ZiGate+" ] }, { "vid": "10C4", "pid": "EA60", "description": "*zigate*", "known_devices": [ "ZiGate" ] }, { "vid": "10C4", "pid": "8B34", "description": "*bv 2010/10*", "known_devices": [ "Bitron Video AV2010/10" ] } ], "zeroconf": [ { "type": "_esphomelib._tcp.local.", "name": "tube*" }, { "type": "_zigate-zigbee-gateway._tcp.local.", "name": "*zigate*" }, { "type": "_zigstar_gw._tcp.local.", "name": "*zigstar*" }, { "type": "_uzg-01._tcp.local.", "name": "uzg-01*" }, { "type": "_slzb-06._tcp.local.", "name": "slzb-06*" } ], "is_built_in": true }, "data": { "ieee": "**REDACTED**", "nwk": 54234, "manufacturer": "ZEHNDER GROUP VAUX ANDIGNY ", "model": "ALCANTARA2 D1.00P1.02Z1.00", "name": "ZEHNDER GROUP VAUX ANDIGNY ALCANTARA2 D1.00P1.02Z1.00", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "quirk_id": null, "manufacturer_code": 4098, "power_source": "Mains", "lqi": 47, "rssi": null, "last_seen": "2024-01-19T15:55:29", "available": true, "device_type": "Router", "signature": { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", "endpoints": { "1": { "profile_id": "0x0104", "device_type": "0x0301", "input_clusters": [ "0x0000", "0x0003", "0x0201" ], "output_clusters": [ "0x0003" ] }, "2": { "profile_id": "0x0104", "device_type": "0x0107", "input_clusters": [ "0x0000", "0x0003", "0x0406" ], "output_clusters": [ "0x0003" ] }, "3": { "profile_id": "0x0104", "device_type": "0x0001", "input_clusters": [ "0x0000", "0x0003", "0x000f" ], "output_clusters": [ "0x0003" ] }, "242": { "profile_id": "0xa1e0", "device_type": "0x0061", "input_clusters": [], "output_clusters": [ "0x0021" ] } }, "manufacturer": "ZEHNDER GROUP VAUX ANDIGNY ", "model": "ALCANTARA2 D1.00P1.02Z1.00" }, "active_coordinator": false, "entities": [ { "entity_id": "sensor.radiateur_bureau_action_cvc", "name": "ZEHNDER GROUP VAUX ANDIGNY ALCANTARA2 D1.00P1.02Z1.00" }, { "entity_id": "number.radiateur_bureau_decalage_de_temperature_locale", "name": "ZEHNDER GROUP VAUX ANDIGNY ALCANTARA2 D1.00P1.02Z1.00" }, { "entity_id": "binary_sensor.radiateur_bureau_entree_binaire", "name": "ZEHNDER GROUP VAUX ANDIGNY ALCANTARA2 D1.00P1.02Z1.00" }, { "entity_id": "button.radiateur_bureau_identifier", "name": "ZEHNDER GROUP VAUX ANDIGNY ALCANTARA2 D1.00P1.02Z1.00" }, { "entity_id": "binary_sensor.radiateur_bureau_occupation", "name": "ZEHNDER GROUP VAUX ANDIGNY ALCANTARA2 D1.00P1.02Z1.00" }, { "entity_id": "climate.radiateur_bureau_thermostat", "name": "ZEHNDER GROUP VAUX ANDIGNY ALCANTARA2 D1.00P1.02Z1.00" } ], "neighbors": [], "routes": [], "endpoint_names": [ { "name": "THERMOSTAT" }, { "name": "OCCUPANCY_SENSOR" }, { "name": "LEVEL_CONTROL_SWITCH" }, { "name": "PROXY_BASIC" } ], "user_given_name": "RADIATEUR_BUREAU", "device_reg_id": "913fa7524cd6f60befea07fbd54df1ba", "area_id": "bureau", "cluster_details": { "1": { "device_type": { "name": "THERMOSTAT", "id": 769 }, "profile_id": 260, "in_clusters": { "0x0000": { "endpoint_attribute": "basic", "attributes": { "0x0004": { "attribute_name": "manufacturer", "value": "ZEHNDER GROUP VAUX ANDIGNY " }, "0x0005": { "attribute_name": "model", "value": "ALCANTARA2 D1.00P1.02Z1.00" } }, "unsupported_attributes": {} }, "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} }, "0x0201": { "endpoint_attribute": "thermostat", "attributes": { "0x0004": { "attribute_name": "abs_max_heat_setpoint_limit", "value": 2800 }, "0x0003": { "attribute_name": "abs_min_heat_setpoint_limit", "value": 700 }, "0x001b": { "attribute_name": "ctrl_sequence_of_oper", "value": 2 }, "0x0000": { "attribute_name": "local_temperature", "value": 700 }, "0x0010": { "attribute_name": "local_temperature_calibration", "value": 0 }, "0x0002": { "attribute_name": "occupancy", "value": 1 }, "0x0011": { "attribute_name": "occupied_cooling_setpoint", "value": 2600 }, "0x0012": { "attribute_name": "occupied_heating_setpoint", "value": 700 }, "0x001e": { "attribute_name": "running_mode", "value": 4 }, "0x0029": { "attribute_name": "running_state", "value": 0 }, "0x001c": { "attribute_name": "system_mode", "value": 1 }, "0x0014": { "attribute_name": "unoccupied_heating_setpoint", "value": 800 } }, "unsupported_attributes": { "0x0015": { "attribute_name": "min_heat_setpoint_limit" }, "0x0018": { "attribute_name": "max_cool_setpoint_limit" }, "0x0007": { "attribute_name": "pi_cooling_demand" }, "0x0005": { "attribute_name": "abs_min_cool_setpoint_limit" }, "0x0006": { "attribute_name": "abs_max_cool_setpoint_limit" }, "0x0008": { "attribute_name": "pi_heating_demand" }, "0x0016": { "attribute_name": "max_heat_setpoint_limit" }, "0x0013": { "attribute_name": "unoccupied_cooling_setpoint" }, "0x0017": { "attribute_name": "min_cool_setpoint_limit" } } } }, "out_clusters": { "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} } } }, "2": { "device_type": { "name": "OCCUPANCY_SENSOR", "id": 263 }, "profile_id": 260, "in_clusters": { "0x0000": { "endpoint_attribute": "basic", "attributes": {}, "unsupported_attributes": {} }, "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} }, "0x0406": { "endpoint_attribute": "occupancy", "attributes": { "0x0000": { "attribute_name": "occupancy", "value": 1 } }, "unsupported_attributes": {} } }, "out_clusters": { "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} } } }, "3": { "device_type": { "name": "LEVEL_CONTROL_SWITCH", "id": 1 }, "profile_id": 260, "in_clusters": { "0x0000": { "endpoint_attribute": "basic", "attributes": {}, "unsupported_attributes": {} }, "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} }, "0x000f": { "endpoint_attribute": "binary_input", "attributes": { "0x0055": { "attribute_name": "present_value", "value": 0 }, "0x006f": { "attribute_name": "status_flags", "value": 0 } }, "unsupported_attributes": {} } }, "out_clusters": { "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} } } }, "242": { "device_type": { "name": "PROXY_BASIC", "id": 97 }, "profile_id": 41440, "in_clusters": {}, "out_clusters": { "0x0021": { "endpoint_attribute": "green_power", "attributes": {}, "unsupported_attributes": {} } } } } } } ```

Logs

Logs ```python [Paste the logs here] ```

Custom quirk

Custom quirk ```python [Paste your custom quirk here] ```

Additional information

No response

stadros83 commented 7 months ago

I've seen in the developpers tools, on climate.NAME_thermostat :

If I have this it is NOT working : system_mode: "[<SystemMode.Auto: 1>]/heat_cool"

If I have this it IS working : system_mode: "[<SystemMode.Emergency_Heating: 1>]/heat"

stadros83 commented 7 months ago

It seems that the problem is coming from SystemMode.AUTO. For HA Auto means that the heater is controled by a schedule, but for this manufacturer AUTO means that the heater is in box mode and can receive commands.

stadros83 commented 7 months ago

I want to add that I can control the heater from ZHA by changing the value of occupied_heating_setpoint.

Example :

stadros83 commented 7 months ago

No one :( ?

github-actions[bot] commented 1 month ago

There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.

gdoffe commented 4 days ago

It seems that the problem is coming from SystemMode.AUTO. For HA Auto means that the heater is controled by a schedule, but for this manufacturer AUTO means that the heater is in box mode and can receive commands.

I re-up this topic as I have a Taffetas 2 that seems to have a similar behavior.

I'm quite new in ZHA development and just discovered quirks. It seems to me that quirks are useful to fix raw data coming from devices according to their clusters.

However here it is more the functional behavior of the heater itself that is problematic. As you mention SystemMode.Auto means you can control the heater through Zigbee (heating only). Switching to SystemMode.Heat moves the heater into a Manual mode.

So the only two interesting system modes from ZHA point of view are:

So SystemMode.Auto is not corresponding to HVACMode.HEAT_COOL but to HVACMode.HEAT.

So ZHA conversion tables defined as constants in application/platforms/climate/const.py are not applicable here and must be redefined:

  HVAC_MODE_2_SYSTEM = {                                                          
      HVACMode.OFF: SystemMode.Off,                                               
      HVACMode.HEAT_COOL: SystemMode.Auto,                                        
      HVACMode.COOL: SystemMode.Cool,                                             
      HVACMode.HEAT: SystemMode.Heat,                                             
      HVACMode.FAN_ONLY: SystemMode.Fan_only,                                     
      HVACMode.DRY: SystemMode.Dry,                                               
  }                                                                               

  SYSTEM_MODE_2_HVAC = {                                                          
      SystemMode.Off: HVACMode.OFF,                                               
      SystemMode.Auto: HVACMode.HEAT_COOL,                                        
      SystemMode.Cool: HVACMode.COOL,                                             
      SystemMode.Heat: HVACMode.HEAT,                                             
      SystemMode.Emergency_Heating: HVACMode.HEAT,                                
      SystemMode.Pre_cooling: HVACMode.COOL,  # this is 'precooling'. is it the same?
      SystemMode.Fan_only: HVACMode.FAN_ONLY,                                     
      SystemMode.Dry: HVACMode.DRY,                                               
      SystemMode.Sleep: HVACMode.OFF,                                             
  } 

For me it seems more related to ZHA integration, so it could not be done inside a quirks, leading to the following patch on zha:

diff --git a/zha/application/platforms/climate/__init__.py b/zha/application/platforms/climate/__init__.py
index a297a43..37dfc64 100644
--- a/zha/application/platforms/climate/__init__.py
+++ b/zha/application/platforms/climate/__init__.py
@@ -581,6 +581,75 @@ class ZenWithinThermostat(Thermostat):
     """Zen Within Thermostat implementation."""

+@MULTI_MATCH(
+    cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
+    manufacturers={"ZEHNDER GROUP VAUX ANDIGNY      "},
+    stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
+)
+class ZehnderThermostat(Thermostat):
+    """Zehnder thermostat to adapt AUTO mode behavior."""
+    ZEHNDER_HVAC_MODE_2_SYSTEM = {
+        HVACMode.OFF: SystemMode.Off,
+        HVACMode.HEAT: SystemMode.Auto,
+    }
+
+    ZEHNDER_SYSTEM_MODE_2_HVAC = {
+        SystemMode.Off: HVACMode.OFF,
+        SystemMode.Auto: HVACMode.HEAT,
+        SystemMode.Heat: HVACMode.HEAT,
+    }
+
+    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
+        """Set new target operation mode."""
+        if hvac_mode not in self.hvac_modes:
+            self.warning(
+                "can't set '%s' mode. Supported modes are: %s",
+                hvac_mode,
+                self.hvac_modes,
+            )
+            return
+
+        if await self._thermostat_cluster_handler.async_set_operation_mode(
+            ZehnderThermostat.ZEHNDER_HVAC_MODE_2_SYSTEM[hvac_mode]
+        ):
+            self.maybe_emit_state_changed_event()
+
+    @property
+    def state(self) -> dict[str, Any]:
+        """Get the state of the lock."""
+        thermostat = self._thermostat_cluster_handler
+        system_mode = ZehnderThermostat.ZEHNDER_SYSTEM_MODE_2_HVAC.get(thermostat.system_mode, "unknown")
+
+        response = super().state
+        response["current_temperature"] = self.current_temperature
+        response["target_temperature"] = self.target_temperature
+        response["target_temperature_high"] = self.target_temperature_high
+        response["target_temperature_low"] = self.target_temperature_low
+        response["hvac_action"] = self.hvac_action
+        response["hvac_mode"] = self.hvac_mode
+        response["preset_mode"] = self.preset_mode
+        response["fan_mode"] = self.fan_mode
+
+        response[ATTR_SYS_MODE] = (
+            f"[{thermostat.system_mode}]/{system_mode}"
+            if self.hvac_mode is not None
+            else None
+        )
+        response[ATTR_OCCUPANCY] = thermostat.occupancy
+        response[ATTR_OCCP_COOL_SETPT] = thermostat.occupied_cooling_setpoint
+        response[ATTR_OCCP_HEAT_SETPT] = thermostat.occupied_heating_setpoint
+        response[ATTR_PI_HEATING_DEMAND] = thermostat.pi_heating_demand
+        response[ATTR_PI_COOLING_DEMAND] = thermostat.pi_cooling_demand
+        response[ATTR_UNOCCP_COOL_SETPT] = thermostat.unoccupied_cooling_setpoint
+        response[ATTR_UNOCCP_HEAT_SETPT] = thermostat.unoccupied_heating_setpoint
+        return response
+
+    @property
+    def hvac_mode(self) -> HVACMode | None:
+        """Return HVAC operation mode."""
+        return ZehnderThermostat.ZEHNDER_SYSTEM_MODE_2_HVAC.get(self._thermostat_cluster_handler.system_mode)
+
+
 @MULTI_MATCH(
     cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
     aux_cluster_handlers=CLUSTER_HANDLER_FAN,

As I am completly noob to this, does it seem correct ?

gdoffe commented 4 days ago

@stadros83 the issue has been automatically closed, maybe you could re-open it ?

stadros83 commented 4 days ago

Not sure it is possible to reopen (at least I probably don't have the rights to do that).

gdoffe commented 4 days ago

Thank you @dmulcahey !

@stadros83 could you test on your side ?

stadros83 commented 3 days ago

Test what exactly ?