Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- NOAA Weather Radio stations can now be saved as favorites, with a Favorites finder mode and optional nearby-station lookup from your saved AccessiWeather locations.

### Fixed
- You can now choose meters per second for wind speed in Settings, and AccessiWeather uses that choice consistently in current conditions, forecasts, and tray text.
- Canadian locations now show barometric pressure in kilopascals, such as 101.3 kPa, when using automatic units.
- Current conditions and tray text now omit feels-like or heat-index details when they are unavailable instead of announcing them as `N/A`.
- The Settings startup checkbox now reflects and repairs the actual OS startup registration instead of trusting a stale saved preference.
Expand Down
2 changes: 2 additions & 0 deletions src/accessiweather/app_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def _initialize_taskbar_updater(self) -> None:
dynamic_enabled=getattr(settings, "taskbar_icon_dynamic_enabled", True),
format_string=getattr(settings, "taskbar_icon_text_format", "{temp} {condition}"),
temperature_unit=getattr(settings, "temperature_unit", "both"),
wind_speed_unit=getattr(settings, "wind_speed_unit", "auto"),
verbosity_level=getattr(settings, "verbosity_level", "standard"),
round_values=getattr(settings, "round_values", False),
)
Expand Down Expand Up @@ -428,6 +429,7 @@ def refresh_runtime_settings(self) -> None:
settings, "taskbar_icon_text_format", "{temp} {condition}"
),
temperature_unit=getattr(settings, "temperature_unit", "both"),
wind_speed_unit=getattr(settings, "wind_speed_unit", "auto"),
verbosity_level=getattr(settings, "verbosity_level", "standard"),
)

Expand Down
23 changes: 19 additions & 4 deletions src/accessiweather/display/presentation/current_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def _build_basic_metrics(
show_uv_index: bool,
*,
unit_system: DisplayUnitSystem | str | None = None,
wind_unit_system: DisplayUnitSystem | str | None = None,
) -> list[Metric]:
"""Build basic weather metrics (temperature, feels like, humidity, wind, dewpoint, etc.)."""
# Format temperature with inline feels-like when there's a significant difference
Expand All @@ -93,9 +94,18 @@ def _build_basic_metrics(
if current.humidity is not None:
metrics.append(Metric("Humidity", f"{current.humidity:.0f}%"))

wind_value = format_wind(current, unit_pref, precision=precision, unit_system=unit_system)
resolved_wind_unit_system = wind_unit_system or unit_system
wind_value = format_wind(
current,
unit_pref,
precision=precision,
unit_system=resolved_wind_unit_system,
)
if wind_value:
wind_value = _normalize_metric_wind_units(wind_value, unit_system=unit_system)
wind_value = _normalize_metric_wind_units(
wind_value,
unit_system=resolved_wind_unit_system,
)

gust_value: str | None = None
if current.wind_gust_mph is not None:
Expand All @@ -104,9 +114,12 @@ def _build_basic_metrics(
unit=unit_pref,
wind_speed_kph=current.wind_gust_kph,
precision=0,
unit_system=unit_system,
unit_system=resolved_wind_unit_system,
)
gust_value = _normalize_metric_wind_units(
gust_value,
unit_system=resolved_wind_unit_system,
)
gust_value = _normalize_metric_wind_units(gust_value, unit_system=unit_system)

if wind_value and gust_value:
metrics.append(Metric("Wind", f"{wind_value}, gusting to {gust_value}"))
Expand Down Expand Up @@ -365,6 +378,7 @@ def build_current_conditions(
air_quality: AirQualityPresentation | None = None,
alerts: WeatherAlerts | None = None,
unit_system: DisplayUnitSystem | str | None = None,
wind_unit_system: DisplayUnitSystem | str | None = None,
anomaly_callout: AnomalyCallout | None = None,
) -> CurrentConditionsPresentation:
"""Create a structured presentation for the current weather using helper functions."""
Expand Down Expand Up @@ -410,6 +424,7 @@ def build_current_conditions(
show_visibility,
show_uv_index,
unit_system=unit_system,
wind_unit_system=wind_unit_system,
)
)

