aprs.me/lib/aprsme_web/live/weather_live/callsign_view.ex

332 lines
12 KiB
Elixir

defmodule AprsmeWeb.WeatherLive.CallsignView do
@moduledoc false
use AprsmeWeb, :live_view
use Gettext, backend: AprsmeWeb.Gettext
import Phoenix.LiveView, only: [push_event: 3, connected?: 1]
alias Aprsme.Packets
alias AprsmeWeb.MapLive.PacketUtils
alias AprsmeWeb.WeatherUnits
@impl true
def mount(%{"callsign" => callsign}, _session, socket) do
normalized_callsign = String.upcase(String.trim(callsign))
# Subscribe to weather-specific topic for targeted updates
if connected?(socket) do
weather_topic = "weather:#{normalized_callsign}"
Phoenix.PubSub.subscribe(Aprsme.PubSub, weather_topic)
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)
# Get locale from socket assigns (set by LocaleHook)
locale = Map.get(socket.assigns, :locale, "en")
unit_labels = WeatherUnits.unit_labels(locale)
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
# Convert units based on locale
{temp_value, _} = WeatherUnits.format_temperature(pkt.temperature, locale)
{dew_value, _} = if dew_point, do: WeatherUnits.format_temperature(dew_point, locale), else: {nil, nil}
{wind_speed_value, _} = WeatherUnits.format_wind_speed(pkt.wind_speed, locale)
{rain_1h_value, _} = WeatherUnits.format_rain(pkt.rain_1h, locale)
{rain_24h_value, _} = WeatherUnits.format_rain(pkt.rain_24h, locale)
{rain_since_midnight_value, _} = WeatherUnits.format_rain(pkt.rain_since_midnight, locale)
%{
timestamp: pkt.received_at,
temperature: temp_value,
dew_point: dew_value,
humidity: pkt.humidity,
pressure: pkt.pressure,
wind_direction: pkt.wind_direction,
wind_speed: wind_speed_value,
rain_1h: rain_1h_value,
rain_24h: rain_24h_value,
rain_since_midnight: rain_since_midnight_value,
luminosity: pkt.luminosity
}
end)
|> Jason.encode!()
# Add chart labels for translation with locale-aware units
chart_labels = %{
temp_title: gettext("Temperature & Dew Point (%{unit})", unit: unit_labels.temperature),
temp_label: gettext("Temperature (%{unit})", unit: unit_labels.temperature),
dew_label: gettext("Dew Point (%{unit})", unit: unit_labels.temperature),
humidity_title: gettext("Humidity (%)"),
humidity_label: gettext("Humidity (%)"),
pressure_title: gettext("Pressure (mb)"),
pressure_label: gettext("Pressure (mb)"),
wind_title: gettext("Wind (%{unit})", unit: unit_labels.wind_speed),
wind_label: gettext("Wind Speed (%{unit})", unit: unit_labels.wind_speed),
gust_label: gettext("Wind Gust (%{unit})", unit: unit_labels.wind_speed),
time: gettext("Time"),
degf: unit_labels.temperature,
percent: gettext("%"),
mb: gettext("mb")
}
socket =
socket
|> assign(:callsign, normalized_callsign)
|> assign(:weather_packet, weather_packet)
|> assign(:page_title, "Weather for #{normalized_callsign}")
|> assign(:weather_history, weather_history)
|> assign(:weather_history_json, weather_history_json)
|> assign(:chart_labels, chart_labels)
{:ok, socket}
end
@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)
# Get locale from socket assigns
locale = Map.get(socket.assigns, :locale, "en")
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
# Convert units based on locale
{temp_value, _} = WeatherUnits.format_temperature(pkt.temperature, locale)
{dew_value, _} = if dew_point, do: WeatherUnits.format_temperature(dew_point, locale), else: {nil, nil}
{wind_speed_value, _} = WeatherUnits.format_wind_speed(pkt.wind_speed, locale)
{rain_1h_value, _} = WeatherUnits.format_rain(pkt.rain_1h, locale)
{rain_24h_value, _} = WeatherUnits.format_rain(pkt.rain_24h, locale)
{rain_since_midnight_value, _} = WeatherUnits.format_rain(pkt.rain_since_midnight, locale)
%{
timestamp: pkt.received_at,
temperature: temp_value,
dew_point: dew_value,
humidity: pkt.humidity,
pressure: pkt.pressure,
wind_direction: pkt.wind_direction,
wind_speed: wind_speed_value,
rain_1h: rain_1h_value,
rain_24h: rain_24h_value,
rain_since_midnight: rain_since_midnight_value,
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)
# Push event to update all charts with new data
socket = push_event(socket, "update_weather_charts", %{weather_history: weather_history_json})
{:noreply, socket}
end
# Keep the old handler for backward compatibility
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)
# Get locale from socket assigns
locale = Map.get(socket.assigns, :locale, "en")
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
# Convert units based on locale
{temp_value, _} = WeatherUnits.format_temperature(pkt.temperature, locale)
{dew_value, _} = if dew_point, do: WeatherUnits.format_temperature(dew_point, locale), else: {nil, nil}
{wind_speed_value, _} = WeatherUnits.format_wind_speed(pkt.wind_speed, locale)
{rain_1h_value, _} = WeatherUnits.format_rain(pkt.rain_1h, locale)
{rain_24h_value, _} = WeatherUnits.format_rain(pkt.rain_24h, locale)
{rain_since_midnight_value, _} = WeatherUnits.format_rain(pkt.rain_since_midnight, locale)
%{
timestamp: pkt.received_at,
temperature: temp_value,
dew_point: dew_value,
humidity: pkt.humidity,
pressure: pkt.pressure,
wind_direction: pkt.wind_direction,
wind_speed: wind_speed_value,
rain_1h: rain_1h_value,
rain_24h: rain_24h_value,
rain_since_midnight: rain_since_midnight_value,
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)
# Push event to update all charts with new data
socket = push_event(socket, "update_weather_charts", %{weather_history: 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
Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field ->
value = Map.get(packet, field) || Map.get(packet, to_string(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()
start_time = DateTime.add(end_time, -7 * 24 * 3600, :second)
callsign
|> Packets.get_weather_packets(start_time, end_time, %{limit: 1})
|> List.first()
end
defp get_weather_history(callsign, start_time, end_time) do
Packets.get_weather_packets(callsign, start_time, end_time, %{limit: 500})
end
defp default_time_range do
now = DateTime.utc_now()
{DateTime.add(now, -48 * 3600, :second), now}
end
defp calc_dew_point(temp, humidity) when is_number(temp) and is_number(humidity) do
temp - (100 - humidity) / 5
end
defp calc_dew_point(_, _), do: nil
@doc """
Gets weather field value, returning "0" instead of "N/A" for missing numeric data.
"""
def get_weather_field_zero(packet, key) do
value = PacketUtils.get_weather_field(packet, key)
# Return "0" for missing numeric fields, keep other values as-is
case value do
"N/A" -> "0"
_ -> value
end
end
@doc """
Formats weather values with appropriate units based on locale.
"""
def format_weather_value(packet, key, locale) do
value = PacketUtils.get_weather_field(packet, key)
case {key, value} do
{:temperature, value} when is_binary(value) and value != "N/A" ->
case Float.parse(value) do
{num_value, _} ->
{converted_value, unit} = WeatherUnits.format_temperature(num_value, locale)
"#{converted_value}#{unit}"
:error ->
value
end
{:wind_speed, value} when is_binary(value) and value != "N/A" ->
case Float.parse(value) do
{num_value, _} ->
{converted_value, unit} = WeatherUnits.format_wind_speed(num_value, locale)
"#{converted_value} #{unit}"
:error ->
value
end
{:wind_gust, value} when is_binary(value) and value != "N/A" ->
case Float.parse(value) do
{num_value, _} ->
{converted_value, unit} = WeatherUnits.format_wind_speed(num_value, locale)
"#{converted_value} #{unit}"
:error ->
value
end
{:rain_1h, value} when is_binary(value) and value != "N/A" ->
case Float.parse(value) do
{num_value, _} ->
{converted_value, unit} = WeatherUnits.format_rain(num_value, locale)
"#{converted_value} #{unit}"
:error ->
value
end
{:rain_24h, value} when is_binary(value) and value != "N/A" ->
case Float.parse(value) do
{num_value, _} ->
{converted_value, unit} = WeatherUnits.format_rain(num_value, locale)
"#{converted_value} #{unit}"
:error ->
value
end
{:rain_since_midnight, value} when is_binary(value) and value != "N/A" ->
case Float.parse(value) do
{num_value, _} ->
{converted_value, unit} = WeatherUnits.format_rain(num_value, locale)
"#{converted_value} #{unit}"
:error ->
value
end
_ ->
case value do
"N/A" -> "N/A"
_ -> "#{value}"
end
end
end
end