Skip to content

Commit 352eee7

Browse files
mqtt: extract shared control topic helpers (#345)
1 parent 1f335fe commit 352eee7

2 files changed

Lines changed: 45 additions & 37 deletions

File tree

src/batcontrol/mqtt_api.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@
5151
logger = logging.getLogger(__name__)
5252
logger.info('Loading module')
5353

54+
TOPIC_MODE = 'mode'
55+
TOPIC_CHARGE_RATE = 'charge_rate'
56+
TOPIC_LIMIT_BATTERY_CHARGE_RATE = 'limit_battery_charge_rate'
57+
TOPIC_API_OVERRIDE_ACTIVE = 'api_override_active'
58+
TOPIC_SET_SUFFIX = '/set'
59+
5460

5561
class MqttApi:
5662
""" MQTT API to publish data from batcontrol to MQTT for further processing+visualization"""
57-
SET_SUFFIX = '/set'
5863

5964
def __init__(self, config: dict, interval_minutes: int = 60):
6065
self.config = config
@@ -151,6 +156,14 @@ def wait_ready(self) -> bool:
151156

152157
return True
153158

159+
def _topic(self, topic: str) -> str:
160+
"""Build a state topic below the configured base topic."""
161+
return f"{self.base_topic}/{topic}"
162+
163+
def _set_topic(self, topic: str) -> str:
164+
"""Build a command topic below the configured base topic."""
165+
return f"{self.base_topic}/{topic}{TOPIC_SET_SUFFIX}"
166+
154167
def _handle_message(self, client, userdata, message): # pylint: disable=unused-argument
155168
""" Handle and dispatch incoming messages"""
156169
logger.debug('Received message on %s', message.topic)
@@ -179,7 +192,7 @@ def register_set_callback(
179192
""" Generic- register a callback for changing values inside batcontrol via
180193
MQTT set topics
181194
"""
182-
topic_string = self.base_topic + "/" + topic + MqttApi.SET_SUFFIX
195+
topic_string = self._set_topic(topic)
183196
logger.debug('Registering callback for %s', topic_string)
184197
# set api endpoints, generic subscription
185198
self.callbacks[topic_string] = {
@@ -192,22 +205,22 @@ def publish_mode(self, mode: int) -> None:
192205
/mode
193206
"""
194207
if self.client.is_connected():
195-
self.client.publish(self.base_topic + '/mode', mode)
208+
self.client.publish(self._topic(TOPIC_MODE), mode)
196209

197210
def publish_charge_rate(self, rate: float) -> None:
198211
""" Publish the forced charge rate in W to MQTT
199212
/charge_rate
200213
"""
201214
if self.client.is_connected():
202-
self.client.publish(self.base_topic + '/charge_rate', rate)
215+
self.client.publish(self._topic(TOPIC_CHARGE_RATE), rate)
203216

204217
def publish_limit_battery_charge_rate(self, limit: int) -> None:
205218
""" Publish dynamic battery charge rate limit to MQTT
206219
/limit_battery_charge_rate
207220
"""
208221
if self.client.is_connected():
209222
self.client.publish(
210-
self.base_topic + '/limit_battery_charge_rate',
223+
self._topic(TOPIC_LIMIT_BATTERY_CHARGE_RATE),
211224
limit
212225
)
213226

@@ -458,7 +471,7 @@ def publish_api_override_active(self, active: bool) -> None:
458471
"""
459472
if self.client.is_connected():
460473
self.client.publish(
461-
self.base_topic + '/api_override_active',
474+
self._topic(TOPIC_API_OVERRIDE_ACTIVE),
462475
str(active).lower(),
463476
retain=True
464477
)
@@ -518,10 +531,8 @@ def send_mqtt_discovery_messages(self) -> None:
518531
"number",
519532
"power",
520533
"W",
521-
self.base_topic +
522-
"/charge_rate",
523-
self.base_topic +
524-
"/charge_rate/set",
534+
self._topic(TOPIC_CHARGE_RATE),
535+
self._set_topic(TOPIC_CHARGE_RATE),
525536
entity_category="config",
526537
min_value=0,
527538
max_value=10000,
@@ -533,10 +544,8 @@ def send_mqtt_discovery_messages(self) -> None:
533544
"number",
534545
"power",
535546
"W",
536-
self.base_topic +
537-
"/limit_battery_charge_rate",
538-
self.base_topic +
539-
"/limit_battery_charge_rate/set",
547+
self._topic(TOPIC_LIMIT_BATTERY_CHARGE_RATE),
548+
self._set_topic(TOPIC_LIMIT_BATTERY_CHARGE_RATE),
540549
entity_category="config",
541550
min_value=-1,
542551
max_value=10000,
@@ -648,7 +657,7 @@ def send_mqtt_discovery_messages(self) -> None:
648657
"binary_sensor",
649658
None,
650659
None,
651-
self.base_topic + "/api_override_active",
660+
self._topic(TOPIC_API_OVERRIDE_ACTIVE),
652661
entity_category="diagnostic",
653662
value_template="{% if value == 'true' %}ON{% else %}OFF{% endif %}")
654663

@@ -788,8 +797,8 @@ def send_mqtt_discovery_for_mode(self) -> None:
788797
"select",
789798
None,
790799
None,
791-
self.base_topic + "/mode",
792-
self.base_topic + "/mode/set",
800+
self._topic(TOPIC_MODE),
801+
self._set_topic(TOPIC_MODE),
793802
entity_category=None,
794803
options=[
795804
"Charge from Grid",

tests/batcontrol/test_mqtt_api.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ def _make_message(topic: str, payload):
2525
return msg
2626

2727

28+
def _make_discovery_stub():
29+
"""Return a minimal stub for discovery helper tests."""
30+
api = MagicMock(spec=MqttApi)
31+
api.base_topic = 'batcontrol'
32+
api.publish_mqtt_discovery_message = MagicMock()
33+
api._topic = MqttApi._topic.__get__(api, MqttApi)
34+
api._set_topic = MqttApi._set_topic.__get__(api, MqttApi)
35+
api.send_mqtt_discovery_for_mode = (
36+
MqttApi.send_mqtt_discovery_for_mode.__get__(api, MqttApi)
37+
)
38+
api.send_mqtt_discovery_messages = (
39+
MqttApi.send_mqtt_discovery_messages.__get__(api, MqttApi)
40+
)
41+
return api
42+
43+
2844
class TestHandleMessageBytesDecoding:
2945
"""_handle_message must decode bytes payloads before calling convert."""
3046

@@ -93,12 +109,7 @@ class TestModeDiscovery:
93109
"""Mode discovery should expose the full externally supported mode model."""
94110

95111
def test_mode_discovery_includes_limit_battery_charge_mode(self):
96-
api = MagicMock(spec=MqttApi)
97-
api.base_topic = 'batcontrol'
98-
api.publish_mqtt_discovery_message = MagicMock()
99-
api.send_mqtt_discovery_for_mode = (
100-
MqttApi.send_mqtt_discovery_for_mode.__get__(api, MqttApi)
101-
)
112+
api = _make_discovery_stub()
102113

103114
api.send_mqtt_discovery_for_mode()
104115

@@ -120,13 +131,7 @@ class TestDiscoveryMessages:
120131
"""Discovery should expose key externally visible runtime state."""
121132

122133
def test_discovery_includes_limit_battery_charge_rate_number(self):
123-
api = MagicMock(spec=MqttApi)
124-
api.base_topic = 'batcontrol'
125-
api.publish_mqtt_discovery_message = MagicMock()
126-
api.send_mqtt_discovery_for_mode = MagicMock()
127-
api.send_mqtt_discovery_messages = (
128-
MqttApi.send_mqtt_discovery_messages.__get__(api, MqttApi)
129-
)
134+
api = _make_discovery_stub()
130135

131136
api.send_mqtt_discovery_messages()
132137

@@ -148,13 +153,7 @@ def test_discovery_includes_limit_battery_charge_rate_number(self):
148153
)
149154

150155
def test_discovery_includes_api_override_active_binary_sensor(self):
151-
api = MagicMock(spec=MqttApi)
152-
api.base_topic = 'batcontrol'
153-
api.publish_mqtt_discovery_message = MagicMock()
154-
api.send_mqtt_discovery_for_mode = MagicMock()
155-
api.send_mqtt_discovery_messages = (
156-
MqttApi.send_mqtt_discovery_messages.__get__(api, MqttApi)
157-
)
156+
api = _make_discovery_stub()
158157

159158
api.send_mqtt_discovery_messages()
160159

0 commit comments

Comments
 (0)