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