Changing power source from 'mains' to 'battery' with a ZHA quirk

Hello. I’m trying to report the battery remaining value from an STM32WB55 microcontroller.
I am reporting the battery voltage and remaining percentage to the ‘PowerConfig’ cluster, but it’s not showing up in ZHA.
I assume it’s because the Zigbee stack from ST doesn’t seem to allow for changing the power source in the Basic cluster, so I am trying to see if it is possible to change the node descriptor via a ZHA quirk and not having much luck.

So far my quirk looks like this:

class STM32Sensor(CustomDevice):
    """STM32 sensor device."""

    def __init__(self, *args, **kwargs):
        """Init."""
        self.temperature_bus = Bus()
        super().__init__(*args, **kwargs)

    signature = {
        MODELS_INFO: [("stm32", "wb55")],
        NODE_DESCRIPTOR: STM32_MAINS_NODE_DESC,
        ENDPOINTS: {
            1: {
                PROFILE_ID: 260,
                DEVICE_TYPE: 0x000c,
                INPUT_CLUSTERS: [
                    #BasicCluster.cluster_id,
                    #Identify.cluster_id,
                    #TemperatureMeasurementCluster.cluster_id,
                    0x0000,
                    0x0001,
                    0x0402
                ],
                OUTPUT_CLUSTERS: [
                ],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [
                ],
                OUTPUT_CLUSTERS: [
                    #Green power
                    0x0021
                ],
            }
        },
    }

    replacement = {
        SKIP_CONFIGURATION: True,
        NODE_DESCRIPTOR: STM32_BATTERY_NODE_DESC,
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [
                    #BasicCluster.cluster_id,
                    #Identify.cluster_id,
                    #TemperatureMeasurementCluster.cluster_id,
                    0x0000,
                    0x0001,
                    0x0402
                ],
                OUTPUT_CLUSTERS: [
                ],
            },
            242: {
                #PROFILE_ID: 41440,
                #DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [
                ],
                OUTPUT_CLUSTERS: [
                    #Green power
                    0x0021
                ],
            }
        },
    }
STM32_MAINS_NODE_DESC = NodeDescriptor(
    complex_descriptor_available=1, 
    user_descriptor_available=1, 
    reserved=0, 
    aps_flags=0, 
    #mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, 
    mac_capability_flags=142,
    manufacturer_code=4311, 
    maximum_buffer_size=57, 
    maximum_incoming_transfer_size=2000, 
    server_mask=11264, 
    maximum_outgoing_transfer_size=128, 
)

STM32_BATTERY_NODE_DESC = NodeDescriptor(
    complex_descriptor_available=1, 
    user_descriptor_available=1, 
    reserved=0, 
    aps_flags=0, 
    #mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|FullFunctionDevice: 138>, 
    mac_capability_flags=138,
    manufacturer_code=4311, 
    maximum_buffer_size=57, 
    maximum_incoming_transfer_size=2000, 
    server_mask=11264, 
    maximum_outgoing_transfer_size=128, 
)

I would not blame the stack, but rather how the stack is used. Usually the power source depends on the HW.

Look at this quirck where I modify an existing quirk to stop the on/off attribute from being used in HA (it’s reporting incorrect values).

What is important is the local implementation to replace the OnOffCluster which is called LidlOnOffCluster, and then referencing it in the INPUT_CLUSTERS - where the class name is written in stead of the cluster_id.

In your case you want to change the behavior of the Basic Cluster, so you need to implement a class for that and reference it properly in the replacement.

I also recommand to use zha-toolkit and use it’s “scan_device” tool.

"""Quirk for LIDL RGB+CCT bulb."""
import logging

from zigpy.profiles import zha
from zigpy.quirks import CustomCluster, CustomDevice
from zigpy.types.named import Bool
from zigpy.zcl.clusters.general import (
    Basic,
    GreenPowerProxy,
    Groups,
    Identify,
    LevelControl,
    OnOff,
    Ota,
    Scenes,
    Time,
)
from zigpy.zcl.clusters.lighting import Color
from zigpy.zcl.clusters.lightlink import LightLink

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)

