Skip to content

Duplicate MQTT event handlers accumulate on integration reload, causing multiple service calls per button press#174

Open
Marco1971Repo wants to merge 1 commit into
HASwitchPlate:mainfrom
Marco1971Repo:main
Open

Duplicate MQTT event handlers accumulate on integration reload, causing multiple service calls per button press#174
Marco1971Repo wants to merge 1 commit into
HASwitchPlate:mainfrom
Marco1971Repo:main

Conversation

@Marco1971Repo

Copy link
Copy Markdown

I am not very experienced with Python or Home Assistant internals, and I found this fix with the help of AI. It seems to solve the problem on my setup. I had previously mentioned this issue in the openHASP firmware discussions (#986) but received no response. I hope this can be of some help to the project.

Bug: Duplicate MQTT event handlers accumulate on integration reload, causing multiple service calls per button press

Summary

When the openHASP integration is reloaded in Home Assistant (without a full restart), MQTT event handlers for plate objects are registered multiple times. As a result, a single button press on the display triggers the associated action N times, where N equals the number of times the integration has been reloaded since the last full HA restart.

Environment

  • Home Assistant with openHASP custom component (latest version)
  • openHASP plate configured via openhasp.yaml
  • Plate remains powered and connected to MQTT broker during HA reload

Steps to Reproduce

  1. Start Home Assistant with openHASP integration and at least one plate configured with button event handlers in openhasp.yaml
  2. Perform a full restart of Home Assistant
  3. Press a button on the display → action is triggered once
  4. Make any change to a YAML file and perform a reload of the openHASP integration (without a full restart)
  5. Press the same button again → action is triggered twice
  6. Reload the integration again
  7. Press the button → action is triggered three times

The number of duplicate invocations increases by one with each reload.

Root Cause

The bug is in async_added_to_hass() inside the SwitchPlate class, specifically in the lwt_message_received callback (__init__.py).

When the integration is reloaded:

  1. async_will_remove_from_hass() is called on the old SwitchPlate instance, which correctly unsubscribes all plate-level MQTT topics (/LWT, /state/page, etc.)
  2. A new SwitchPlate instance is created with fresh _objects, each with empty _subscriptions = []
  3. async_added_to_hass() is called on the new instance, which re-subscribes to /LWT

The problem arises at step 3: the physical plate never went offline during the HA reload, so the MQTT broker still holds the retained online LWT message. The moment the new instance subscribes to the /LWT topic, the broker immediately replays the retained message, triggering lwt_message_received with HASP_ONLINE.

This causes enable_object() to be called on every object, registering a new MQTT subscription for each one.

However, on the previous full restart cycle, enable_object() was already called (also triggered by the retained LWT message), and those subscriptions were never cleaned up — because async_will_remove_from_hass() only unsubscribes the plate-level topics stored in self._subscriptions, not the per-object subscriptions stored in each HASPObject._subscriptions.

The per-object subscriptions are only cleaned up when the plate goes offline (the else branch of lwt_message_received), which never happens during a simple reload.

Relevant code (before fix)

# In lwt_message_received (async_added_to_hass):
if message == HASP_ONLINE:
    self._available = True
    self.hass.bus.async_fire(
        EVENT_HASP_PLATE_ONLINE,
        {CONF_PLATE: self._entry.data[CONF_HWID]},
    )
    if self._pages_jsonl:
        await self.async_load_page(self._pages_jsonl)
    else:
        await self.refresh()

    for obj in self._objects:
        await obj.enable_object()  # ← registers new subscriptions
                                   #   without removing existing ones

Note that disable_object() already exists and works correctly — it is called in the HASP_OFFLINE branch and in async_will_remove_from_hass(). It was simply not being called before enable_object() in the online branch.

Fix

Call disable_object() on all objects before calling enable_object() when a HASP_ONLINE LWT message is received. This guarantees that any previously registered subscriptions are always removed before new ones are added, making the operation idempotent regardless of how many times the integration is reloaded.

Since disable_object() clears _subscriptions = [] after unsubscribing, calling it on a freshly created object (with no existing subscriptions) is completely safe and has no side effects.

Fix applied to __init__.py

if message == HASP_ONLINE:
    self._available = True
    self.hass.bus.async_fire(
        EVENT_HASP_PLATE_ONLINE,
        {CONF_PLATE: self._entry.data[CONF_HWID]},
    )
    # Ensure any existing subscriptions are removed before
    # re-registering, to avoid duplicate event handlers after
    # a HA reload while the plate remains online (retained LWT
    # message triggers this handler again on re-subscription).
    for obj in self._objects:
        await obj.disable_object()

    if self._pages_jsonl:
        await self.async_load_page(self._pages_jsonl)
    else:
        await self.refresh()

    for obj in self._objects:
        await obj.enable_object()

Impact

  • Before fix: every integration reload adds one extra invocation per button press event. After N reloads, every button press triggers the associated action N+1 times.
  • After fix: button press events are always handled exactly once, regardless of how many times the integration has been reloaded.

Testing

  1. Apply the fix to custom_components/openhasp/__init__.py
  2. Perform a full HA restart
  3. Press a button on the display → action triggers once ✅
  4. Reload the openHASP integration
  5. Press the button → action still triggers once ✅
  6. Reload multiple times → behavior remains consistent ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants