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
9 changes: 4 additions & 5 deletions src/batcontrol/mqtt_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,7 @@ def on_connect(self, client, userdata, flags, rc):
# Handle reconnect case
for topic in self.callbacks:
logger.debug('Subscribing topic: %s', topic)
for topic in self.callbacks:
client.subscribe(topic)
client.subscribe(topic)

def wait_ready(self) -> bool:
""" Wait for MQTT connection to be ready"""
Expand Down Expand Up @@ -310,7 +309,7 @@ def publish_SOC(self, soc: float) -> None: # pylint: disable=invalid-name
/SOC
"""
if self.client.is_connected():
self.client.publish(self.base_topic + '/SOC', f'{int(soc):03}')
self.client.publish(self._topic('SOC'), f'{soc:.2f}')

def publish_stored_energy_capacity(self, stored_energy: float) -> None:
""" Publish the stored energy capacity in Wh to MQTT
Expand Down Expand Up @@ -453,7 +452,7 @@ def publish_discharge_blocked(self, discharge_blocked: bool) -> None:
self.client.publish(
self.base_topic +
'/discharge_blocked',
str(discharge_blocked))
str(discharge_blocked).lower())

def publish_production_offset(self, production_offset: float) -> None:
""" Publish the production offset percentage to MQTT
Expand Down Expand Up @@ -670,7 +669,7 @@ def send_mqtt_discovery_messages(self) -> None:
None,
self.base_topic +
"/discharge_blocked",
value_template="{% if value | lower == 'True' %}blocked{% else %}not blocked{% endif %}")
value_template="{% if value | lower == 'true' %}blocked{% else %}not blocked{% endif %}")

self.publish_mqtt_discovery_message(
"Reserved Energy Capacity",
Expand Down
75 changes: 74 additions & 1 deletion tests/batcontrol/test_mqtt_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Tests for MqttApi._handle_message, focusing on bytes payload decoding."""
from unittest.mock import MagicMock
from unittest.mock import MagicMock, call

from batcontrol.mqtt_api import MqttApi

Expand Down Expand Up @@ -41,6 +41,20 @@ def _make_discovery_stub():
return api


def _make_publish_stub():
"""Return a minimal stub for publish helper tests."""
api = MagicMock(spec=MqttApi)
api.base_topic = 'batcontrol'
api.client = MagicMock()
api.client.is_connected.return_value = True
api._topic = MqttApi._topic.__get__(api, MqttApi)
api.publish_SOC = MqttApi.publish_SOC.__get__(api, MqttApi)
api.publish_discharge_blocked = (
MqttApi.publish_discharge_blocked.__get__(api, MqttApi)
)
return api


class TestHandleMessageBytesDecoding:
"""_handle_message must decode bytes payloads before calling convert."""

Expand Down Expand Up @@ -105,6 +119,49 @@ def test_str_convert_with_plain_string(self):
assert received == ['true']


class TestReconnectSubscriptions:
"""Reconnect handling should subscribe to each callback topic once."""

def test_on_connect_subscribes_each_callback_topic_once(self):
api = _make_handler_stub()
api.base_topic = 'batcontrol'
api.auto_discover_enable = False
api.callbacks = {
'batcontrol/mode/set': {},
'batcontrol/charge_rate/set': {},
}
api.on_connect = MqttApi.on_connect.__get__(api, MqttApi)
client = MagicMock()

api.on_connect(client, None, None, 0)

assert client.subscribe.call_args_list == [
call('batcontrol/mode/set'),
call('batcontrol/charge_rate/set'),
]
Comment thread
filiplajszczak marked this conversation as resolved.


class TestPublishedState:
"""Published state payloads should preserve precision and parse cleanly."""

def test_publish_soc_uses_decimal_precision(self):
api = _make_publish_stub()

api.publish_SOC(87.65)

api.client.publish.assert_called_once_with('batcontrol/SOC', '87.65')

def test_publish_discharge_blocked_uses_lowercase_boolean(self):
api = _make_publish_stub()

api.publish_discharge_blocked(True)

api.client.publish.assert_called_once_with(
'batcontrol/discharge_blocked',
'true',
)


class TestModeDiscovery:
"""Mode discovery should expose the full externally supported mode model."""

Expand Down Expand Up @@ -169,6 +226,22 @@ def test_discovery_includes_api_override_active_binary_sensor(self):
for call in api.publish_mqtt_discovery_message.call_args_list
)

def test_discharge_blocked_discovery_accepts_lowercase_true(self):
api = _make_discovery_stub()

api.send_mqtt_discovery_messages()

assert any(
call.args[:3] == (
'Discharge Blocked',
'batcontrol_discharge_blocked',
'sensor',
)
and call.args[5] == 'batcontrol/discharge_blocked'
and "value | lower == 'true'" in call.kwargs['value_template']
for call in api.publish_mqtt_discovery_message.call_args_list
)


class TestPeakShavingEnabledApi:
"""Regression test: peak_shaving/enabled must correctly parse the bytes payload."""
Expand Down