live update info page
This commit is contained in:
parent
7a36960eb6
commit
a78fb8a385
8 changed files with 181 additions and 3 deletions
|
|
@ -12,12 +12,16 @@ defmodule Aprsme.DeviceParser do
|
||||||
cond do
|
cond do
|
||||||
Map.has_key?(packet_data, :destination) and not is_nil(packet_data[:destination]) ->
|
Map.has_key?(packet_data, :destination) and not is_nil(packet_data[:destination]) ->
|
||||||
packet_data[:destination]
|
packet_data[:destination]
|
||||||
|
|
||||||
Map.has_key?(packet_data, "destination") and not is_nil(packet_data["destination"]) ->
|
Map.has_key?(packet_data, "destination") and not is_nil(packet_data["destination"]) ->
|
||||||
packet_data["destination"]
|
packet_data["destination"]
|
||||||
|
|
||||||
Map.has_key?(packet_data, :device_identifier) and not is_nil(packet_data[:device_identifier]) ->
|
Map.has_key?(packet_data, :device_identifier) and not is_nil(packet_data[:device_identifier]) ->
|
||||||
packet_data[:device_identifier]
|
packet_data[:device_identifier]
|
||||||
|
|
||||||
Map.has_key?(packet_data, "device_identifier") and not is_nil(packet_data["device_identifier"]) ->
|
Map.has_key?(packet_data, "device_identifier") and not is_nil(packet_data["device_identifier"]) ->
|
||||||
packet_data["device_identifier"]
|
packet_data["device_identifier"]
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
extract_from_data_extended(packet_data)
|
extract_from_data_extended(packet_data)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ defmodule Aprsme.Packets do
|
||||||
parsed_device_id = Aprsme.DeviceParser.extract_device_identifier(packet_data)
|
parsed_device_id = Aprsme.DeviceParser.extract_device_identifier(packet_data)
|
||||||
device_id = parsed_device_id || Map.get(packet_attrs, :destination)
|
device_id = parsed_device_id || Map.get(packet_attrs, :destination)
|
||||||
packet_attrs = Map.put(packet_attrs, :device_identifier, device_id)
|
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))}")
|
# Logger.debug("Inserting packet with device_identifier=#{inspect(device_id)}, destination=#{inspect(Map.get(packet_attrs, :destination))}")
|
||||||
insert_packet(packet_attrs, packet_data)
|
insert_packet(packet_attrs, packet_data)
|
||||||
rescue
|
rescue
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"callsign" => callsign}, _session, socket) do
|
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||||
normalized_callsign = String.upcase(String.trim(callsign))
|
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)
|
packet = get_latest_packet(normalized_callsign)
|
||||||
neighbors = get_neighbors(packet, normalized_callsign)
|
neighbors = get_neighbors(packet, normalized_callsign)
|
||||||
|
|
||||||
|
|
@ -24,6 +30,32 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
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
|
defp get_latest_packet(callsign) do
|
||||||
%{callsign: callsign, limit: 1}
|
%{callsign: callsign, limit: 1}
|
||||||
|> Packets.get_recent_packets()
|
|> Packets.get_recent_packets()
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,9 @@
|
||||||
<%= if @packet.manufacturer && @packet.manufacturer != "" do %>
|
<%= if @packet.manufacturer && @packet.manufacturer != "" do %>
|
||||||
{@packet.manufacturer} {@packet.equipment_type || ""}
|
{@packet.manufacturer} {@packet.equipment_type || ""}
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="text-gray-500 font-mono">{@packet.device_identifier || "Unknown"}</span>
|
<span class="text-gray-500 font-mono">
|
||||||
|
{@packet.device_identifier || "Unknown"}
|
||||||
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"callsign" => callsign}, _session, socket) do
|
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||||
normalized_callsign = String.upcase(String.trim(callsign))
|
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)
|
weather_packet = get_latest_weather_packet(normalized_callsign)
|
||||||
{start_time, end_time} = default_time_range()
|
{start_time, end_time} = default_time_range()
|
||||||
weather_history = get_weather_history(normalized_callsign, start_time, end_time)
|
weather_history = get_weather_history(normalized_callsign, start_time, end_time)
|
||||||
|
|
@ -47,6 +53,78 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
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
|
defp get_latest_weather_packet(callsign) do
|
||||||
# Get weather packets from the last 7 days to find the most recent one
|
# Get weather packets from the last 7 days to find the most recent one
|
||||||
end_time = DateTime.utc_now()
|
end_time = DateTime.utc_now()
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||||
{get_weather_field_zero(@weather_packet, :rain_since_midnight)} in
|
{get_weather_field_zero(@weather_packet, :rain_since_midnight)} in
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<dt class="text-xs font-medium text-gray-500">Raw Packet</dt>
|
<dt class="text-xs font-medium text-gray-500">Raw Packet</dt>
|
||||||
<dd class="mt-1 text-xs font-mono text-gray-900 break-all">
|
<dd class="mt-1 text-xs font-mono text-gray-900 break-all">
|
||||||
|
|
@ -163,7 +163,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="mt-2 text-xs text-gray-500">
|
<div class="mt-2 text-xs text-gray-500">
|
||||||
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
|
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ defmodule Aprsme.Repo.Migrations.RecreateNotifyPacketsInsertFunction do
|
||||||
BEGIN
|
BEGIN
|
||||||
payload := json_build_object(
|
payload := json_build_object(
|
||||||
'id', NEW.id,
|
'id', NEW.id,
|
||||||
|
'sender', NEW.sender,
|
||||||
'lat', NEW.lat,
|
'lat', NEW.lat,
|
||||||
'lon', NEW.lon,
|
'lon', NEW.lon,
|
||||||
'inserted_at', NEW.inserted_at
|
'inserted_at', NEW.inserted_at
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Reference in a new issue