more live updates and chart version update
This commit is contained in:
parent
eaccdf4465
commit
d6417ff16b
9 changed files with 255 additions and 137 deletions
|
|
@ -45,8 +45,171 @@ defmodule Aprsme.EncodingUtils do
|
||||||
def sanitize_string(nil), do: nil
|
def sanitize_string(nil), do: nil
|
||||||
def sanitize_string(other), do: other
|
def sanitize_string(other), do: other
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Converts various types to float for consistent display.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Aprsme.EncodingUtils.to_float(1)
|
||||||
|
1.0
|
||||||
|
iex> Aprsme.EncodingUtils.to_float(1.5)
|
||||||
|
1.5
|
||||||
|
iex> Aprsme.EncodingUtils.to_float("2.3")
|
||||||
|
2.3
|
||||||
|
iex> Aprsme.EncodingUtils.to_float("bad")
|
||||||
|
nil
|
||||||
|
iex> Aprsme.EncodingUtils.to_float(nil)
|
||||||
|
nil
|
||||||
|
"""
|
||||||
|
@spec to_float(any()) :: float() | nil
|
||||||
|
def to_float(value) when is_float(value), do: value
|
||||||
|
def to_float(value) when is_integer(value), do: value * 1.0
|
||||||
|
def to_float(%Decimal{} = value), do: Decimal.to_float(value)
|
||||||
|
|
||||||
|
def to_float(value) when is_binary(value) do
|
||||||
|
case Float.parse(value) do
|
||||||
|
{float, _} -> float
|
||||||
|
:error -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_float(_), do: nil
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Converts various types to Decimal for database storage.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> is_struct(Aprsme.EncodingUtils.to_decimal(1), Decimal)
|
||||||
|
true
|
||||||
|
iex> is_struct(Aprsme.EncodingUtils.to_decimal(1.5), Decimal)
|
||||||
|
true
|
||||||
|
iex> is_struct(Aprsme.EncodingUtils.to_decimal("2.3"), Decimal)
|
||||||
|
true
|
||||||
|
iex> Aprsme.EncodingUtils.to_decimal("bad")
|
||||||
|
nil
|
||||||
|
iex> Aprsme.EncodingUtils.to_decimal(nil)
|
||||||
|
nil
|
||||||
|
"""
|
||||||
|
@spec to_decimal(any()) :: Decimal.t() | nil
|
||||||
|
def to_decimal(%Decimal{} = d), do: d
|
||||||
|
def to_decimal(f) when is_float(f), do: Decimal.from_float(f)
|
||||||
|
def to_decimal(i) when is_integer(i), do: Decimal.new(i)
|
||||||
|
|
||||||
|
def to_decimal(s) when is_binary(s) do
|
||||||
|
case Decimal.parse(s) do
|
||||||
|
{d, _} ->
|
||||||
|
d
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
case Float.parse(s) do
|
||||||
|
{f, _} -> Decimal.from_float(f)
|
||||||
|
:error -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_decimal(_), do: nil
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sanitizes all string fields in packet data before database storage.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Aprsme.EncodingUtils.sanitize_packet_strings(["abc", <<255>>])
|
||||||
|
["abc", "ÿ"]
|
||||||
|
iex> Aprsme.EncodingUtils.sanitize_packet_strings(%{"foo" => <<0, 65, 66, 67>>})
|
||||||
|
%{"foo" => "\0ABC"}
|
||||||
|
iex> Aprsme.EncodingUtils.sanitize_packet_strings(nil)
|
||||||
|
nil
|
||||||
|
"""
|
||||||
|
@spec sanitize_packet_strings(any()) :: any()
|
||||||
|
def sanitize_packet_strings(%DateTime{} = dt), do: dt
|
||||||
|
def sanitize_packet_strings(%NaiveDateTime{} = ndt), do: ndt
|
||||||
|
def sanitize_packet_strings(%_struct{} = struct), do: struct |> Map.from_struct() |> sanitize_packet_strings()
|
||||||
|
def sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1)
|
||||||
|
|
||||||
|
def sanitize_packet_strings(binary) when is_binary(binary) do
|
||||||
|
s = sanitize_string(binary)
|
||||||
|
if is_binary(s), do: s, else: ""
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_packet_strings(other), do: other
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Normalizes data_type field to ensure it's always a string.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Aprsme.EncodingUtils.normalize_data_type(%{data_type: :weather})
|
||||||
|
%{data_type: "weather"}
|
||||||
|
iex> Aprsme.EncodingUtils.normalize_data_type(%{"data_type" => :foo})
|
||||||
|
%{"data_type" => "foo"}
|
||||||
|
iex> Aprsme.EncodingUtils.normalize_data_type(%{data_type: "bar"})
|
||||||
|
%{data_type: "bar"}
|
||||||
|
iex> Aprsme.EncodingUtils.normalize_data_type(%{"data_type" => "baz"})
|
||||||
|
%{"data_type" => "baz"}
|
||||||
|
iex> Aprsme.EncodingUtils.normalize_data_type(%{foo: 1})
|
||||||
|
%{foo: 1}
|
||||||
|
"""
|
||||||
|
@spec normalize_data_type(map()) :: map()
|
||||||
|
def normalize_data_type(%{data_type: data_type} = attrs) when is_atom(data_type) do
|
||||||
|
%{attrs | data_type: to_string(data_type)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_data_type(%{"data_type" => data_type} = attrs) when is_atom(data_type) do
|
||||||
|
%{attrs | "data_type" => to_string(data_type)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_data_type(attrs) when is_map(attrs) do
|
||||||
|
case {Map.has_key?(attrs, :data_type), Map.get(attrs, :data_type)} do
|
||||||
|
{true, data_type} when is_atom(data_type) ->
|
||||||
|
%{attrs | data_type: to_string(data_type)}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
attrs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_data_type(attrs), do: attrs
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
List of weather-related fields used for packet classification.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Aprsme.EncodingUtils.weather_fields() |> Enum.member?(:temperature)
|
||||||
|
true
|
||||||
|
iex> :foo in Aprsme.EncodingUtils.weather_fields()
|
||||||
|
false
|
||||||
|
"""
|
||||||
|
@spec weather_fields() :: [atom()]
|
||||||
|
def weather_fields do
|
||||||
|
[
|
||||||
|
:temperature,
|
||||||
|
:humidity,
|
||||||
|
:wind_speed,
|
||||||
|
:wind_direction,
|
||||||
|
:wind_gust,
|
||||||
|
:pressure,
|
||||||
|
:rain_1h,
|
||||||
|
:rain_24h,
|
||||||
|
:rain_since_midnight,
|
||||||
|
:snow,
|
||||||
|
:luminosity
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sanitizes all string fields in an APRS packet to ensure safe JSON encoding.
|
Sanitizes all string fields in an APRS packet to ensure safe JSON encoding.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> result = Aprsme.EncodingUtils.sanitize_packet(%{"information_field" => <<0, 65, 66, 67>>, "data_extended" => %{"comment" => <<0, 68, 69, 70>>}})
|
||||||
|
iex> result["information_field"] == "\0ABC"
|
||||||
|
true
|
||||||
|
iex> result["data_extended"]["comment"] == "\0DEF"
|
||||||
|
true
|
||||||
"""
|
"""
|
||||||
@spec sanitize_packet(struct() | map()) :: struct() | map()
|
@spec sanitize_packet(struct() | map()) :: struct() | map()
|
||||||
def sanitize_packet(%Aprsme.Packet{} = packet) do
|
def sanitize_packet(%Aprsme.Packet{} = packet) do
|
||||||
|
|
@ -65,6 +228,13 @@ defmodule Aprsme.EncodingUtils do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sanitizes string fields in the data_extended structure.
|
Sanitizes string fields in the data_extended structure.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Aprsme.EncodingUtils.sanitize_data_extended(%{"comment" => <<0, 65, 66, 67>>})
|
||||||
|
%{"comment" => "ABC"}
|
||||||
|
iex> Aprsme.EncodingUtils.sanitize_data_extended(nil)
|
||||||
|
nil
|
||||||
"""
|
"""
|
||||||
@spec sanitize_data_extended(nil | map() | MicE.t() | any()) :: nil | map() | MicE.t() | any()
|
@spec sanitize_data_extended(nil | map() | MicE.t() | any()) :: nil | map() | MicE.t() | any()
|
||||||
def sanitize_data_extended(nil), do: nil
|
def sanitize_data_extended(nil), do: nil
|
||||||
|
|
@ -106,6 +276,7 @@ defmodule Aprsme.EncodingUtils do
|
||||||
|> :binary.bin_to_list()
|
|> :binary.bin_to_list()
|
||||||
|> Enum.map(&Integer.to_string(&1, 16))
|
|> Enum.map(&Integer.to_string(&1, 16))
|
||||||
|> Enum.map_join("", &String.pad_leading(&1, 2, "0"))
|
|> Enum.map_join("", &String.pad_leading(&1, 2, "0"))
|
||||||
|
|> String.upcase()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -115,7 +286,6 @@ defmodule Aprsme.EncodingUtils do
|
||||||
|
|
||||||
iex> Aprsme.EncodingUtils.encoding_info("Hello")
|
iex> Aprsme.EncodingUtils.encoding_info("Hello")
|
||||||
%{valid_utf8: true, byte_count: 5, char_count: 5}
|
%{valid_utf8: true, byte_count: 5, char_count: 5}
|
||||||
|
|
||||||
iex> Aprsme.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>)
|
iex> Aprsme.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>)
|
||||||
%{valid_utf8: false, byte_count: 5, invalid_at: 2}
|
%{valid_utf8: false, byte_count: 5, invalid_at: 2}
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -457,11 +457,7 @@ defmodule Aprsme.Is do
|
||||||
|
|
||||||
# Normalize data_type to ensure proper storage
|
# Normalize data_type to ensure proper storage
|
||||||
@spec normalize_data_type(map()) :: map()
|
@spec normalize_data_type(map()) :: map()
|
||||||
defp normalize_data_type(%{data_type: data_type} = attrs) when is_atom(data_type) do
|
defp normalize_data_type(attrs), do: Aprsme.EncodingUtils.normalize_data_type(attrs)
|
||||||
%{attrs | data_type: to_string(data_type)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp normalize_data_type(attrs), do: attrs
|
|
||||||
|
|
||||||
@spec update_packet_stats(map(), integer()) :: map()
|
@spec update_packet_stats(map(), integer()) :: map()
|
||||||
defp update_packet_stats(stats, current_time) do
|
defp update_packet_stats(stats, current_time) do
|
||||||
|
|
|
||||||
|
|
@ -347,20 +347,7 @@ defmodule Aprsme.Packet do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp put_weather_fields(map, data_extended) do
|
defp put_weather_fields(map, data_extended) do
|
||||||
weather_fields = [
|
Enum.reduce(Aprsme.EncodingUtils.weather_fields(), map, fn field, acc ->
|
||||||
:temperature,
|
|
||||||
:humidity,
|
|
||||||
:wind_speed,
|
|
||||||
:wind_direction,
|
|
||||||
:wind_gust,
|
|
||||||
:pressure,
|
|
||||||
:rain_1h,
|
|
||||||
:rain_24h,
|
|
||||||
:rain_since_midnight,
|
|
||||||
:snow
|
|
||||||
]
|
|
||||||
|
|
||||||
Enum.reduce(weather_fields, map, fn field, acc ->
|
|
||||||
value = data_extended[field] || data_extended[to_string(field)]
|
value = data_extended[field] || data_extended[to_string(field)]
|
||||||
maybe_put(acc, field, value)
|
maybe_put(acc, field, value)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -325,36 +325,11 @@ defmodule Aprsme.PacketConsumer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sanitize_packet_strings(%DateTime{} = dt), do: dt
|
defp sanitize_packet_strings(value), do: Aprsme.EncodingUtils.sanitize_packet_strings(value)
|
||||||
defp sanitize_packet_strings(%NaiveDateTime{} = ndt), do: ndt
|
|
||||||
defp sanitize_packet_strings(%_struct{} = struct), do: struct |> Map.from_struct() |> sanitize_packet_strings()
|
|
||||||
defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1)
|
|
||||||
|
|
||||||
defp sanitize_packet_strings(binary) when is_binary(binary) do
|
defp to_float(value), do: Aprsme.EncodingUtils.to_float(value)
|
||||||
s = Aprsme.EncodingUtils.sanitize_string(binary)
|
|
||||||
if is_binary(s), do: s, else: ""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp sanitize_packet_strings(other), do: other
|
defp normalize_data_type(attrs), do: Aprsme.EncodingUtils.normalize_data_type(attrs)
|
||||||
|
|
||||||
defp to_float(value) when is_float(value), do: value
|
|
||||||
defp to_float(value) when is_integer(value), do: value * 1.0
|
|
||||||
defp to_float(%Decimal{} = value), do: Decimal.to_float(value)
|
|
||||||
|
|
||||||
defp to_float(value) when is_binary(value) do
|
|
||||||
case Float.parse(value) do
|
|
||||||
{float, _} -> float
|
|
||||||
:error -> nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_float(_), do: nil
|
|
||||||
|
|
||||||
defp normalize_data_type(%{data_type: data_type} = attrs) when is_atom(data_type) do
|
|
||||||
%{attrs | data_type: to_string(data_type)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp normalize_data_type(attrs), do: attrs
|
|
||||||
|
|
||||||
defp struct_to_map(%{__struct__: struct_type} = struct) do
|
defp struct_to_map(%{__struct__: struct_type} = struct) do
|
||||||
converted_map =
|
converted_map =
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,7 @@ defmodule Aprsme.Packets do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_data_type(attrs) do
|
defp normalize_data_type(attrs) do
|
||||||
case attrs do
|
Aprsme.EncodingUtils.normalize_data_type(attrs)
|
||||||
%{data_type: data_type} when is_atom(data_type) ->
|
|
||||||
Map.put(attrs, :data_type, to_string(data_type))
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
attrs
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp set_received_at(attrs) do
|
defp set_received_at(attrs) do
|
||||||
|
|
@ -566,12 +560,10 @@ defmodule Aprsme.Packets do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Clean packets older than a specific number of days.
|
Clean packets older than a specific number of days.
|
||||||
|
|
||||||
This function allows for more granular cleanup operations by specifying
|
This function allows for more granular cleanup operations by specifying
|
||||||
the exact age threshold for packet deletion.
|
the exact age threshold for packet deletion.
|
||||||
@doc \"""
|
|
||||||
Removes packets older than the specified number of days.
|
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
- `days` - Number of days to keep (packets older than this will be deleted)
|
- `days` - Number of days to keep (packets older than this will be deleted)
|
||||||
|
|
@ -590,51 +582,13 @@ defmodule Aprsme.Packets do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to convert various types to float
|
# Helper to convert various types to float
|
||||||
defp to_float(value) when is_float(value), do: value
|
defp to_float(value), do: Aprsme.EncodingUtils.to_float(value)
|
||||||
defp to_float(value) when is_integer(value), do: value * 1.0
|
|
||||||
defp to_float(%Decimal{} = value), do: Decimal.to_float(value)
|
|
||||||
|
|
||||||
defp to_float(value) when is_binary(value) do
|
|
||||||
case Float.parse(value) do
|
|
||||||
{float, _} -> float
|
|
||||||
:error -> nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_float(_), do: nil
|
|
||||||
|
|
||||||
# Helper to convert various types to Decimal
|
# Helper to convert various types to Decimal
|
||||||
defp to_decimal(%Decimal{} = d), do: d
|
defp to_decimal(value), do: Aprsme.EncodingUtils.to_decimal(value)
|
||||||
defp to_decimal(f) when is_float(f), do: Decimal.from_float(f)
|
|
||||||
defp to_decimal(i) when is_integer(i), do: Decimal.new(i)
|
|
||||||
|
|
||||||
defp to_decimal(s) when is_binary(s) do
|
|
||||||
case Decimal.parse(s) do
|
|
||||||
{d, _} ->
|
|
||||||
d
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
case Float.parse(s) do
|
|
||||||
{f, _} -> Decimal.from_float(f)
|
|
||||||
:error -> nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_decimal(_), do: nil
|
|
||||||
|
|
||||||
# Helper to sanitize all string fields in packet data before database storage
|
# Helper to sanitize all string fields in packet data before database storage
|
||||||
defp sanitize_packet_strings(%DateTime{} = dt), do: dt
|
defp sanitize_packet_strings(value), do: Aprsme.EncodingUtils.sanitize_packet_strings(value)
|
||||||
defp sanitize_packet_strings(%NaiveDateTime{} = ndt), do: ndt
|
|
||||||
defp sanitize_packet_strings(%_struct{} = struct), do: struct |> Map.from_struct() |> sanitize_packet_strings()
|
|
||||||
defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1)
|
|
||||||
|
|
||||||
defp sanitize_packet_strings(binary) when is_binary(binary) do
|
|
||||||
s = Aprsme.EncodingUtils.sanitize_string(binary)
|
|
||||||
if is_binary(s), do: s, else: ""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp sanitize_packet_strings(other), do: other
|
|
||||||
|
|
||||||
# Get packets from last hour only - used to initialize the map
|
# Get packets from last hour only - used to initialize the map
|
||||||
@spec get_last_hour_packets() :: [struct()]
|
@spec get_last_hour_packets() :: [struct()]
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,12 @@ defmodule Aprsme.PostgresNotifier do
|
||||||
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
|
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
|
||||||
case Jason.decode(payload) do
|
case Jason.decode(payload) do
|
||||||
{:ok, packet} ->
|
{:ok, packet} ->
|
||||||
|
# Broadcast to the general packet topic
|
||||||
Phoenix.PubSub.broadcast(Aprsme.PubSub, @packet_topic, {:postgres_packet, packet})
|
Phoenix.PubSub.broadcast(Aprsme.PubSub, @packet_topic, {:postgres_packet, packet})
|
||||||
|
|
||||||
|
# Also broadcast to callsign-specific weather topic if it's a weather packet
|
||||||
|
broadcast_weather_packet_if_relevant(packet)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
:noop
|
:noop
|
||||||
end
|
end
|
||||||
|
|
@ -41,4 +45,25 @@ defmodule Aprsme.PostgresNotifier do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(_msg, state), do: {:noreply, state}
|
def handle_info(_msg, state), do: {:noreply, state}
|
||||||
|
|
||||||
|
defp broadcast_weather_packet_if_relevant(packet) do
|
||||||
|
# Check if this is a weather packet using centralized weather fields
|
||||||
|
has_weather_data =
|
||||||
|
Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field ->
|
||||||
|
value = Map.get(packet, to_string(field))
|
||||||
|
not is_nil(value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if has_weather_data do
|
||||||
|
# Extract callsign from packet
|
||||||
|
callsign = Map.get(packet, "sender") || Map.get(packet, :sender)
|
||||||
|
|
||||||
|
if is_binary(callsign) and callsign != "" do
|
||||||
|
# Normalize callsign and broadcast to weather-specific topic
|
||||||
|
normalized_callsign = String.upcase(String.trim(callsign))
|
||||||
|
weather_topic = "weather:#{normalized_callsign}"
|
||||||
|
Phoenix.PubSub.broadcast(Aprsme.PubSub, weather_topic, {:weather_packet, packet})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -54,25 +54,7 @@ defmodule AprsmeWeb.MapLive.PacketUtils do
|
||||||
"""
|
"""
|
||||||
@spec to_float(any()) :: float()
|
@spec to_float(any()) :: float()
|
||||||
def to_float(value) do
|
def to_float(value) do
|
||||||
case value do
|
Aprsme.EncodingUtils.to_float(value) || 0.0
|
||||||
%Decimal{} = d ->
|
|
||||||
Decimal.to_float(d)
|
|
||||||
|
|
||||||
n when is_float(n) ->
|
|
||||||
n
|
|
||||||
|
|
||||||
n when is_integer(n) ->
|
|
||||||
n * 1.0
|
|
||||||
|
|
||||||
n when is_binary(n) ->
|
|
||||||
case Float.parse(n) do
|
|
||||||
{f, _} -> f
|
|
||||||
:error -> 0.0
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
0.0
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||||
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
|
# Subscribe to weather-specific topic for targeted updates
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
|
weather_topic = "weather:#{normalized_callsign}"
|
||||||
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, weather_topic)
|
||||||
end
|
end
|
||||||
|
|
||||||
weather_packet = get_latest_weather_packet(normalized_callsign)
|
weather_packet = get_latest_weather_packet(normalized_callsign)
|
||||||
|
|
@ -54,6 +55,46 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
def handle_info({:weather_packet, _packet}, socket) do
|
||||||
|
# Update weather data when new weather packet arrives for this callsign
|
||||||
|
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}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Keep the old handler for backward compatibility
|
||||||
def handle_info({:postgres_packet, packet}, socket) do
|
def handle_info({:postgres_packet, packet}, socket) do
|
||||||
# Only update if the packet is for our callsign and contains weather data
|
# 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
|
if packet_matches_callsign?(packet, socket.assigns.callsign) and has_weather_data?(packet) do
|
||||||
|
|
@ -107,20 +148,8 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||||
|
|
||||||
defp has_weather_data?(packet) do
|
defp has_weather_data?(packet) do
|
||||||
# Check if packet contains any weather-related fields
|
# Check if packet contains any weather-related fields
|
||||||
weather_fields = [
|
Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field ->
|
||||||
"temperature",
|
value = Map.get(packet, field) || Map.get(packet, to_string(field))
|
||||||
"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)
|
not is_nil(value)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<script src="https://cdn.plot.ly/plotly-latest.min.js">
|
<script src="https://cdn.plot.ly/plotly-2.29.1.min.js">
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue