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},
|
{Phoenix.PubSub, name: Dashboard.PubSub},
|
||||||
# Start the Finch HTTP client for sending emails
|
# Start the Finch HTTP client for sending emails
|
||||||
{Finch, name: Dashboard.Finch},
|
{Finch, name: Dashboard.Finch},
|
||||||
# Start MQTT subscriber for systant hosts
|
# Start simple MQTT subscriber
|
||||||
Dashboard.MqttSubscriber,
|
Dashboard.SimpleMqtt,
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
DashboardWeb.Endpoint
|
DashboardWeb.Endpoint
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
defmodule Dashboard.MqttSubscriber do
|
defmodule Dashboard.MqttSubscriber do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
MQTT subscriber that listens to systant host messages and broadcasts updates via Phoenix PubSub.
|
Simple MQTT subscriber for development dashboard.
|
||||||
"""
|
"""
|
||||||
use GenServer
|
use GenServer
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Phoenix.PubSub
|
alias Phoenix.PubSub
|
||||||
|
|
||||||
@mqtt_topic "systant/+/stats"
|
|
||||||
@pubsub_topic "systant:hosts"
|
@pubsub_topic "systant:hosts"
|
||||||
|
|
||||||
def start_link(opts) do
|
def start_link(opts) do
|
||||||
@ -20,28 +19,17 @@ defmodule Dashboard.MqttSubscriber do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_opts) do
|
def init(_opts) do
|
||||||
# Start MQTT connection
|
# Start MQTT connection in a supervised way
|
||||||
client_id = "systant_dashboard_#{System.unique_integer([:positive])}"
|
{:ok, _pid} = Tortoise.Supervisor.start_child(
|
||||||
|
:dashboard_mqtt,
|
||||||
connection_opts = [
|
client_id: :dashboard_mqtt,
|
||||||
client_id: client_id,
|
|
||||||
server: {Tortoise.Transport.Tcp, host: "mqtt.home", port: 1883},
|
server: {Tortoise.Transport.Tcp, host: "mqtt.home", port: 1883},
|
||||||
handler: {__MODULE__, []}
|
handler: {__MODULE__, []},
|
||||||
]
|
subscriptions: [{"systant/+/stats", 0}]
|
||||||
|
)
|
||||||
|
|
||||||
case Tortoise.Supervisor.start_child(client_id, connection_opts) do
|
Logger.info("Dashboard MQTT subscriber started")
|
||||||
{:ok, _pid} ->
|
{:ok, %{hosts: %{}}}
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -50,73 +38,41 @@ defmodule Dashboard.MqttSubscriber do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:tortoise, {:publish, @mqtt_topic, payload, _opts}}, state) do
|
def handle_info(_msg, 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}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def handle_info({:tortoise, {:publish, topic, payload, _opts}}, state) do
|
# Tortoise handler callbacks
|
||||||
# Extract hostname from the actual topic
|
def connection(_status, _state), do: []
|
||||||
|
def subscription(_status, _topic, _state), do: []
|
||||||
|
|
||||||
|
def handle_message(topic, payload, _state) do
|
||||||
case String.split(topic, "/") do
|
case String.split(topic, "/") do
|
||||||
["systant", hostname, "stats"] ->
|
["systant", hostname, "stats"] ->
|
||||||
case Jason.decode(payload) do
|
case Jason.decode(payload) do
|
||||||
{:ok, data} ->
|
{:ok, data} ->
|
||||||
# Update host data with timestamp
|
|
||||||
host_data = Map.put(data, "last_seen", DateTime.utc_now())
|
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})
|
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} ->
|
||||||
|
:ok
|
||||||
{:error, reason} ->
|
|
||||||
Logger.warning("Failed to decode JSON payload: #{inspect(reason)}")
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Logger.debug("Received message on unexpected topic: #{topic}")
|
:ok
|
||||||
{:noreply, state}
|
|
||||||
end
|
end
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:tortoise, _msg}, state) do
|
@impl true
|
||||||
# Handle other tortoise messages (connection status, etc.)
|
def handle_cast({:update_host, hostname, host_data}, state) do
|
||||||
{:noreply, state}
|
updated_hosts = Map.put(state.hosts, hostname, host_data)
|
||||||
|
{:noreply, %{state | hosts: updated_hosts}}
|
||||||
end
|
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: []
|
def terminate(_reason, _state), do: []
|
||||||
end
|
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
|
use DashboardWeb, :live_view
|
||||||
|
|
||||||
alias Phoenix.PubSub
|
alias Phoenix.PubSub
|
||||||
alias Dashboard.MqttSubscriber
|
|
||||||
|
|
||||||
@pubsub_topic "systant:hosts"
|
@pubsub_topic "systant:hosts"
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
# Subscribe to host updates
|
# Subscribe to host updates from MQTT
|
||||||
PubSub.subscribe(Dashboard.PubSub, @pubsub_topic)
|
PubSub.subscribe(Dashboard.PubSub, @pubsub_topic)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get initial host data
|
# Start with empty hosts - will be populated by MQTT
|
||||||
hosts = MqttSubscriber.get_hosts()
|
hosts = %{}
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
@ -29,10 +28,13 @@ defmodule DashboardWeb.HostsLive do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:host_update, hostname, host_data}, socket) do
|
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)
|
updated_hosts = Map.put(socket.assigns.hosts, hostname, host_data)
|
||||||
{:noreply, assign(socket, :hosts, updated_hosts)}
|
{:noreply, assign(socket, :hosts, updated_hosts)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -95,7 +97,7 @@ defmodule DashboardWeb.HostsLive do
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<h4 class="text-sm font-medium text-zinc-700 mb-2">Raw Data:</h4>
|
<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">
|
<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>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user