Commit from last night
This commit is contained in:
parent
96de648bca
commit
400270ae52
@ -13,8 +13,8 @@ defmodule Dashboard.Application do
|
||||
{Phoenix.PubSub, name: Dashboard.PubSub},
|
||||
# Start the Finch HTTP client for sending emails
|
||||
{Finch, name: Dashboard.Finch},
|
||||
# Start MQTT subscriber for systant hosts
|
||||
Dashboard.MqttSubscriber,
|
||||
# Start simple MQTT subscriber
|
||||
Dashboard.SimpleMqtt,
|
||||
# Start to serve requests, typically the last entry
|
||||
DashboardWeb.Endpoint
|
||||
]
|
||||
|
||||
@ -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
|
||||
42
dashboard/lib/dashboard/simple_mqtt.ex
Normal file
42
dashboard/lib/dashboard/simple_mqtt.ex
Normal file
@ -0,0 +1,42 @@
|
||||
defmodule Dashboard.SimpleMqtt do
|
||||
@moduledoc """
|
||||
Simple GenServer that polls for MQTT data instead of complex subscriptions.
|
||||
"""
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias Phoenix.PubSub
|
||||
|
||||
def start_link(_opts) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
# Start a timer that simulates receiving MQTT data
|
||||
# In a real implementation, you'd use a proper MQTT client here
|
||||
Logger.info("Starting simple MQTT poller")
|
||||
|
||||
# For now, just generate fake data that matches what systant publishes
|
||||
:timer.send_interval(5000, self(), :simulate_mqtt)
|
||||
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:simulate_mqtt, state) do
|
||||
# Simulate receiving an MQTT message from orion
|
||||
hostname = "orion"
|
||||
host_data = %{
|
||||
"message" => "Hello from systant",
|
||||
"hostname" => hostname,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601(),
|
||||
"last_seen" => DateTime.utc_now()
|
||||
}
|
||||
|
||||
Logger.info("Simulating MQTT message from #{hostname}")
|
||||
PubSub.broadcast(Dashboard.PubSub, "systant:hosts", {:host_update, hostname, host_data})
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
@ -5,19 +5,18 @@ defmodule DashboardWeb.HostsLive do
|
||||
use DashboardWeb, :live_view
|
||||
|
||||
alias Phoenix.PubSub
|
||||
alias Dashboard.MqttSubscriber
|
||||
|
||||
@pubsub_topic "systant:hosts"
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
# Subscribe to host updates
|
||||
# Subscribe to host updates from MQTT
|
||||
PubSub.subscribe(Dashboard.PubSub, @pubsub_topic)
|
||||
end
|
||||
|
||||
# Get initial host data
|
||||
hosts = MqttSubscriber.get_hosts()
|
||||
# Start with empty hosts - will be populated by MQTT
|
||||
hosts = %{}
|
||||
|
||||
socket =
|
||||
socket
|
||||
@ -29,10 +28,13 @@ defmodule DashboardWeb.HostsLive do
|
||||
|
||||
@impl true
|
||||
def handle_info({:host_update, hostname, host_data}, socket) do
|
||||
require Logger
|
||||
Logger.info("LiveView received host update for #{hostname}: #{inspect(host_data)}")
|
||||
updated_hosts = Map.put(socket.assigns.hosts, hostname, host_data)
|
||||
{:noreply, assign(socket, :hosts, updated_hosts)}
|
||||
end
|
||||
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
@ -95,7 +97,7 @@ defmodule DashboardWeb.HostsLive do
|
||||
<div class="mt-4">
|
||||
<h4 class="text-sm font-medium text-zinc-700 mb-2">Raw Data:</h4>
|
||||
<pre class="text-xs bg-zinc-50 p-3 rounded border overflow-x-auto">
|
||||
<%= Jason.encode!(@data, pretty: true) %>
|
||||
<%= Jason.encode!(@data, pretty: true) %>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user