- Fix leader election guard test for :undefined pid - Fix cpu_sup.util() pattern matching to handle numeric return - Fix encoding_utils to always return explicit nil when needed - Remove unused finite_float? function - Fix signal handler to match :ok return from :os.set_signal/2 - Remove redundant catch-all pattern in database metrics - Fix Gridsquare pattern matching in info_live template - Remove unnecessary nil check for calculate_course result - Fix get_packet_received_at to not check for nil (always returns DateTime) - Remove redundant catch-all pattern in weather format_weather_value - Fix query builder to use from(p in Packet) instead of bare Packet atom Reduced dialyzer errors from 49 to 6. Remaining warnings are mostly false positives from template compilation and overloaded function specs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
225 lines
7.6 KiB
Elixir
225 lines
7.6 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.Callsign
|
|
alias AprsmeWeb.MapLive.PacketUtils
|
|
alias AprsmeWeb.TimeUtils
|
|
alias AprsmeWeb.WeatherUnits
|
|
|
|
@impl true
|
|
def mount(%{"callsign" => callsign}, _session, socket) do
|
|
normalized_callsign = Callsign.normalize(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 = convert_weather_history_to_json(weather_history, locale)
|
|
|
|
# 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
|
|
# Check if this packet is actually newer than what we have
|
|
current_packet = socket.assigns.weather_packet
|
|
|
|
if should_update_weather?(current_packet, packet) do
|
|
update_weather_data(socket)
|
|
else
|
|
{:noreply, socket}
|
|
end
|
|
end
|
|
|
|
# Keep the old handler for backward compatibility
|
|
def handle_info({:postgres_packet, packet}, socket) do
|
|
# Only process if it's a packet with weather data for this callsign
|
|
has_weather_data =
|
|
not is_nil(packet.temperature) or not is_nil(packet.humidity) or
|
|
not is_nil(packet.pressure) or not is_nil(packet.wind_speed) or
|
|
not is_nil(packet.wind_direction) or not is_nil(packet.rain_1h)
|
|
|
|
if packet.sender == socket.assigns.callsign && has_weather_data do
|
|
handle_info({:weather_packet, packet}, socket)
|
|
else
|
|
{:noreply, socket}
|
|
end
|
|
end
|
|
|
|
def handle_info(_message, socket), do: {:noreply, socket}
|
|
|
|
defp should_update_weather?(nil, _new_packet), do: true
|
|
|
|
defp should_update_weather?(current_packet, new_packet) do
|
|
# Only update if the new packet is actually newer
|
|
DateTime.after?(new_packet.received_at, current_packet.received_at)
|
|
end
|
|
|
|
defp update_weather_data(socket) do
|
|
# Fetch updated data only once
|
|
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 = convert_weather_history_to_json(weather_history, locale)
|
|
|
|
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
|
|
|
|
defp get_latest_weather_packet(callsign) do
|
|
# Use optimized cached query that checks recent data first
|
|
Aprsme.Packets.get_latest_weather_packet(callsign)
|
|
end
|
|
|
|
defp get_weather_history(callsign, start_time, end_time) do
|
|
# Use cached queries to avoid repeated database hits
|
|
Aprsme.Packets.get_weather_packets(callsign, start_time, end_time, %{limit: 500})
|
|
end
|
|
|
|
defp default_time_range do
|
|
TimeUtils.time_range(:last_two_days)
|
|
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
|
|
|
|
defp convert_weather_history_to_json(weather_history, locale) do
|
|
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!()
|
|
end
|
|
|
|
@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
|
|
|
|
# Weather formatters mapping
|
|
@weather_formatters %{
|
|
temperature: {&WeatherUnits.format_temperature/2, ""},
|
|
wind_speed: {&WeatherUnits.format_wind_speed/2, " "},
|
|
wind_gust: {&WeatherUnits.format_wind_speed/2, " "},
|
|
rain_1h: {&WeatherUnits.format_rain/2, " "},
|
|
rain_24h: {&WeatherUnits.format_rain/2, " "},
|
|
rain_since_midnight: {&WeatherUnits.format_rain/2, " "}
|
|
}
|
|
|
|
@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 value do
|
|
"N/A" ->
|
|
"N/A"
|
|
|
|
value when is_binary(value) ->
|
|
case @weather_formatters[key] do
|
|
{formatter, separator} ->
|
|
case Float.parse(value) do
|
|
{num_value, _} ->
|
|
{converted_value, unit} = formatter.(num_value, locale)
|
|
"#{converted_value}#{separator}#{unit}"
|
|
|
|
:error ->
|
|
value
|
|
end
|
|
|
|
nil ->
|
|
"#{value}"
|
|
end
|
|
end
|
|
end
|
|
end
|