Expand Down
9 changes: 8 additions & 1 deletion src/accessiweather/display/presentation/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Location,
MarineForecast,
)
from ...units import DisplayUnitSystem
from ...utils import TemperatureUnit
from ...utils.unit_utils import format_precipitation
from .forecast_hourly import build_hourly_section_text, build_hourly_summary, render_hourly_fallback
Expand Down Expand Up @@ -155,6 +156,7 @@ def build_forecast(
marine: MarineForecast | None = None,
confidence: ForecastConfidence | None = None,
mobility_briefing: str | None = None,
wind_unit_system: DisplayUnitSystem | str | None = None,
) -> ForecastPresentation:
"""Create a structured forecast including optional hourly highlights."""
title = f"Forecast for {location.name}"
Expand Down Expand Up @@ -183,6 +185,7 @@ def build_forecast(
unit_pref,
settings=settings,
location_timezone=location_timezone,
wind_unit_system=wind_unit_system,
)
else:
hourly = []
Expand Down Expand Up @@ -217,7 +220,11 @@ def build_forecast(

for period in selected_periods:
temp_pair = format_forecast_temperature(period, unit_pref, precision)
wind_value = format_period_wind(period, unit_pref) if include_wind else None
wind_value = (
format_period_wind(period, unit_pref, unit_system=wind_unit_system)
if include_wind
else None
)
details = (
period.detailed_forecast
if include_details
Expand Down
15 changes: 13 additions & 2 deletions src/accessiweather/display/presentation/forecast_hourly.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from datetime import tzinfo

from ...models import AppSettings, HourlyForecast
from ...units import DisplayUnitSystem
from ...utils import TemperatureUnit, calculate_dewpoint, format_temperature
from ...utils.unit_utils import format_precipitation, format_wind_speed
from .forecast_time import _resolve_forecast_display_time
Expand All @@ -25,6 +26,7 @@ def build_hourly_summary(
settings: AppSettings | None = None,
*,
location_timezone: tzinfo | None = None,
wind_unit_system: DisplayUnitSystem | str | None = None,
) -> list[HourlyPeriodPresentation]:
"""Generate the next six hours of simplified forecast data."""
round_values = getattr(settings, "round_values", False) if settings else False
Expand Down Expand Up @@ -64,7 +66,11 @@ def build_hourly_summary(
if not period.has_data():
continue
temperature = format_period_temperature(period, unit_pref, precision)
wind = format_hourly_wind(period, unit_pref) if include_wind else None
wind = (
format_hourly_wind(period, unit_pref, unit_system=wind_unit_system)
if include_wind
else None
)

# Use enhanced time formatter with user preferences
display_time = _resolve_forecast_display_time(
Expand Down Expand Up @@ -107,7 +113,12 @@ def build_hourly_summary(
else None
)
gust_val = (
format_wind_speed(period.wind_gust_mph, unit_pref, precision=0)
format_wind_speed(
period.wind_gust_mph,
unit_pref,
precision=0,
unit_system=wind_unit_system,
)
if include_wind_gust and period.wind_gust_mph is not None
else None
)
Expand Down
20 changes: 18 additions & 2 deletions src/accessiweather/display/presentation/measurement_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ def format_forecast_temperature(
def format_period_wind(
period: ForecastPeriod,
unit_pref: TemperatureUnit = TemperatureUnit.FAHRENHEIT,
*,
unit_system: DisplayUnitSystem | str | None = None,
) -> str | None:
"""Return a combined wind string for a forecast period, respecting unit preference."""
if not period.wind_speed and not period.wind_direction:
Expand All @@ -306,7 +308,14 @@ def format_period_wind(
if period.wind_direction:
parts.append(period.wind_direction)
if period.wind_speed_mph is not None:
parts.append(format_wind_speed(period.wind_speed_mph, unit_pref, precision=0))
parts.append(
format_wind_speed(
period.wind_speed_mph,
unit_pref,
precision=0,
unit_system=unit_system,
)
)
elif period.wind_speed:
parts.append(period.wind_speed)
return " ".join(parts) if parts else None
Expand Down Expand Up @@ -470,12 +479,19 @@ def _get_feels_like_reason(current: CurrentConditions, diff_f: float) -> str | N
def format_hourly_wind(
period: HourlyForecastPeriod,
unit_pref: TemperatureUnit = TemperatureUnit.FAHRENHEIT,
*,
unit_system: DisplayUnitSystem | str | None = None,
) -> str | None:
"""Return wind description for hourly periods when both pieces are present."""
if not period.wind_direction:
return None
if period.wind_speed_mph is not None:
speed_str = format_wind_speed(period.wind_speed_mph, unit_pref, precision=0)
speed_str = format_wind_speed(
period.wind_speed_mph,
unit_pref,
precision=0,
unit_system=unit_system,
)
elif period.wind_speed:
speed_str = period.wind_speed
else:
Expand Down
30 changes: 25 additions & 5 deletions src/accessiweather/display/weather_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
WeatherData,
)
from ..services.mobility_briefing import build_mobility_briefing
from ..units import resolve_display_unit_system, resolve_temperature_unit_preference
from ..units import (
resolve_display_unit_system,
resolve_temperature_unit_preference,
resolve_wind_display_unit_system,
)
from ..utils import TemperatureUnit
from .presentation.aviation import build_aviation
from .presentation.environmental import AirQualityPresentation, build_air_quality_panel
Expand Down Expand Up @@ -76,7 +80,9 @@ def __init__(self, settings: AppSettings):

def present(self, weather_data: WeatherData) -> WeatherPresentation:
"""Build a structured presentation for the given weather data."""
unit_pref, unit_system = self._resolve_unit_preferences(weather_data.location)
unit_pref, unit_system, wind_unit_system = self._resolve_unit_preferences(
weather_data.location
)

air_quality_panel = (
build_air_quality_panel(
Expand All @@ -99,6 +105,7 @@ def present(self, weather_data: WeatherData) -> WeatherPresentation:
air_quality=air_quality_panel,
alerts=weather_data.alerts,
unit_system=unit_system,
wind_unit_system=wind_unit_system,
anomaly_callout=getattr(weather_data, "anomaly_callout", None),
)
if weather_data.current
Expand All @@ -113,6 +120,7 @@ def present(self, weather_data: WeatherData) -> WeatherPresentation:
marine=weather_data.marine,
confidence=weather_data.forecast_confidence,
mobility_briefing=build_mobility_briefing(weather_data),
wind_unit_system=wind_unit_system,
)
if weather_data.forecast
else None
Expand Down Expand Up @@ -163,7 +171,7 @@ def present_current(
) -> CurrentConditionsPresentation | None:
if not current or not current.has_data():
return None
unit_pref, unit_system = self._resolve_unit_preferences(location)
unit_pref, unit_system, wind_unit_system = self._resolve_unit_preferences(location)
air_quality_panel = (
build_air_quality_panel(location, environmental, settings=self.settings)
if environmental
Expand All @@ -181,6 +189,7 @@ def present_current(
air_quality=air_quality_panel,
alerts=alerts,
unit_system=unit_system,
wind_unit_system=wind_unit_system,
)

def present_forecast(
Expand All @@ -194,7 +203,7 @@ def present_forecast(
) -> ForecastPresentation | None:
if not forecast or not forecast.has_data():
return None
unit_pref, _unit_system = self._resolve_unit_preferences(location)
unit_pref, _unit_system, wind_unit_system = self._resolve_unit_preferences(location)
return self._build_forecast(
forecast,
hourly_forecast,
Expand All @@ -203,6 +212,7 @@ def present_forecast(
confidence=confidence,
mobility_briefing=mobility_briefing,
marine=marine,
wind_unit_system=wind_unit_system,
)

def present_alerts(
Expand Down Expand Up @@ -230,6 +240,7 @@ def _build_current_conditions(
air_quality: AirQualityPresentation | None = None,
alerts: WeatherAlerts | None = None,
unit_system=None,
wind_unit_system=None,
anomaly_callout=None,
) -> CurrentConditionsPresentation:
return build_current_conditions(
Expand All @@ -244,6 +255,7 @@ def _build_current_conditions(
air_quality=air_quality,
alerts=alerts,
unit_system=unit_system,
wind_unit_system=wind_unit_system,
anomaly_callout=anomaly_callout,
)

Expand All @@ -256,6 +268,7 @@ def _build_forecast(
marine: MarineForecast | None = None,
confidence: ForecastConfidence | None = None,
mobility_briefing: str | None = None,
wind_unit_system=None,
) -> ForecastPresentation:
return build_forecast(
forecast,
Expand All @@ -266,6 +279,7 @@ def _build_forecast(
marine=marine,
confidence=confidence,
mobility_briefing=mobility_briefing,
wind_unit_system=wind_unit_system,
)

def _build_alerts(
Expand Down Expand Up @@ -347,11 +361,17 @@ def _build_source_attribution(

def _resolve_unit_preferences(
self, location: Location
) -> tuple[TemperatureUnit, object | None]:
) -> tuple[TemperatureUnit, object | None, object | None]:
preference = getattr(self.settings, "temperature_unit", "both")
wind_preference = getattr(self.settings, "wind_speed_unit", "auto")
return (
resolve_temperature_unit_preference(preference, location),
resolve_display_unit_system(preference, location),
resolve_wind_display_unit_system(
wind_preference,
temperature_preference=preference,
location=location,
),
)

def _format_timestamp(self, value: datetime) -> str:
Expand Down
1 change: 1 addition & 0 deletions src/accessiweather/models/config_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"pirate_weather_api_key",
# Display preferences
"round_values",
"wind_speed_unit",
"enable_alerts",
"minimize_to_tray",
"minimize_on_startup",
Expand Down
2 changes: 2 additions & 0 deletions src/accessiweather/models/config_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def to_dict(self) -> dict:
settings = cast("AppSettings", self)
return {
"temperature_unit": settings.temperature_unit,
"wind_speed_unit": settings.wind_speed_unit,
"update_interval_minutes": settings.update_interval_minutes,
"enable_alerts": settings.enable_alerts,
"minimize_to_tray": settings.minimize_to_tray,
Expand Down Expand Up @@ -125,6 +126,7 @@ def from_dict(cls, data: dict) -> AppSettings:

settings = settings_cls(
temperature_unit=data.get("temperature_unit", "both"),
wind_speed_unit=data.get("wind_speed_unit", "auto"),
update_interval_minutes=data.get("update_interval_minutes", 10),
enable_alerts=settings_cls._as_bool(data.get("enable_alerts"), True),
minimize_to_tray=settings_cls._as_bool(data.get("minimize_to_tray"), False),
Expand Down
1 change: 1 addition & 0 deletions src/accessiweather/models/config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AppSettings(AppSettingsValidationMixin, AppSettingsSerializationMixin):
"""Application settings."""

temperature_unit: str = "both"
wind_speed_unit: str = "auto"
update_interval_minutes: int = 10
enable_alerts: bool = True
minimize_to_tray: bool = False
Expand Down
5 changes: 5 additions & 0 deletions src/accessiweather/models/config_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ def validate_on_access(self, setting_name: str) -> bool:
if value not in valid_modes:
setattr(settings, setting_name, "local")

elif setting_name == "wind_speed_unit":
valid_units = {"auto", "mph", "km/h", "m/s"}
if value not in valid_units:
setattr(settings, setting_name, "auto")

elif setting_name == "forecast_time_reference":
valid_references = {"location", "user_local"}
if value not in valid_references:
Expand Down
Loading
Loading