Lampie: Notifications on Multiple Switches for Home Assistant

Is the configuration value stored elsewhere? If possible, I think it’d be nice to try to read it from wherever it’s already stored to avoid adding complexity to the setup. (Especially since the goal is to support several switch types.) Since this is an integration, we have access to far more of the internal state of things.

Can you find, for instance, where it’s stored in /config/.storage/core.config_entries?

So is this always the same value that @zanix is getting with:

{% set identifiers = device_attr(repeat.item, "identifiers") | first %}
{% set device_identifier = identifiers[1].split('_')[1] %}

And if it’s not the same, which is the one that are used in events that get published (that we consume for config double tap, for instance)?

It’s internal to the MQTT entities. The way that Home Assistant and Z2M talk to each other, Z2M publishes MQTT discovery packets which tell Home Assistant what topics to use for each entity.

You’ll have to reach into the MQTT integration to grab that. I implemented this in a different integration here. Actually now that I think about it, it should probably handle the device_identifier part as well, since you’ll be able to grab that path from inside the MQTT entities.

Ok, that seems worth using here.

We’re going for:

f"zigbee2mqtt/{device_id}/set"

The method you referenced is pulling out the base path from the first of subscriptions (by removing the last path component). I don’t have a working knowledge of MQTT terms, so I don’t fully know from reading that code what to expect.

Does MQTT guarantee that each “subscription” topic would be in the form:

f"/arbitrary/path/to/z2m/{device_id}/set

With the final path component (an action or whatever) always being there?

I wrote it almost 2 years ago, so not sure I remember 100% what the internals in Home Assistant’s MQTT integration are doing. We’ll probably need to debug it and tweak from there, but I think that’s about right.

Does MQTT guarantee that each “subscription” topic would be in the form

Yes, I think so.

They are not the same, rohan’s will output the name of the switch

{{ device_attr('light.kitchen', 'name') }}

Kitchen

My code outputs the Z2M identifier

{{ device_attr('light.kitchen', 'identifiers') }}

[
  [
    "mqtt",
    "zigbee2mqtt_0x9400000000000066"
  ]
]

Then I split the second value to get the ID.
You can publish to either the name of the device or the ID to MQTT.

zigbee2mqtt/{device_id}/set
zigbee2mqtt/{name}/set

Using the name from HA will work most of the time. The reason I changed to use the identifier is because you can have a different name in HA and it if it doesn’t match the name in Z2M then the path with the name will not work.

How I came across this issue is because one of the people that used my blueprint had one name set in HA as “Living Room West Light” and a path name set in Z2M as “living_room/lights/west_light” so Z2M would expect the publish path would be zigbee2mqtt/living_room/lights/west_light/set but the publish path with the identifier still worked.

However, if you can get the command_topic from the MQTT config of the device then none of this matters as you will have the correct path anyway.

Wow, thanks for the very clear explanation.

And how are those used for subscriptions (i.e. mqtt.async_subscribe)? I think Z2M makes these into triggers for users, but I believe the payloads are the same.

I’d guess these were the device ID and not the device name, but when checking if a message is relevant, I indexed & lookup both values for right now just to be safe.

As far as I can tell, Z2M only publishes to the name path, so zigbee2mqtt/{name} but it will read commands from zigbee2mqtt/{device_id}/set

Here is what the path set by Z2M looks like in MQTT Explorer

And when my blueprint sets an LED effect to the Kitchen switch

That’s what I’m suggesting with my code example to reach into the MQTT integration internals.

1 Like

Since these start out as disabled entities, I wonder if we can have Lampie either automatically enable them programmatically , or grab their MQTT path info and subscribe to them itself.

Either probably works, unless they disable the entities after the fact and wonder why the integration broke. Maybe only using MQTT is the safer path?

1 Like

That would be a hilarious bug report.

I think I agree that reading from the MQTT topic for those entities directly makes more sense.

Oh, wait—when @zanix said they were disabled, I didn’t realize that they were disabled by default. The local protection & 2x tap to dismiss notifications are disabled by default?

If so, I’d rather go the route of querying MQTT.

I haven’t done this before. Reading the docs, it looks like you publish a message and then (presumably) a previously configured subscriber gets the response. Is there any way to set that up to read a single value elegantly? And also, does that read from some cache or is it a round-trip to the switch over Zigbee?

Just double checked my install. Local protection is enabled, 2x tap to disable notification is disabled by default.

Yeah - you’ll need something similar to the code example I linked earlier. Let me see if I can get you one. Basically as part of the MQTT discovery, Zigbee2MQTT tells Home Assistant the exact part of the topic to read from to subscribe and populate each entity from. So by subscribing to that same topic, you’ll be notified of updates to it just like Home Assistant itself will (for the disabled entity).

There’s no round trip to the switch unless you ask Z2M to refresh it (You could do it once at setup time, but once you’ve added the subscription, you’ll be notified if that value changes).

@wbyoung here’s the promised code examples for you:

from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er

from homeassistant.components.mqtt.models import MqttData
from homeassistant.util.hass_dict import HassKey

MQTT_DATA: HassKey[MqttData] = HassKey(MQTT_DOMAIN)

def get_entities(entity: str, hass: HomeAssistant):
    device_registry = dr.async_get(hass)
    entity_registry = er.async_get(hass)

    device_id = entity_registry.async_get('REPLACE_ME_WITH_LIGHT_ENTITY_ID').device_id

    all_entities_for_switch = er.async_entries_for_device(entity_registry, device_id)
    double_tap_clear_entity = find_entity(all_entities_for_switch, "DoubleTapClearNotifications")
    local_protection_entity = find_entity(all_entities_for_switch, "LocalProtection")


    hass.data[MQTT_DATA].debug_info_entities.get(double_tap_clear_entity)['discovery_data']['discovery_payload']
    hass.data[MQTT_DATA].debug_info_entities.get(local_protection_entity)['discovery_data']['discovery_payload']

    
