diff --git a/lib/aprsme/device_parser.ex b/lib/aprsme/device_parser.ex index f9c7643..0aec1e6 100644 --- a/lib/aprsme/device_parser.ex +++ b/lib/aprsme/device_parser.ex @@ -12,12 +12,16 @@ defmodule Aprsme.DeviceParser do cond do Map.has_key?(packet_data, :destination) and not is_nil(packet_data[:destination]) -> packet_data[:destination] + Map.has_key?(packet_data, "destination") and not is_nil(packet_data["destination"]) -> packet_data["destination"] + Map.has_key?(packet_data, :device_identifier) and not is_nil(packet_data[:device_identifier]) -> packet_data[:device_identifier] + Map.has_key?(packet_data, "device_identifier") and not is_nil(packet_data["device_identifier"]) -> packet_data["device_identifier"] + true -> extract_from_data_extended(packet_data) end diff --git a/lib/aprsme/packets.ex b/lib/aprsme/packets.ex index 4a1f073..b05ed5e 100644 --- a/lib/aprsme/packets.ex +++ b/lib/aprsme/packets.ex @@ -49,6 +49,7 @@ defmodule Aprsme.Packets do parsed_device_id = Aprsme.DeviceParser.extract_device_identifier(packet_data) device_id = parsed_device_id || Map.get(packet_attrs, :destination) packet_attrs = Map.put(packet_attrs, :device_identifier, device_id) + # Logger.debug("Inserting packet with device_identifier=#{inspect(device_id)}, destination=#{inspect(Map.get(packet_attrs, :destination))}") insert_packet(packet_attrs, packet_data) rescue diff --git a/lib/aprsme_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex index 6b191cd..ef0be71 100644 --- a/lib/aprsme_web/live/info_live/show.ex +++ b/lib/aprsme_web/live/info_live/show.ex @@ -11,6 +11,12 @@ defmodule AprsmeWeb.InfoLive.Show do @impl true def mount(%{"callsign" => callsign}, _session, socket) do normalized_callsign = String.upcase(String.trim(callsign)) + + # Subscribe to Postgres notifications for live updates + if connected?(socket) do + Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") + end + packet = get_latest_packet(normalized_callsign) neighbors = get_neighbors(packet, normalized_callsign) @@ -24,6 +30,32 @@ defmodule AprsmeWeb.InfoLive.Show do {:ok, socket} end + @impl true + def handle_info({:postgres_packet, packet}, socket) do + # Only update if the packet is for our callsign + if packet_matches_callsign?(packet, socket.assigns.callsign) do + # Refresh data when new packet arrives + packet = get_latest_packet(socket.assigns.callsign) + neighbors = get_neighbors(packet, socket.assigns.callsign) + + socket = + socket + |> assign(:packet, packet) + |> assign(:neighbors, neighbors) + + {:noreply, socket} + else + {:noreply, socket} + end + end + + def handle_info(_message, socket), do: {:noreply, socket} + + defp packet_matches_callsign?(packet, callsign) do + packet_sender = Map.get(packet, "sender") || Map.get(packet, :sender, "") + String.upcase(packet_sender) == String.upcase(callsign) + end + defp get_latest_packet(callsign) do %{callsign: callsign, limit: 1} |> Packets.get_recent_packets() diff --git a/lib/aprsme_web/live/info_live/show.html.heex b/lib/aprsme_web/live/info_live/show.html.heex index a917e89..6c12a14 100644 --- a/lib/aprsme_web/live/info_live/show.html.heex +++ b/lib/aprsme_web/live/info_live/show.html.heex @@ -180,7 +180,9 @@ <%= if @packet.manufacturer && @packet.manufacturer != "" do %> {@packet.manufacturer} {@packet.equipment_type || ""} <% else %> - {@packet.device_identifier || "Unknown"} + + {@packet.device_identifier || "Unknown"} + <% end %> diff --git a/lib/aprsme_web/live/weather_live/callsign_view.ex b/lib/aprsme_web/live/weather_live/callsign_view.ex index f75243f..c49d6bd 100644 --- a/lib/aprsme_web/live/weather_live/callsign_view.ex +++ b/lib/aprsme_web/live/weather_live/callsign_view.ex @@ -8,6 +8,12 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do @impl true def mount(%{"callsign" => callsign}, _session, socket) do normalized_callsign = String.upcase(String.trim(callsign)) + + # Subscribe to Postgres notifications for live updates + if connected?(socket) do + Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") + end + weather_packet = get_latest_weather_packet(normalized_callsign) {start_time, end_time} = default_time_range() weather_history = get_weather_history(normalized_callsign, start_time, end_time) @@ -47,6 +53,78 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do {:ok, socket} end + @impl true + def handle_info({:postgres_packet, packet}, socket) do + # Only update if the packet is for our callsign and contains weather data + if packet_matches_callsign?(packet, socket.assigns.callsign) and has_weather_data?(packet) do + # Refresh weather data when new weather packet arrives + weather_packet = get_latest_weather_packet(socket.assigns.callsign) + {start_time, end_time} = default_time_range() + weather_history = get_weather_history(socket.assigns.callsign, start_time, end_time) + + weather_history_json = + weather_history + |> Enum.map(fn pkt -> + dew_point = + if is_number(pkt.temperature) and is_number(pkt.humidity) do + calc_dew_point(pkt.temperature, pkt.humidity) + end + + %{ + timestamp: pkt.received_at, + temperature: pkt.temperature, + dew_point: dew_point, + humidity: pkt.humidity, + pressure: pkt.pressure, + wind_direction: pkt.wind_direction, + wind_speed: pkt.wind_speed, + rain_1h: pkt.rain_1h, + rain_24h: pkt.rain_24h, + rain_since_midnight: pkt.rain_since_midnight, + luminosity: pkt.luminosity + } + end) + |> Jason.encode!() + + socket = + socket + |> assign(:weather_packet, weather_packet) + |> assign(:weather_history, weather_history) + |> assign(:weather_history_json, weather_history_json) + + {:noreply, socket} + else + {:noreply, socket} + end + end + + def handle_info(_message, socket), do: {:noreply, socket} + + defp packet_matches_callsign?(packet, callsign) do + packet_sender = Map.get(packet, "sender") || Map.get(packet, :sender, "") + String.upcase(packet_sender) == String.upcase(callsign) + end + + defp has_weather_data?(packet) do + # Check if packet contains any weather-related fields + weather_fields = [ + "temperature", + "humidity", + "pressure", + "wind_speed", + "wind_direction", + "rain_1h", + "rain_24h", + "rain_since_midnight", + "luminosity" + ] + + Enum.any?(weather_fields, fn field -> + value = Map.get(packet, field) || Map.get(packet, String.to_atom(field)) + not is_nil(value) + end) + end + defp get_latest_weather_packet(callsign) do # Get weather packets from the last 7 days to find the most recent one end_time = DateTime.utc_now() diff --git a/lib/aprsme_web/live/weather_live/callsign_view.html.heex b/lib/aprsme_web/live/weather_live/callsign_view.html.heex index b813d3c..9f66fad 100644 --- a/lib/aprsme_web/live/weather_live/callsign_view.html.heex +++ b/lib/aprsme_web/live/weather_live/callsign_view.html.heex @@ -152,7 +152,7 @@
{get_weather_field_zero(@weather_packet, :rain_since_midnight)} in
- +
Raw Packet
@@ -163,7 +163,7 @@ <% end %>
- +
Raw comment: {@weather_packet.comment || @weather_packet["comment"]}
diff --git a/priv/repo/migrations/20250617200000_recreate_notify_packets_insert_function.exs b/priv/repo/migrations/20250617200000_recreate_notify_packets_insert_function.exs index b61f118..049453f 100644 --- a/priv/repo/migrations/20250617200000_recreate_notify_packets_insert_function.exs +++ b/priv/repo/migrations/20250617200000_recreate_notify_packets_insert_function.exs @@ -12,6 +12,7 @@ defmodule Aprsme.Repo.Migrations.RecreateNotifyPacketsInsertFunction do BEGIN payload := json_build_object( 'id', NEW.id, + 'sender', NEW.sender, 'lat', NEW.lat, 'lon', NEW.lon, 'inserted_at', NEW.inserted_at diff --git a/priv/repo/migrations/20250704164340_update_packets_notification_payload.exs b/priv/repo/migrations/20250704164340_update_packets_notification_payload.exs new file mode 100644 index 0000000..e8a1bba --- /dev/null +++ b/priv/repo/migrations/20250704164340_update_packets_notification_payload.exs @@ -0,0 +1,60 @@ +defmodule Aprsme.Repo.Migrations.UpdatePacketsNotificationPayload do + use Ecto.Migration + + def up do + execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;" + execute "DROP FUNCTION IF EXISTS notify_packets_insert();" + + execute """ + CREATE OR REPLACE FUNCTION notify_packets_insert() RETURNS trigger AS $$ + DECLARE + payload TEXT; + BEGIN + payload := json_build_object( + 'id', NEW.id, + 'sender', NEW.sender, + 'lat', NEW.lat, + 'lon', NEW.lon, + 'inserted_at', NEW.inserted_at + )::text; + PERFORM pg_notify('aprs_packets', payload); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + """ + + execute """ + CREATE TRIGGER packets_notify_insert + AFTER INSERT ON packets + FOR EACH ROW EXECUTE FUNCTION notify_packets_insert(); + """ + end + + def down do + execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;" + execute "DROP FUNCTION IF EXISTS notify_packets_insert();" + + execute """ + CREATE OR REPLACE FUNCTION notify_packets_insert() RETURNS trigger AS $$ + DECLARE + payload TEXT; + BEGIN + payload := json_build_object( + 'id', NEW.id, + 'lat', NEW.lat, + 'lon', NEW.lon, + 'inserted_at', NEW.inserted_at + )::text; + PERFORM pg_notify('aprs_packets', payload); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + """ + + execute """ + CREATE TRIGGER packets_notify_insert + AFTER INSERT ON packets + FOR EACH ROW EXECUTE FUNCTION notify_packets_insert(); + """ + end +end