_LOGGER = logging.getLogger(__name__)


class LidlOnOffCluster(CustomCluster, OnOff):
    """Lidl OnOff custom cluster."""

    def _update_attribute(self, attrid, value):
        _LOGGER.debug(f"LIDL ATTR {attrid} {value!r} {value!=Bool.false}")
        if attrid != 0 or value != Bool.false:
            super()._update_attribute(attrid, value)
        # else ignore update


class LidlRGBCCTColorCluster(CustomCluster, Color):
    """Lidl RGB+CCT Lighting custom cluster."""

    # Set correct capabilities to ct, xy, hs
    # LIDL bulbs do not correctly report this attribute (comes back as None in Home Assistant)
    _CONSTANT_ATTRIBUTES = {0x400A: 0b11001}


class RGBCCTLight(CustomDevice):
    """Lidl RGB+CCT Lighting device."""

    signature = {
        MODELS_INFO: [("_TZ3000_dbou1ap4", "TS0505A")],
        ENDPOINTS: {
            1: {
                # <SimpleDescriptor endpoint=1 profile=269 device_type=268
                # device_version=1
                # input_clusters=[0, 3, 4, 5, 6, 8, 768, 4096]
                # output_clusters=[10, 25]
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    LevelControl.cluster_id,
                    Color.cluster_id,
                    LightLink.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
                # device_version=0
                # 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.EXTENDED_COLOR_LIGHT,
                #DEVICE_TYPE: zha.DeviceType.COLOR_TEMPERATURE_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    LidlOnOffCluster,
                    LevelControl.cluster_id,
                    LidlRGBCCTColorCluster,
                    # LightLink.cluster_id,  # Disabled because of "unsupported" replies
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }
1 Like

Yes it is possible that I am not using the stack properly. Here is the relevant post on the ST forum in case you are interested.

Thank you very much for the sample code. I managed to change the values in the PowerSource attribute for the Basic cluster, but the battery is still not showing up in ZHA. Maybe I am missing something else?

My wb55.py file is as follows:

"""STM32 weather sensor device."""
import logging

from zigpy.profiles import zha
from zigpy.zcl.clusters.general import Groups, Identify
from zigpy.zcl.clusters.measurement import PressureMeasurement
from zigpy.quirks import CustomCluster, CustomDevice
from zigpy.zcl.clusters.general import (
    Basic,
    GreenPowerProxy,
    Groups,
    Identify,
    LevelControl,
    OnOff,
    Ota,
    Scenes,
    Time,
)

from zhaquirks import Bus
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    NODE_DESCRIPTOR,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SKIP_CONFIGURATION,
)

TEMPERATURE_HUMIDITY_DEVICE_TYPE = 0x5F01
WB55_DEVICE_TYPE = 0x000c

_LOGGER = logging.getLogger(__name__)

class STM32BasicCluster(CustomCluster, Basic):
    """STM32 Basic custom cluster."""

    # Set correct capabilities for the power source
    # Possibly a bug in the STM32 zigbee stack, as it does not correctly report this attribute (comes back as None in Home Assistant)

    """ PowerSource Attribute: 0x0007
        Attribute Value b6-b0 Description
        0x00 Unknown
        0x01 Mains (single phase)
        0x02 Mains (3 phase)
        0x03 Battery
        0x04 DC source
        0x05 Emergency mains constantly powered
        0x06 Emergency mains and transfer switch
        Bit b7 of this attribute SHALL be set to 1 if the device has a secondary power source in the form of a battery
        backup. Otherwise, bit b7 SHALL be set to 0. 
    """
    _CONSTANT_ATTRIBUTES = {0x0007: 3} 


class STM32Sensor(CustomDevice):
    """STM32 weather sensor device."""

    def __init__(self, *args, **kwargs):
        """Init."""
        self.temperature_bus = Bus() 
        super().__init__(*args, **kwargs)

    signature = {
        MODELS_INFO: [("stm32", "wb55")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: 260,
                DEVICE_TYPE: 0x000c,
                INPUT_CLUSTERS: [
                    0x0000,
                    0x0001,
                    0x0402
                ],
                OUTPUT_CLUSTERS: [
                ],
            },
            2: {
                PROFILE_ID: 260,
                DEVICE_TYPE: 0x000c,
                INPUT_CLUSTERS: [
                    0x0000
                ],
                OUTPUT_CLUSTERS: [
                ],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [
                ],
                OUTPUT_CLUSTERS: [
                    0x0021
                ],
            }
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [
                    STM32BasicCluster,
                    0x0001,
                    0x0402
                ],
                OUTPUT_CLUSTERS: [
                ],
            },
            2: {
                PROFILE_ID: 260,
                DEVICE_TYPE: 0x000c,
                INPUT_CLUSTERS: [
                    STM32BasicCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [
                    0x0021
                ],
            }
        },
    }

