Summary
When the bridge runs in ApiMode with ble_connection=on-demand, the MQTT toggleLid command can be routed through a direct ServiceMode handler instead of through ApiMode.
In the observed case this caused the command to fail completely: the lid did not move, because the direct handler tried to use a client that does not exist in on-demand mode.
The underlying issue is that ApiMode embeds ServiceMode, while ServiceMode also registers direct MQTT control handlers that assume an already available BLE client.
Observed behavior
Configuration:
[SERVICE]
mode=api
ble_connection=on-demand
Sending the MQTT toggleLid command produced:
Caught exception in on_message: 'NoneType' object has no attribute 'toggle_lid_position'
The toilet did not react to the command.
In on-demand BLE mode there is intentionally no permanently connected ServiceMode.client. However, the MQTT ToggleLidPosition event was still handled directly by ServiceMode, which attempted to execute:
self.client.toggle_lid_position()
with self.client being None.
As a result, the command was never sent to the AquaClean.
Expected behavior
When the application runs in ApiMode, MQTT commands should be routed through ApiMode so that the configured BLE connection lifecycle is respected.
For toggleLid, the expected flow is:
MQTT ToggleLidPosition
-> ApiMode MQTT handler
-> run_command("toggle-lid")
-> establish on-demand BLE connection
-> client.toggle_lid_position()
-> disconnect BLE
The MQTT topic and command semantics should remain unchanged.
Cause
ApiMode creates and embeds a ServiceMode instance.
Historically, ServiceMode registered its own direct MQTT handlers for control commands. That is appropriate when ServiceMode owns a persistent BLE client, but it is not safe when the same service object is embedded in ApiMode while ble_connection=on-demand.
In that configuration, direct ServiceMode handlers bypass the ApiMode methods responsible for establishing the temporary BLE connection.
For the observed toggleLid failure, this results in a call on None instead of creating an on-demand connection first.
Affected routing
The concrete failure reproduced here was ToggleLidPosition.
The same routing pattern also applies to other direct MQTT handlers that bypass ApiMode's connection lifecycle. The proposed fix therefore addresses the handler ownership rather than special-casing only toggleLid.
The investigation identified the same architectural issue for at least:
ToggleLidPosition
ResetFilterCounter
Connect
Only the toggleLid failure described above was directly observed as a user-facing failure in this report.
Proposed fix
Allow ServiceMode to disable registration of its direct MQTT control handlers when it is embedded by ApiMode.
ApiMode then registers the affected MQTT handlers itself and routes them through its existing connection-aware methods.
Conceptually:
Standalone ServiceMode
-> keeps existing direct MQTT handlers
ApiMode
-> creates ServiceMode without direct control handlers
-> registers MQTT handlers in ApiMode
-> routes commands through run_command() / do_connect()
-> respects persistent vs. on-demand BLE mode
For toggleLid, this changes the execution path from a direct call on ServiceMode.client to:
await self.run_command("toggle-lid")
which uses the correct on-demand connection lifecycle.
Compatibility
The proposed change is intended to preserve existing external behavior:
- no MQTT topic changes;
- no payload changes;
- no command semantic changes;
- standalone
ServiceMode keeps its existing direct-handler behavior;
- persistent BLE mode continues to use the existing persistent client path;
- only handler ownership/routing changes when
ServiceMode is embedded in ApiMode.
Regression coverage
Regression tests should verify that:
ApiMode creates ServiceMode with direct MQTT control-handler registration disabled;
ApiMode owns the affected MQTT handlers;
ToggleLidPosition is routed through ApiMode.run_command("toggle-lid");
ResetFilterCounter and Connect are routed through the corresponding ApiMode lifecycle-aware methods;
- standalone
ServiceMode retains its legacy direct-handler behavior.
Environment where reproduced
The failure was reproduced with:
- application mode:
ApiMode
- BLE connection mode:
on-demand
- MQTT enabled
- AquaClean Mera connected through the bridge's normal BLE transport
The problem is primarily a bridge-side routing/lifecycle issue and is not believed to depend on a specific AquaClean firmware behavior.
Summary
When the bridge runs in
ApiModewithble_connection=on-demand, the MQTTtoggleLidcommand can be routed through a directServiceModehandler instead of throughApiMode.In the observed case this caused the command to fail completely: the lid did not move, because the direct handler tried to use a client that does not exist in on-demand mode.
The underlying issue is that
ApiModeembedsServiceMode, whileServiceModealso registers direct MQTT control handlers that assume an already available BLE client.Observed behavior
Configuration:
Sending the MQTT
toggleLidcommand produced:The toilet did not react to the command.
In on-demand BLE mode there is intentionally no permanently connected
ServiceMode.client. However, the MQTTToggleLidPositionevent was still handled directly byServiceMode, which attempted to execute:with
self.clientbeingNone.As a result, the command was never sent to the AquaClean.
Expected behavior
When the application runs in
ApiMode, MQTT commands should be routed throughApiModeso that the configured BLE connection lifecycle is respected.For
toggleLid, the expected flow is:The MQTT topic and command semantics should remain unchanged.
Cause
ApiModecreates and embeds aServiceModeinstance.Historically,
ServiceModeregistered its own direct MQTT handlers for control commands. That is appropriate whenServiceModeowns a persistent BLE client, but it is not safe when the same service object is embedded inApiModewhileble_connection=on-demand.In that configuration, direct
ServiceModehandlers bypass theApiModemethods responsible for establishing the temporary BLE connection.For the observed
toggleLidfailure, this results in a call onNoneinstead of creating an on-demand connection first.Affected routing
The concrete failure reproduced here was
ToggleLidPosition.The same routing pattern also applies to other direct MQTT handlers that bypass
ApiMode's connection lifecycle. The proposed fix therefore addresses the handler ownership rather than special-casing onlytoggleLid.The investigation identified the same architectural issue for at least:
ToggleLidPositionResetFilterCounterConnectOnly the
toggleLidfailure described above was directly observed as a user-facing failure in this report.Proposed fix
Allow
ServiceModeto disable registration of its direct MQTT control handlers when it is embedded byApiMode.ApiModethen registers the affected MQTT handlers itself and routes them through its existing connection-aware methods.Conceptually:
For
toggleLid, this changes the execution path from a direct call onServiceMode.clientto:which uses the correct on-demand connection lifecycle.
Compatibility
The proposed change is intended to preserve existing external behavior:
ServiceModekeeps its existing direct-handler behavior;ServiceModeis embedded inApiMode.Regression coverage
Regression tests should verify that:
ApiModecreatesServiceModewith direct MQTT control-handler registration disabled;ApiModeowns the affected MQTT handlers;ToggleLidPositionis routed throughApiMode.run_command("toggle-lid");ResetFilterCounterandConnectare routed through the correspondingApiModelifecycle-aware methods;ServiceModeretains its legacy direct-handler behavior.Environment where reproduced
The failure was reproduced with:
ApiModeon-demandThe problem is primarily a bridge-side routing/lifecycle issue and is not believed to depend on a specific AquaClean firmware behavior.