def find_entity(entities: list[er.RegistryEntry], original_name: str) -> str:
    for entity in entities:
        if entity.original_name == original_name:
            return entity.entity_id

If you replace REPLACE_ME_WITH_LIGHT_ENTITY_ID with the entity id of the light entity or fan entity coming from Z2M for example: light.office_lights or fan.office_fan (which will be user provided when they select the switch they want to use), the rest of the code will use that to look up the device id, from there get all of the entities that correspond for it and then find the double_tap_clear and local_protection entities. Next, it goes digging inside the MQTT data to get the discovery payload for those entities. This will look like this:

{'availability': [{...}, {...}], 'availability_mode': 'all', 'command_topic': 'zigbee2mqtt/Office Lights/set/doubleTapClearNotifications', 'device': {'configuration_url': 'https://zigbee2mqtt/#/device/0xd44867fffe899d68/info', 'hw_version': 0, 'identifiers': [...], 'manufacturer': 'Inovelli', 'model': 'mmWave Zigbee Dimmer', 'model_id': 'VZM32-SN', 'sw_version': '0.08', 'via_device': 'zigbee2mqtt_bridge_0x00124b002590967b', 'name': 'Office Lights'}, 'entity_category': 'config', 'object_id': 'office_lights_doubleTapClearNotifications', 'options': ['Enabled (Default)', 'Disabled'], 'origin': {'sw_version': '2.5.0', 'name': 'Zigbee2MQTT', 'support_url': 'https://www.zigbee2mqtt.io'}, 'state_topic': 'zigbee2mqtt/Office Lights', 'unique_id': '0xd44867fffe899d68_doubleTapClearNotifications_zigbee2mqtt', 'value_template': '{{ value_json.doubleTapClearNotifications }}', 'name': 'DoubleTapClearNotifications'}

The relevant part for your use case is the state_topic and the value_template. In the example I’ve provided above this looks like:

'state_topic' = 'zigbee2mqtt/Office Lights'
'value_template' = '{{ value_json.doubleTapClearNotifications }}'

You would therefore be able to subscribe to the state topic and then set up a callback for any responses on that topic to parse the template to get the value.

I like this approach because you are not dependent on the user’s naming of any of the entities (except the light/fan entity itself which they will provide your during the entity setup). It does utilize quite a bit of internals from the MQTT integration but it’s a pattern I’ve used in other integrations successfully before.

Oh and one other thing, you’ll probably need to add this block to the beginning of your integration setup code so it waits until MQTT is enabled and connected before continuing on (if it needs MQTT of course):

# Make sure MQTT integration is enabled and the client is available
if not await mqtt.async_wait_for_mqtt_client(hass):
    LOGGER.error("MQTT integration is not available")
    return False
hass.data.setdefault(DOMAIN, {})
return True

Oops. I don’t know think I was clear enough before. I was concerned about the entities themselves being disabled (in HA’s entity registry) rather than the state of them. If the entities are enabled by default (in HA’s entity registry), then querying HA for the state may be a little simpler (and more consistent with the ZHA implementation) than querying MQTT.

The code example you gave is excellent, though. Would this work for the light entity as well?

hass.data[MQTT_DATA].debug_info_entities.get(light_entity)['discovery_data']['discovery_payload']['state_topic']

Rather than looking at the first subscription?

Correct, my answer was whether the entities themselves were enabled, not the state of them.

Yes, that would work for the light entity as well. I didn’t realize you needed to subscribe to updates about it as well.

Ok, in that case I think it does make sense to just query MQTT. I guess it could be assumed that if the entity was disabled by default and is still disabled, that this is a setting that would be the default. But it’s not much more work to just get the real value from MQTT. (Technically, this is a problem with ZHA, too since users could disable the entities.)

So here’s some pseudo code for what I’ll be trying to do:

for each switch configured by the user:
    # is this even necessary for any reason now?
    # won't the topic of these always match that of the light entity?
    find the local protection entity
    find the 2x tap to dismiss entity

    full_topic = hass.data[MQTT_DATA].debug_info_entities.get(light_entity)['discovery_data']['discovery_payload']['state_topic']
    base_topic = full_topic without the last path component
    subscribe to "{base_topic}/+" if not already subscribed from another switch
    publish to "{full_topic}/get" with the following payload:
        # is it valid to combine these get requests into one?
        {
            "localProtection": "",
            "doubleTapClearNotifications": "",
        }

on message received callback:
    if localProtection in value:
        store the value
    if doubleTapClearNotifications in value:
        store the value
    if action in value:
        process the action if it is a 2x tap on the config

Alright, I implemented that way of handling things and released a new alpha. Anyone with an actual Zigbee2MQTT setup (and perhaps coding experience), feel free to try it out.

All of my previous requests and comments still apply to this alpha.

@rohan added some functionality to MQTT to support receiving messages when a notification is complete, so I’ve updated the alpha to integrate these changes.

I’m still not sure how far it gets since I don’t have Z2M running, but anyone who’s watching this thread who wants to give it a go, feel free to do so and report back. Hopefully @jncasey will be able to check this out in the next few weeks and push it over the finish line.

All of my previous requests and comments still apply to this alpha.

1 Like

This topic was automatically closed 67 days after the last reply. New replies are no longer allowed.