Output from zha-toolkit scan device:

{
  "ieee": "00:80:e1:25:01:04:55:a6",
  "nwk": "0x49c1",
  "model": "wb55",
  "manufacturer": "stm32",
  "manufacturer_id": "0x4311",
  "endpoints": [
    {
      "id": 1,
      "device_type": "0x000c",
      "profile": "0x0104",
      "in_clusters": {
        "0x0000": {
          "cluster_id": "0x0000",
          "title": "STM32BasicCluster",
          "name": "basic",
          "attributes": {},
          "commands_received": {},
          "commands_generated": {}
        },
        "0x0001": {
          "cluster_id": "0x0001",
          "title": "Power Configuration",
          "name": "power",
          "attributes": {},
          "commands_received": {},
          "commands_generated": {}
        },
        "0x0402": {
          "cluster_id": "0x0402",
          "title": "Temperature Measurement",
          "name": "temperature",
          "attributes": {},
          "commands_received": {},
          "commands_generated": {}
        }
      },
      "out_clusters": {}
    },
    {
      "id": 2,
      "device_type": "0x000c",
      "profile": "0x0104",
      "in_clusters": {
        "0x0000": {
          "cluster_id": "0x0000",
          "title": "STM32BasicCluster",
          "name": "basic",
          "attributes": {},
          "commands_received": {},
          "commands_generated": {}
        }
      },
      "out_clusters": {}
    },
    {
      "id": 242,
      "device_type": "0x0061",
      "profile": "0xa1e0"
    }
  ]
}

Unfortunately, attribute discovery is not active on your STM32 device, so scan_device does not list the details of the available attributes.

Possibly your device never sent its battery percentage.

You may need to do a “Reconfigure device” to have HA rediscover that its battery powered and configure reporting at least. Maybe even remove the device from ZHA and pair again (I do not know all the internals of ZHA).

I did not dig into the STM32 solution yet, but soem interesting resources:

So I would guess you need to call ZbZclBasicServerConfigDefaults at some point with the proper data.
Supposing that it’s easy to find:

which I suppose are discussing the problem and the solution.

Ant there is also an example in the AN5498 application note: https://www.st.com/resource/en/application_note/an5498-how-to-use-zigbee-clusters-templates-on-stm32wb-series-stmicroelectronics.pdf#page=16

I think this might be the case. I see in the scans of my other zigbee devices is lists all the attributes and values.
When I try to scan the STM32 I see these errors in the ‘homeassistant.log’ file:

