[ZHA] Implement Bind/Unbind services (was: Thermostat remote temperature)

EDIT: If the bind/unbind services would be implemented then we could send the required bind requests through the developer tools with less limitations than the current bind/unbind functionnality.

The original goal, in summary, I would like a Thermostat use the temperature sent by a separate sensor which seems to require binding and RemoteSensing configuration.
The RemoteSensing configuration could be set using the developer tools, but the binding is not currently available.

The binding of a Temperature Measurement In cluster to a Temperature Mesurement Out cluster of compatible devices should be proposed in the UI.

Discussion

As far as I understand it a HVAC device such as a Thermostat can receive the temperature from a remote sensors if attribute 0x201/0x001a RemoteSensing is correctly set.

I believe that the remote sensor has to be bound to the thermostat. TI’s sample implementation show that it accepts incoming temperature reports and uses them as the “LocalTemperature” in zclSampleThermostat_ProcessInReportCmd .
And I believe that this is possible if the Thermostat has a “TemperatureMeasurement” output cluster.

I am trying to set this up in Home Assistant, but I think it all boils down to zigpy functionnality. I’ve been able to bind a remote control to a light bulb (through a group) for instance, but I can’t bind the temperature sensor to the Thermostat.

I have a Thermostat that has the appropriate output cluster:

{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=0, maximum_buffer_size=80, maximum_incoming_transfer_size=160, server_mask=10752, maximum_outgoing_transfer_size=160, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0301",
      "in_clusters": [
        "0x0000",
        "0x0001",
        "0x0003",
        "0x0201"
      ],
      "out_clusters": [
        "0x0003",
        "0x0402"
      ]
    }
  },
  "manufacturer": "Private",
  "model": "Private",
  "class": "zigpy.device.Device"
}

and a temperature sensor with input cluster:

{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=0, maximum_buffer_size=80, maximum_incoming_transfer_size=160, server_mask=0, maximum_outgoing_transfer_size=160, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0302",
      "in_clusters": [
        "0x0000",
        "0x0001",
        "0x0003",
        "0x0402",
        "0x0405"
      ],
      "out_clusters": [
        "0x0003"
      ]
    }
  },
  "manufacturer": "eWeLink",
  "model": "TH01",
  "class": "zigpy.device.Device"
}

But only the coordinator shows up in the bindable devices and the Temperature Measurement cluster does not appear amongst the bindable clusters.

The use for this is when the Thermostat mounted on the valve is measuring a temperature that is too high because the radiator is in some kind of enclosure with the thermostatic valve, or - in my case - when the Thermostat is between the radiator and the wall with a gap between the radiator and the wall that is less than 10 cm with limited air flow due to a library cabinet.

So how to set this up using zigpy & Home Assistant?

Have you tried Generic Thermostat where you spesify your temperature source and switch

and The Climate integration that allows you to control and monitor HVAC (heating, ventilating, and air conditioning) devices and thermostats.

Well, this is a thermostatic head
image
that mounts on a thermostatic valve:
image

and it works autonomously by measuring temperature internally.

I used the “Generic Thermostat” for another use case: some electrical heaters with a local temperature setting using a slider that I evolved by adding a 16A relay ahead of them, mounting a Zigbee thermometer in every piece, and having HA ensure the thermostat function.

However the thermostatic head is not made for external control, but allows “Remote Sensing” if the temperature sensor can be bound to the thermostatic head and we enable Remote Sensing on the head.

The extra advantage is that this continues to work as long as the thermostatic head head and thermometer communicate with each other without the need of HA itself (once it is configured).

The generic thermostat stops working when HA fails for some reason - I’ll then have to switch on the relays manually and revert to the old regulation of the heaters. Also when the generic thermostat is no longer there and the heaters were already on, I’ll get overheating because I put the level of the heaters to the maximum to heat as fast as possible (otherwise the heater turns off too soon because the local temperature is higher than the target temperature a few meters away).

With some help, I started updating core on https://github.com/mdeweerd/core/tree/addTemperatureMeasurementBinding .

I also learnt about a cool way to update the zha component on my live installation: set up the zha directory in custom_components. This allows me to continue to use associated devices in my live setup.

I am not there yet, but it’s coming.

It appears that the binding logic in HA/ZHA is wrong - the IN and OUT clusters were inverted needs improvement. Currently only OUT clusters can be bound to IN clusters which is usefull when they send commands, but there is also the case where IN clusters actually send reports to the OUT clusters that receive them and binding has to be done from IN (source) to OUT (target). Generally this is the case for sensors. The Thermostat cluster receiving these reports can use them as local or outdoor temperature, occupancy, and even humidity.

After modifying the core ZHA module, I was able to trigger binding from a reporting cluster to client cluster, and while the HA log indicates the expected “Bound” message, no packet is sent over the air.

I hope to find what is blocking but there are several layers to understand well enough to achieve the end goal.

api.py of the zha integration has the following

SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind"
SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind"

but these services do not appear in the services.yaml for the zha integration.

It seems that we would be able to send “custom” (un)bind commands if these services were fully implemented - which would solve the initial objective.