The weather view was crashing with CaseClauseError when receiving numeric values (like 83.0) from the database instead of strings. Also improved the UI to only show weather fields that have valid data. Changes: - Added support for numeric values in format_weather_value function - Added has_weather_field? helper to check for valid weather data - Updated template to conditionally display weather fields - Fields with nil, "N/A", or empty values are now hidden - Wind field shows direction and speed independently if available This prevents crashes and provides a cleaner UI by only showing available weather data instead of displaying N/A values. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
255 lines
8.2 KiB
Elixir
255 lines
8.2 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 """
|
|
Checks if a weather field has a valid value (not nil, "N/A", or empty).
|
|
"""
|
|
def has_weather_field?(packet, field) do
|
|
value = PacketUtils.get_weather_field(packet, field)
|
|
|
|
case value do
|
|
nil -> false
|
|
"N/A" -> false
|
|
"" -> false
|
|
_ -> true
|
|
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 value do
|
|
nil ->
|
|
nil
|
|
|
|
"N/A" ->
|
|
nil
|
|
|
|
value when is_number(value) ->
|
|
case @weather_formatters[key] do
|
|
{formatter, separator} ->
|
|
{converted_value, unit} = formatter.(value, locale)
|
|
"#{converted_value}#{separator}#{unit}"
|
|
|
|
nil ->
|
|
"#{value}"
|
|
end
|
|
|
|
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 ->
|
|
nil
|
|
end
|
|
|
|
nil ->
|
|
"#{value}"
|
|
end
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
end
|