Update mqtt_controller.py
__init__ was completely rewritten to be decoupled. It now listens for mic_muted/mic_unmuted events to manage its own state instead of reading from ServerState.
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
from .event_bus import EventHandler, subscribe
|
from .config import MqttConfig
|
||||||
|
from .event_bus import EventBus, EventHandler, subscribe
|
||||||
|
from .models import Preferences
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .models import ServerState
|
from .models import ServerState
|
||||||
@@ -12,17 +15,30 @@ if TYPE_CHECKING:
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
class MqttController(EventHandler):
|
class MqttController(EventHandler):
|
||||||
def __init__(self, state: "ServerState", host: str, port: int, username: str, password: str):
|
def __init__(
|
||||||
super().__init__(state)
|
self,
|
||||||
self._host = host
|
loop: asyncio.AbstractEventLoop,
|
||||||
self._port = port
|
event_bus: EventBus,
|
||||||
self._username = username
|
config: MqttConfig,
|
||||||
self._password = password
|
app_name: str,
|
||||||
|
mac_address: str,
|
||||||
|
preferences: Preferences,
|
||||||
|
):
|
||||||
|
super().__init__(event_bus)
|
||||||
|
self.loop = loop
|
||||||
|
self.preferences = preferences
|
||||||
|
|
||||||
self._device_name = self.state.name
|
self._host = config.host
|
||||||
self._device_id = self.state.name.lower().replace(" ", "_")
|
self._port = config.port
|
||||||
|
self._username = config.username
|
||||||
|
self._password = config.password
|
||||||
|
|
||||||
|
self._device_name = app_name
|
||||||
|
self._device_id = app_name.lower().replace(" ", "_")
|
||||||
self._topic_prefix = f"lva/{self._device_id}"
|
self._topic_prefix = f"lva/{self._device_id}"
|
||||||
|
|
||||||
|
self._is_muted = False # Internal state
|
||||||
|
|
||||||
self.CONFIGURABLE_STATES: List[str] = ["idle", "listening", "thinking", "responding", "error"]
|
self.CONFIGURABLE_STATES: List[str] = ["idle", "listening", "thinking", "responding", "error"]
|
||||||
self.topics = {
|
self.topics = {
|
||||||
"mute": { "command": f"{self._topic_prefix}/mute/set", "state": f"{self._topic_prefix}/mute/state" },
|
"mute": { "command": f"{self._topic_prefix}/mute/set", "state": f"{self._topic_prefix}/mute/state" },
|
||||||
@@ -40,6 +56,9 @@ class MqttController(EventHandler):
|
|||||||
self._client.on_connect = self._on_connect
|
self._client.on_connect = self._on_connect
|
||||||
self._client.on_message = self._on_message
|
self._client.on_message = self._on_message
|
||||||
|
|
||||||
|
# Subscribe to events *after* __init__ is complete
|
||||||
|
self._subscribe_all_methods()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
try:
|
try:
|
||||||
if self._username:
|
if self._username:
|
||||||
@@ -59,49 +78,33 @@ class MqttController(EventHandler):
|
|||||||
def _on_connect(self, client, userdata, flags, rc):
|
def _on_connect(self, client, userdata, flags, rc):
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
_LOGGER.info("Connected to MQTT broker")
|
_LOGGER.info("Connected to MQTT broker")
|
||||||
# Subscribe to all command topics
|
client.subscribe(f"{self._topic_prefix}/+/set")
|
||||||
client.subscribe(f"{self._topic_prefix}/+/set") # Wildcard for all commands
|
|
||||||
|
|
||||||
# Subscribe to retained state topics to sync on startup
|
|
||||||
client.subscribe(f"{self._topic_prefix}/+/state")
|
client.subscribe(f"{self._topic_prefix}/+/state")
|
||||||
|
|
||||||
self._publish_discovery_configs()
|
self._publish_discovery_configs()
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("Failed to connect to MQTT, return code %d", rc)
|
_LOGGER.error("Failed to connect to MQTT, return code %d", rc)
|
||||||
|
|
||||||
def _on_message(self, client, userdata, msg):
|
def _on_message(self, client, userdata, msg):
|
||||||
"""
|
|
||||||
Handles incoming MQTT messages (runs in MQTT thread).
|
|
||||||
This must not block or modify shared state.
|
|
||||||
It schedules the real handler to run on the main asyncio loop.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
topic = msg.topic
|
topic = msg.topic
|
||||||
payload = msg.payload.decode()
|
payload = msg.payload.decode()
|
||||||
_LOGGER.debug("Received MQTT message on topic %s (from MQTT thread)", msg.topic)
|
_LOGGER.debug("Received MQTT message on topic %s (from MQTT thread)", msg.topic)
|
||||||
|
self.loop.call_soon_threadsafe(self._handle_message_on_loop, topic, payload)
|
||||||
# Schedule the actual processing on the main event loop
|
|
||||||
self.state.loop.call_soon_threadsafe(self._handle_message_on_loop, topic, payload)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Error in _on_message")
|
_LOGGER.exception("Error in _on_message")
|
||||||
|
|
||||||
def _handle_message_on_loop(self, topic: str, payload: str):
|
def _handle_message_on_loop(self, topic: str, payload: str):
|
||||||
"""
|
|
||||||
This method runs on the main asyncio loop and safely handles
|
|
||||||
the MQTT message logic and state changes.
|
|
||||||
"""
|
|
||||||
_LOGGER.debug("Handling message for topic %s on main loop", topic)
|
_LOGGER.debug("Handling message for topic %s on main loop", topic)
|
||||||
|
|
||||||
if topic == self.topics["mute"]["command"]:
|
if topic == self.topics["mute"]["command"]:
|
||||||
self.state.event_bus.publish("set_mic_mute", {"state": payload.upper() == "ON"})
|
# Publish event for MicMuteHandler to process
|
||||||
|
self.event_bus.publish("set_mic_mute", {"state": payload.upper() == "ON"})
|
||||||
|
|
||||||
elif topic == self.topics["num_leds"]["command"]:
|
elif topic == self.topics["num_leds"]["command"]:
|
||||||
try:
|
try:
|
||||||
num_leds = int(payload)
|
num_leds = int(payload)
|
||||||
self.state.event_bus.publish("set_num_leds", {"num_leds": num_leds})
|
# Publish event for LedController and MicMuteHandler (to save)
|
||||||
if self.state.preferences.num_leds != num_leds:
|
self.event_bus.publish("set_num_leds", {"num_leds": num_leds})
|
||||||
self.state.preferences.num_leds = num_leds
|
|
||||||
self.state.save_preferences()
|
|
||||||
self.publish_num_leds_state(num_leds)
|
self.publish_num_leds_state(num_leds)
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
|
|
||||||
@@ -109,32 +112,29 @@ the MQTT message logic and state changes.
|
|||||||
state_topics = self.topics[state_name]
|
state_topics = self.topics[state_name]
|
||||||
if topic == state_topics["effect_command"]:
|
if topic == state_topics["effect_command"]:
|
||||||
effect_id = payload.lower().replace(" ", "_")
|
effect_id = payload.lower().replace(" ", "_")
|
||||||
self.state.event_bus.publish(f"set_{state_name}_effect", {"effect": effect_id})
|
self.event_bus.publish(f"set_{state_name}_effect", {"effect": effect_id})
|
||||||
|
|
||||||
elif topic == state_topics["light_command"]:
|
elif topic == state_topics["light_command"]:
|
||||||
try:
|
try:
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
|
|
||||||
if "state" in data:
|
if "state" in data:
|
||||||
if data["state"].upper() == "OFF":
|
if data["state"].upper() == "OFF":
|
||||||
self.state.event_bus.publish(f"set_{state_name}_effect", {"effect": "off"})
|
self.event_bus.publish(f"set_{state_name}_effect", {"effect": "off"})
|
||||||
else: # ON command
|
else: # ON command
|
||||||
self.state.event_bus.publish(f"turn_on_{state_name}")
|
self.event_bus.publish(f"turn_on_{state_name}")
|
||||||
|
|
||||||
if "color" in data or "brightness" in data:
|
if "color" in data or "brightness" in data:
|
||||||
self.state.event_bus.publish(f"set_{state_name}_color", data)
|
self.event_bus.publish(f"set_{state_name}_color", data)
|
||||||
|
|
||||||
except json.JSONDecodeError: pass
|
except json.JSONDecodeError: pass
|
||||||
|
|
||||||
# Handle retained state messages on startup
|
|
||||||
elif topic == state_topics["effect_state"]:
|
elif topic == state_topics["effect_state"]:
|
||||||
effect_id = payload.lower().replace(" ", "_")
|
effect_id = payload.lower().replace(" ", "_")
|
||||||
self.state.event_bus.publish(f"set_{state_name}_effect", {"effect": effect_id, "retained": True})
|
self.event_bus.publish(f"set_{state_name}_effect", {"effect": effect_id, "retained": True})
|
||||||
elif topic == state_topics["light_state"]:
|
elif topic == state_topics["light_state"]:
|
||||||
try:
|
try:
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
data["retained"] = True
|
data["retained"] = True
|
||||||
self.state.event_bus.publish(f"set_{state_name}_color", data)
|
self.event_bus.publish(f"set_{state_name}_color", data)
|
||||||
except json.JSONDecodeError: pass
|
except json.JSONDecodeError: pass
|
||||||
|
|
||||||
def _publish_discovery_configs(self):
|
def _publish_discovery_configs(self):
|
||||||
@@ -160,10 +160,12 @@ the MQTT message logic and state changes.
|
|||||||
_LOGGER.debug("Published all MQTT discovery configs")
|
_LOGGER.debug("Published all MQTT discovery configs")
|
||||||
self._client.publish(availability_topic, "online", retain=True)
|
self._client.publish(availability_topic, "online", retain=True)
|
||||||
|
|
||||||
self.publish_mute_state(self.state.mic_muted)
|
# Publish initial state from internal values
|
||||||
self.publish_num_leds_state(self.state.preferences.num_leds)
|
self.publish_mute_state(self._is_muted)
|
||||||
|
self.publish_num_leds_state(self.preferences.num_leds)
|
||||||
|
|
||||||
def publish_mute_state(self, is_muted: bool):
|
def publish_mute_state(self, is_muted: bool):
|
||||||
|
self._is_muted = is_muted
|
||||||
self._client.publish(self.topics["mute"]["state"], "ON" if is_muted else "OFF", retain=True)
|
self._client.publish(self.topics["mute"]["state"], "ON" if is_muted else "OFF", retain=True)
|
||||||
|
|
||||||
def publish_num_leds_state(self, num_leds: int):
|
def publish_num_leds_state(self, num_leds: int):
|
||||||
@@ -184,3 +186,13 @@ the MQTT message logic and state changes.
|
|||||||
"color": { "r": data.get("color")[0], "g": data.get("color")[1], "b": data.get("color")[2] }
|
"color": { "r": data.get("color")[0], "g": data.get("color")[1], "b": data.get("color")[2] }
|
||||||
}
|
}
|
||||||
self._client.publish(state_topics["light_state"], json.dumps(light_state), retain=True)
|
self._client.publish(state_topics["light_state"], json.dumps(light_state), retain=True)
|
||||||
|
|
||||||
|
# --- Listen for state changes from other components ---
|
||||||
|
|
||||||
|
@subscribe
|
||||||
|
def mic_muted(self, data: dict):
|
||||||
|
self.publish_mute_state(True)
|
||||||
|
|
||||||
|
@subscribe
|
||||||
|
def mic_unmuted(self, data: dict):
|
||||||
|
self.publish_mute_state(False)
|
||||||
|
|||||||
Reference in New Issue
Block a user