Files
systant/dashboard/lib/dashboard/mqtt_subscriber.ex
T
ryanandClaude 4a928b7067 Update CLAUDE.md and fix final Tortoise handler return value
- Add dashboard development commands (just dashboard, mix phx.server)
- Document Dashboard.Application and Dashboard.MqttSubscriber components
- Add comprehensive dashboard section with MQTT configuration details
- Include critical implementation notes for Tortoise handler return values
- Fix handle_message to return {:ok, state} instead of [] to prevent crashes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-03 19:44:41 -07:00

97 lines
2.6 KiB
Elixir

defmodule Dashboard.MqttSubscriber do
@moduledoc """
Simple MQTT subscriber for development dashboard.
"""
use GenServer
require Logger
alias Phoenix.PubSub
@pubsub_topic "systant:hosts"
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def get_hosts do
GenServer.call(__MODULE__, :get_hosts)
end
@impl true
def init(_opts) do
# Start MQTT connection directly with hostname-based client ID to avoid conflicts
{:ok, hostname} = :inet.gethostname()
client_id = "systant-dashboard-#{hostname}"
connection_opts = [
client_id: client_id,
server: {Tortoise.Transport.Tcp, host: "mqtt.home", port: 1883},
handler: {__MODULE__, []},
subscriptions: [{"systant/+/stats", 0}]
]
case Tortoise.Connection.start_link(connection_opts) do
{:ok, _pid} ->
Logger.info("Dashboard MQTT subscriber connected successfully")
{:ok, %{hosts: %{}}}
{:error, {:already_started, _pid}} ->
Logger.info("Dashboard MQTT connection already exists, reusing")
{:ok, %{hosts: %{}}}
{:error, reason} ->
Logger.error("Failed to connect to MQTT broker: #{inspect(reason)}")
{:stop, reason}
end
end
@impl true
def handle_call(:get_hosts, _from, state) do
{:reply, state.hosts, state}
end
@impl true
def handle_info(_msg, state) do
{:noreply, state}
end
# Tortoise handler callbacks
def connection(status, state) do
Logger.info("MQTT connection status: #{status}")
{:ok, state}
end
def subscription(status, topic, state) do
Logger.info("MQTT subscription status for #{topic}: #{status}")
{:ok, state}
end
def handle_message(topic, payload, state) do
topic_parts = if is_binary(topic), do: String.split(topic, "/"), else: topic
case topic_parts do
["systant", hostname, "stats"] ->
case Jason.decode(payload) do
{:ok, data} ->
host_data = Map.put(data, "last_seen", DateTime.utc_now())
# Broadcast to LiveView
PubSub.broadcast(Dashboard.PubSub, @pubsub_topic, {:host_update, hostname, host_data})
# Update our state
GenServer.cast(__MODULE__, {:update_host, hostname, host_data})
{:error, _reason} ->
:ok
end
_ ->
:ok
end
{:ok, state}
end
@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
@impl true
def terminate(_reason, _state), do: []
end