Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions apps/predbat/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ def initialize(self, gateway_device_id=None, mqtt_host=None, mqtt_port=8883, mqt
# Last read-only state sent to the gateway (None until the first send,
# so the current state is always pushed once on startup)
self._last_read_only = None
# Monotonic-ish timestamp of the last set_read_only send, used to force a
# periodic re-send so the gateway re-syncs even if a command was missed
self._last_read_only_sent_time = 0

# Set once the first MQTT connection attempt has completed (success or failure)
self._first_connection_attempted = False
Expand Down Expand Up @@ -1119,19 +1122,25 @@ async def _check_read_only_state(self):
Called on every run() cycle. Sends the current state once on startup (when
_last_read_only is still None) and again on each subsequent transition, so the
gateway firmware always knows whether PredBat is permitted to control the
inverter. The command is gateway-wide, so it carries no inverter serial. Gated
only on an active MQTT connection — it does not depend on auto-config, so the
state is established as early as possible.
inverter. To guard against the gateway losing sync (the command is not retained,
so a single missed message would leave it stale), the state is also re-sent at
least every 30 minutes even when unchanged. The command is gateway-wide, so it
carries no inverter serial. Gated only on an active MQTT connection — it does not
depend on auto-config, so the state is established as early as possible.
"""
if not self._mqtt_connected:
# Force a re-send after reconnect (command is not retained).
self._last_read_only = None
return
read_only = bool(self.get_arg("set_read_only", False))
if read_only == self._last_read_only:
now = time.time()
changed = read_only != self._last_read_only
stale = now - self._last_read_only_sent_time >= 30 * 60
if not changed and not stale:
return
Comment thread
springfall2008 marked this conversation as resolved.
await self.publish_command("set_read_only", enable=read_only)
self._last_read_only = read_only
self._last_read_only_sent_time = now
self.log(f"Info: GatewayMQTT: set_read_only command sent (read_only={read_only})")

async def _check_inverter_resets(self):
Expand Down
21 changes: 21 additions & 0 deletions apps/predbat/tests/test_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -3001,6 +3001,7 @@ def _make_gateway(self, read_only=False, connected=True):
gw.log = MagicMock()
gw._mqtt_connected = connected
gw._last_read_only = None
gw._last_read_only_sent_time = 0
gw._published = []

self._read_only = read_only
Expand Down Expand Up @@ -3058,6 +3059,26 @@ def test_sends_on_change(self):
assert gw._published[1] == ("set_read_only", {"enable": True})
assert gw._last_read_only is True

def test_resends_when_stale(self):
"""An unchanged state is re-published once more than 30 minutes have elapsed."""
gw = self._make_gateway(read_only=False)
self._run(gw._check_read_only_state())
assert len(gw._published) == 1
# Pretend the last send happened over 30 minutes ago.
gw._last_read_only_sent_time -= 30 * 60 + 1
self._run(gw._check_read_only_state())
assert len(gw._published) == 2
assert gw._published[1] == ("set_read_only", {"enable": False})

def test_no_resend_just_under_interval(self):
"""An unchanged state is not re-published before the 30 minute interval."""
gw = self._make_gateway(read_only=False)
self._run(gw._check_read_only_state())
# Just under the re-send interval — no extra publish expected.
gw._last_read_only_sent_time -= 30 * 60 - 5
self._run(gw._check_read_only_state())
assert len(gw._published) == 1

def test_not_connected_skips_send(self):
"""Nothing is published while MQTT is disconnected, and state is not latched."""
gw = self._make_gateway(read_only=True, connected=False)
Expand Down
Loading