Update mqtt_controller.py
Added MQTT topics for the new alarm duration control: lva/<device_id>/alarm_duration/set lva/<device_id>/alarm_duration/state Published a new Home Assistant number discovery config: number.<device_id>_alarm_duration (0–3600 seconds, step 5, unit s). On /alarm_duration/set: validates and parses the payload as a non-negative integer, publishes a set_alarm_duration event on the EventBus, echoes the value back to /alarm_duration/state. On connect, publishes the current alarm_duration_seconds from preferences to the state topic for retained startup behavior.
This commit is contained in:
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MqttController(EventHandler):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -39,10 +40,27 @@ class MqttController(EventHandler):
|
||||
|
||||
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 = {
|
||||
"mute": { "command": f"{self._topic_prefix}/mute/set", "state": f"{self._topic_prefix}/mute/state" },
|
||||
"num_leds": { "command": f"{self._topic_prefix}/num_leds/set", "state": f"{self._topic_prefix}/num_leds/state" }
|
||||
"mute": {
|
||||
"command": f"{self._topic_prefix}/mute/set",
|
||||
"state": f"{self._topic_prefix}/mute/state",
|
||||
},
|
||||
"num_leds": {
|
||||
"command": f"{self._topic_prefix}/num_leds/set",
|
||||
"state": f"{self._topic_prefix}/num_leds/state",
|
||||
},
|
||||
# New: alarm duration
|
||||
"alarm_duration": {
|
||||
"command": f"{self._topic_prefix}/alarm_duration/set",
|
||||
"state": f"{self._topic_prefix}/alarm_duration/state",
|
||||
},
|
||||
}
|
||||
for state_name in self.CONFIGURABLE_STATES:
|
||||
self.topics[state_name] = {
|
||||
@@ -63,14 +81,18 @@ class MqttController(EventHandler):
|
||||
try:
|
||||
if self._username:
|
||||
self._client.username_pw_set(self._username, self._password)
|
||||
_LOGGER.debug("Connecting to MQTT broker at %s:%s", self._host, self._port)
|
||||
_LOGGER.debug(
|
||||
"Connecting to MQTT broker at %s:%s", self._host, self._port
|
||||
)
|
||||
self._client.connect(self._host, self._port, 60)
|
||||
self._client.loop_start()
|
||||
except Exception:
|
||||
_LOGGER.exception("Failed to connect to MQTT broker")
|
||||
|
||||
def stop(self):
|
||||
self._client.publish(f"{self._topic_prefix}/availability", "offline", retain=True)
|
||||
self._client.publish(
|
||||
f"{self._topic_prefix}/availability", "offline", retain=True
|
||||
)
|
||||
self._client.loop_stop()
|
||||
self._client.disconnect()
|
||||
_LOGGER.debug("Disconnected from MQTT broker")
|
||||
@@ -88,8 +110,12 @@ class MqttController(EventHandler):
|
||||
try:
|
||||
topic = msg.topic
|
||||
payload = msg.payload.decode()
|
||||
_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)
|
||||
_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
|
||||
)
|
||||
except Exception:
|
||||
_LOGGER.exception("Error in _on_message")
|
||||
|
||||
@@ -98,7 +124,9 @@ class MqttController(EventHandler):
|
||||
|
||||
if topic == self.topics["mute"]["command"]:
|
||||
# Publish event for MicMuteHandler to process
|
||||
self.event_bus.publish("set_mic_mute", {"state": payload.upper() == "ON"})
|
||||
self.event_bus.publish(
|
||||
"set_mic_mute", {"state": payload.upper() == "ON"}
|
||||
)
|
||||
|
||||
elif topic == self.topics["num_leds"]["command"]:
|
||||
try:
|
||||
@@ -106,56 +134,181 @@ class MqttController(EventHandler):
|
||||
# Publish event for LedController and MicMuteHandler (to save)
|
||||
self.event_bus.publish("set_num_leds", {"num_leds": num_leds})
|
||||
self.publish_num_leds_state(num_leds)
|
||||
except ValueError: pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
elif topic == self.topics["alarm_duration"]["command"]:
|
||||
try:
|
||||
duration = int(payload)
|
||||
if duration < 0:
|
||||
raise ValueError
|
||||
# Publish event for MicMuteHandler (to save preferences)
|
||||
self.event_bus.publish(
|
||||
"set_alarm_duration",
|
||||
{"alarm_duration_seconds": duration},
|
||||
)
|
||||
self.publish_alarm_duration_state(duration)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Invalid alarm_duration payload received: %r", payload
|
||||
)
|
||||
|
||||
for state_name in self.CONFIGURABLE_STATES:
|
||||
state_topics = self.topics[state_name]
|
||||
if topic == state_topics["effect_command"]:
|
||||
effect_id = payload.lower().replace(" ", "_")
|
||||
self.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"]:
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
if "state" in data:
|
||||
if data["state"].upper() == "OFF":
|
||||
self.event_bus.publish(f"set_{state_name}_effect", {"effect": "off"})
|
||||
self.event_bus.publish(
|
||||
f"set_{state_name}_effect", {"effect": "off"}
|
||||
)
|
||||
else: # ON command
|
||||
self.event_bus.publish(f"turn_on_{state_name}")
|
||||
|
||||
if "color" in data or "brightness" in data:
|
||||
self.event_bus.publish(f"set_{state_name}_color", data)
|
||||
except json.JSONDecodeError: pass
|
||||
self.event_bus.publish(
|
||||
f"set_{state_name}_color", data
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
elif topic == state_topics["effect_state"]:
|
||||
effect_id = payload.lower().replace(" ", "_")
|
||||
self.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"]:
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
data["retained"] = True
|
||||
self.event_bus.publish(f"set_{state_name}_color", data)
|
||||
except json.JSONDecodeError: pass
|
||||
self.event_bus.publish(
|
||||
f"set_{state_name}_color", data
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
def _publish_discovery_configs(self):
|
||||
availability_topic = f"{self._topic_prefix}/availability"
|
||||
device_info = { "identifiers": [self._device_id], "name": self._device_name, "manufacturer": "LVA Project" }
|
||||
options = ["Off", "Solid", "Slow Pulse", "Medium Pulse", "Fast Pulse", "Slow Blink", "Medium Blink", "Fast Blink", "Spin"]
|
||||
device_info = {
|
||||
"identifiers": [self._device_id],
|
||||
"name": self._device_name,
|
||||
"manufacturer": "LVA Project",
|
||||
}
|
||||
options = [
|
||||
"Off",
|
||||
"Solid",
|
||||
"Slow Pulse",
|
||||
"Medium Pulse",
|
||||
"Fast Pulse",
|
||||
"Slow Blink",
|
||||
"Medium Blink",
|
||||
"Fast Blink",
|
||||
"Spin",
|
||||
]
|
||||
|
||||
mute_cfg = { "name": "Mute Microphone", "unique_id": f"{self._device_id}_mute", "command_topic": self.topics["mute"]["command"], "state_topic": self.topics["mute"]["state"], "availability_topic": availability_topic, "icon": "mdi:microphone-off", "device": device_info }
|
||||
self._client.publish(f"homeassistant/switch/{self._device_id}_mute/config", json.dumps(mute_cfg), retain=True)
|
||||
mute_cfg = {
|
||||
"name": "Mute Microphone",
|
||||
"unique_id": f"{self._device_id}_mute",
|
||||
"command_topic": self.topics["mute"]["command"],
|
||||
"state_topic": self.topics["mute"]["state"],
|
||||
"availability_topic": availability_topic,
|
||||
"icon": "mdi:microphone-off",
|
||||
"device": device_info,
|
||||
}
|
||||
self._client.publish(
|
||||
f"homeassistant/switch/{self._device_id}_mute/config",
|
||||
json.dumps(mute_cfg),
|
||||
retain=True,
|
||||
)
|
||||
|
||||
num_leds_cfg = { "name": "Number of LEDs", "unique_id": f"{self._device_id}_num_leds", "command_topic": self.topics["num_leds"]["command"], "state_topic": self.topics["num_leds"]["state"], "availability_topic": availability_topic, "min": 1, "max": 256, "step": 1, "icon": "mdi:counter", "device": device_info, "mode": "box", "entity_category": "config" }
|
||||
self._client.publish(f"homeassistant/number/{self._device_id}_num_leds/config", json.dumps(num_leds_cfg), retain=True)
|
||||
num_leds_cfg = {
|
||||
"name": "Number of LEDs",
|
||||
"unique_id": f"{self._device_id}_num_leds",
|
||||
"command_topic": self.topics["num_leds"]["command"],
|
||||
"state_topic": self.topics["num_leds"]["state"],
|
||||
"availability_topic": availability_topic,
|
||||
"min": 1,
|
||||
"max": 256,
|
||||
"step": 1,
|
||||
"icon": "mdi:counter",
|
||||
"device": device_info,
|
||||
"mode": "box",
|
||||
"entity_category": "config",
|
||||
}
|
||||
self._client.publish(
|
||||
f"homeassistant/number/{self._device_id}_num_leds/config",
|
||||
json.dumps(num_leds_cfg),
|
||||
retain=True,
|
||||
)
|
||||
|
||||
# New: Alarm Duration number (seconds)
|
||||
alarm_duration_cfg = {
|
||||
"name": "Alarm Duration",
|
||||
"unique_id": f"{self._device_id}_alarm_duration",
|
||||
"command_topic": self.topics["alarm_duration"]["command"],
|
||||
"state_topic": self.topics["alarm_duration"]["state"],
|
||||
"availability_topic": availability_topic,
|
||||
"min": 0,
|
||||
"max": 3600,
|
||||
"step": 5,
|
||||
"unit_of_measurement": "s",
|
||||
"icon": "mdi:timer",
|
||||
"device": device_info,
|
||||
"mode": "box",
|
||||
"entity_category": "config",
|
||||
}
|
||||
self._client.publish(
|
||||
f"homeassistant/number/{self._device_id}_alarm_duration/config",
|
||||
json.dumps(alarm_duration_cfg),
|
||||
retain=True,
|
||||
)
|
||||
|
||||
for state_name in self.CONFIGURABLE_STATES:
|
||||
capital_name = state_name.title()
|
||||
|
||||
select_cfg = { "name": f"{capital_name} Effect", "unique_id": f"{self._device_id}_{state_name}_effect", "command_topic": self.topics[state_name]["effect_command"], "state_topic": self.topics[state_name]["effect_state"], "availability_topic": availability_topic, "options": options, "icon": "mdi:palette-swatch-variant", "device": device_info, "entity_category": "config" }
|
||||
self._client.publish(f"homeassistant/select/{self._device_id}_{state_name}_effect/config", json.dumps(select_cfg), retain=True)
|
||||
select_cfg = {
|
||||
"name": f"{capital_name} Effect",
|
||||
"unique_id": f"{self._device_id}_{state_name}_effect",
|
||||
"command_topic": self.topics[state_name]["effect_command"],
|
||||
"state_topic": self.topics[state_name]["effect_state"],
|
||||
"availability_topic": availability_topic,
|
||||
"options": options,
|
||||
"icon": "mdi:palette-swatch-variant",
|
||||
"device": device_info,
|
||||
"entity_category": "config",
|
||||
}
|
||||
self._client.publish(
|
||||
f"homeassistant/select/{self._device_id}_{state_name}_effect/config",
|
||||
json.dumps(select_cfg),
|
||||
retain=True,
|
||||
)
|
||||
|
||||
light_cfg = { "name": f"{capital_name} Color", "unique_id": f"{self._device_id}_{state_name}_color", "schema": "json", "command_topic": self.topics[state_name]["light_command"], "state_topic": self.topics[state_name]["light_state"], "availability_topic": availability_topic, "brightness": True, "color_mode": True, "supported_color_modes": ["rgb"], "device": device_info, "entity_category": "config" }
|
||||
self._client.publish(f"homeassistant/light/{self._device_id}_{state_name}_color/config", json.dumps(light_cfg), retain=True)
|
||||
light_cfg = {
|
||||
"name": f"{capital_name} Color",
|
||||
"unique_id": f"{self._device_id}_{state_name}_color",
|
||||
"schema": "json",
|
||||
"command_topic": self.topics[state_name]["light_command"],
|
||||
"state_topic": self.topics[state_name]["light_state"],
|
||||
"availability_topic": availability_topic,
|
||||
"brightness": True,
|
||||
"color_mode": True,
|
||||
"supported_color_modes": ["rgb"],
|
||||
"device": device_info,
|
||||
"entity_category": "config",
|
||||
}
|
||||
self._client.publish(
|
||||
f"homeassistant/light/{self._device_id}_{state_name}_color/config",
|
||||
json.dumps(light_cfg),
|
||||
retain=True,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Published all MQTT discovery configs")
|
||||
self._client.publish(availability_topic, "online", retain=True)
|
||||
@@ -163,13 +316,29 @@ class MqttController(EventHandler):
|
||||
# Publish initial state from internal values
|
||||
self.publish_mute_state(self._is_muted)
|
||||
self.publish_num_leds_state(self.preferences.num_leds)
|
||||
self.publish_alarm_duration_state(
|
||||
getattr(self.preferences, "alarm_duration_seconds", 0)
|
||||
)
|
||||
|
||||
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):
|
||||
self._client.publish(self.topics["num_leds"]["state"], str(num_leds), retain=True)
|
||||
self._client.publish(
|
||||
self.topics["num_leds"]["state"], str(num_leds), retain=True
|
||||
)
|
||||
|
||||
def publish_alarm_duration_state(self, duration_seconds: int):
|
||||
self._client.publish(
|
||||
self.topics["alarm_duration"]["state"],
|
||||
str(int(duration_seconds)),
|
||||
retain=True,
|
||||
)
|
||||
|
||||
@subscribe
|
||||
def publish_state_to_mqtt(self, data: dict):
|
||||
@@ -178,14 +347,24 @@ class MqttController(EventHandler):
|
||||
state_topics = self.topics[state_name]
|
||||
|
||||
effect_name = data.get("effect", "off").replace("_", " ").title()
|
||||
self._client.publish(state_topics["effect_state"], effect_name, retain=True)
|
||||
self._client.publish(
|
||||
state_topics["effect_state"], effect_name, retain=True
|
||||
)
|
||||
|
||||
light_state = {
|
||||
"state": "ON" if data.get("effect") != "off" else "OFF",
|
||||
"brightness": int(data.get("brightness", 0.5) * 255),
|
||||
"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 ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user