Commit from last night

This commit is contained in:
2025-08-03 13:33:21 -07:00
parent 96de648bca
commit 400270ae52
4 changed files with 80 additions and 80 deletions
+29 -73
View File
@@ -1,13 +1,12 @@
defmodule Dashboard.MqttSubscriber do
@moduledoc """
MQTT subscriber that listens to systant host messages and broadcasts updates via Phoenix PubSub.
Simple MQTT subscriber for development dashboard.
"""
use GenServer
require Logger
alias Phoenix.PubSub
@mqtt_topic "systant/+/stats"
@pubsub_topic "systant:hosts"
def start_link(opts) do
@@ -20,28 +19,17 @@ defmodule Dashboard.MqttSubscriber do
@impl true
def init(_opts) do
# Start MQTT connection
client_id = "systant_dashboard_#{System.unique_integer([:positive])}"
connection_opts = [
client_id: client_id,
# Start MQTT connection in a supervised way
{:ok, _pid} = Tortoise.Supervisor.start_child(
:dashboard_mqtt,
client_id: :dashboard_mqtt,
server: {Tortoise.Transport.Tcp, host: "mqtt.home", port: 1883},
handler: {__MODULE__, []}
]
case Tortoise.Supervisor.start_child(client_id, connection_opts) do
{:ok, _pid} ->
Logger.info("MQTT subscriber connected as #{client_id}")
# Subscribe to systant stats topic
Tortoise.Connection.subscribe(client_id, @mqtt_topic, qos: 0)
{:ok, %{client_id: client_id, hosts: %{}}}
{:error, reason} ->
Logger.error("Failed to start MQTT connection: #{inspect(reason)}")
{:stop, reason}
end
handler: {__MODULE__, []},
subscriptions: [{"systant/+/stats", 0}]
)
Logger.info("Dashboard MQTT subscriber started")
{:ok, %{hosts: %{}}}
end
@impl true
@@ -50,73 +38,41 @@ defmodule Dashboard.MqttSubscriber do
end
@impl true
def handle_info({:tortoise, {:publish, @mqtt_topic, payload, _opts}}, state) do
case Jason.decode(payload) do
{:ok, data} ->
# Extract hostname from topic
hostname = extract_hostname_from_topic(@mqtt_topic)
# Update host data with timestamp
host_data = Map.put(data, "last_seen", DateTime.utc_now())
updated_hosts = Map.put(state.hosts, hostname, host_data)
# Broadcast update via PubSub
PubSub.broadcast(Dashboard.PubSub, @pubsub_topic, {:host_update, hostname, host_data})
Logger.debug("Received update from #{hostname}: #{inspect(data)}")
{:noreply, %{state | hosts: updated_hosts}}
{:error, reason} ->
Logger.warning("Failed to decode JSON payload: #{inspect(reason)}")
{:noreply, state}
end
def handle_info(_msg, state) do
{:noreply, state}
end
def handle_info({:tortoise, {:publish, topic, payload, _opts}}, state) do
# Extract hostname from the actual topic
# Tortoise handler callbacks
def connection(_status, _state), do: []
def subscription(_status, _topic, _state), do: []
def handle_message(topic, payload, _state) do
case String.split(topic, "/") do
["systant", hostname, "stats"] ->
case Jason.decode(payload) do
{:ok, data} ->
# Update host data with timestamp
host_data = Map.put(data, "last_seen", DateTime.utc_now())
updated_hosts = Map.put(state.hosts, hostname, host_data)
# Broadcast update via PubSub
# Broadcast to LiveView
PubSub.broadcast(Dashboard.PubSub, @pubsub_topic, {:host_update, hostname, host_data})
Logger.debug("Received update from #{hostname}: #{inspect(data)}")
# Update our state
GenServer.cast(__MODULE__, {:update_host, hostname, host_data})
{:noreply, %{state | hosts: updated_hosts}}
{:error, reason} ->
Logger.warning("Failed to decode JSON payload: #{inspect(reason)}")
{:noreply, state}
{:error, _reason} ->
:ok
end
_ ->
Logger.debug("Received message on unexpected topic: #{topic}")
{:noreply, state}
:ok
end
[]
end
def handle_info({:tortoise, _msg}, state) do
# Handle other tortoise messages (connection status, etc.)
{:noreply, state}
@impl true
def handle_cast({:update_host, hostname, host_data}, state) do
updated_hosts = Map.put(state.hosts, hostname, host_data)
{:noreply, %{state | hosts: updated_hosts}}
end
# Private functions
defp extract_hostname_from_topic(topic) do
case String.split(topic, "/") do
["systant", hostname, "stats"] -> hostname
_ -> "unknown"
end
end
# Tortoise handler callbacks (required when using handler: {module, args})
def connection(_status, _state), do: []
def subscription(_status, _topic_filter, _state), do: []
def terminate(_reason, _state), do: []
end