systant/lib/system_stats_daemon/mqtt_client.ex
ryan 9d8306a64b Initial commit: Elixir MQTT system stats daemon
- MQTT client with configurable broker connection
- Periodic system stats publishing (30s interval)
- Command listening on MQTT topic with logging
- Systemd service configuration
- NixOS module for declarative deployment

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 16:56:10 -07:00

92 lines
2.7 KiB
Elixir

defmodule SystemStatsDaemon.MqttClient do
use GenServer
require Logger
@moduledoc """
MQTT client for publishing system stats and handling commands
"""
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
config = Application.get_env(:system_stats_daemon, __MODULE__)
Logger.info("Starting MQTT client with config: #{inspect(config)}")
# Use a unique client ID to avoid conflicts
client_id = "#{config[:client_id]}_#{:rand.uniform(1000)}"
connection_opts = [
client_id: client_id,
server: {Tortoise.Transport.Tcp, host: to_charlist(config[:host]), port: config[:port]},
handler: {Tortoise.Handler.Logger, []},
user_name: config[:username],
password: config[:password],
subscriptions: [{config[:command_topic], 0}]
]
case Tortoise.Connection.start_link(connection_opts) do
{:ok, _pid} ->
Logger.info("MQTT client connected successfully")
# Send immediate hello message
hello_msg = %{
message: "Hello - system_stats_daemon started",
timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
hostname: get_hostname()
}
Tortoise.publish(client_id, config[:stats_topic], Jason.encode!(hello_msg), qos: 0)
schedule_stats_publish(config[:publish_interval])
{:ok, %{config: config, client_id: client_id}}
{:error, reason} ->
Logger.error("Failed to connect to MQTT broker: #{inspect(reason)}")
{:stop, reason}
end
end
def handle_info(:publish_stats, state) do
publish_stats(state.config, state.client_id)
schedule_stats_publish(state.config[:publish_interval])
{:noreply, state}
end
def handle_info(_msg, state) do
{:noreply, state}
end
def terminate(reason, state) do
Logger.info("MQTT client terminating: #{inspect(reason)}")
:ok
end
defp publish_stats(config, client_id) do
stats = %{
message: "Hello from system_stats_daemon",
timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
hostname: get_hostname()
}
payload = Jason.encode!(stats)
case Tortoise.publish(client_id, config[:stats_topic], payload, qos: 0) do
:ok ->
Logger.info("Published stats: #{payload}")
{:error, reason} ->
Logger.error("Failed to publish stats: #{inspect(reason)}")
end
end
defp schedule_stats_publish(interval) do
Process.send_after(self(), :publish_stats, interval)
end
defp get_hostname do
case :inet.gethostname() do
{:ok, hostname} -> List.to_string(hostname)
_ -> "unknown"
end
end
end