2022-09-04 23:08:43.308 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed 'discover_attributes_extended' starting 0x0001/0x0000. Error: [0x59ad:1:0x0001]: Message send failure
2022-09-04 23:08:45.720 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_GENERAL_COMMAND status for discover_commands starting 0
2022-09-04 23:08:46.651 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_GENERAL_COMMAND status for discover_attribute starting 0x0403/0x0000
2022-09-04 23:08:58.212 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed to discover 0x{0001} commands starting 0. Error: [0x59ad:1:0x0001]: Message send failure
2022-09-04 23:09:13.214 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed to discover commands starting 0. Error: [0x59ad:1:0x0001]: Message send failure
2022-09-04 23:09:17.804 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_attribute starting 0x0000/0x0000
2022-09-04 23:09:18.162 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:18.479 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:18.882 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_attribute starting 0x0006/0x0000
2022-09-04 23:09:19.328 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:19.620 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:19.916 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_attribute starting 0x0000/0x0000
2022-09-04 23:09:20.206 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_attribute starting 0x0000/0x0000
2022-09-04 23:09:20.505 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:20.809 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:21.133 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_attribute starting 0x0006/0x0000
2022-09-04 23:09:21.419 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_attribute starting 0x0006/0x0000
2022-09-04 23:09:21.728 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:22.034 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] got Status.UNSUP_CLUSTER_COMMAND status for discover_commands starting 0
2022-09-04 23:09:28.081 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed 'discover_attributes_extended' starting 0x0003/0x0000. Error: [0x59ad:1:0x0003]: Message send failure
2022-09-04 23:09:42.989 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed 'discover_attributes_extended' starting 0x0003/0x0000. Error: [0x59ad:1:0x0003]: Message send failure
2022-09-04 23:09:57.950 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed to discover 0x{0003} commands starting 0. Error: [0x59ad:1:0x0003]: Message send failure
2022-09-04 23:10:10.603 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed to discover 0x{0403} commands starting 0. Error: [0xb514:1:0x0403]: Message send failure
2022-09-04 23:10:12.901 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed to discover commands starting 0. Error: [0x59ad:1:0x0003]: Message send failure
2022-09-04 23:10:27.818 ERROR (MainThread) [custom_components.zha_toolkit.scan_device] Failed 'discover_attributes_extended' starting 0x0402/0x0000. Error: [0x59ad:1:0x0402]: Message send failure

I am sending the battery voltage and percentage to the PowerConfig cluster, and I can see these values in the cluster manager.

I couldn’t get ZbZclBasicServerConfigDefaults to work. But I did manage to get the PowerSource attribute working without a quirk with the following code using ZbZclBasicWriteDirect, however the Power source still shows as ‘Mains’ in the Node descriptor.

struct ZbZclBasicServerDefaults basic_config;
basic_config.power_source = ZCL_BASIC_POWER_BATTERY;

 enum ZclStatusCodeT status;
 status = ZbZclBasicWriteDirect(zigbee_app_info.zb, SW1_ENDPOINT, ZCL_BASIC_ATTR_POWER_SOURCE, &basic_config.power_source, 1);

My guess as to why ZHA doesn’t show the battery info is because it can’t discover the attributes when adding the device to the network? But I’m not sure how to enable attribute discovery on the STM32, so I guess I will have to do some more digging.

Cheers,

If HA shows that it is using mains then there is no reason for HA to show the battery level.

I would reconfigure your device or remove it from the network and add it again.

image

Regarding the Discover Attributes Extended functionnality, I was not able to find anything for STM32 at this time…

I was able to get the power source to show up as battery in HA.
I had to change the Node Descriptor ‘MAC Capabilities’ power source bit in the STM32 code.
Then removing and re-adding the device again.

When I add the device again, the battery percent shows up in the diagnostics but with an icon of an eye. Is there a way to make this show up as a battery icon by default? (like the aqara sensors)
Also the percentage remaining only refreshes when I manually read the attribute value from the Manage Cluster page.

Surely, one would need to look up what zha requires…

Obviously the value needs to be communicated to HA to show it. The ZHA code can probably help identify the reporting interval/polling interval that ZHA will try to set. In my experience ZHA only sets the reporing interval when the device is being (re)configured.
You could define a default reporting configuration in the device (as some kind of initial factory setting), where you could set it to report when the % changes and at least once every 24 hours.
Or you could configure the reporting interval using zha-toolkit for instance.
Or you could poll the battery using an automation.

If you can not change the firmware of the device then suggest also open a new issue for “Device support request” just for discussion here too as many ZHA/zigpy/quirk developers do not read forums → https://github.com/zigpy/zha-device-handlers/issues and https://github.com/zigpy/zha-device-handlers/discussions or if you are developing firmware for the device yourself then maybe instead try posting a question to zigpy discussion for advice on firmware Zigbee firmware configuration development → https://github.com/zigpy/zigpy/discussions