From 65180751d33259144ebd821a00ffc56c26681677 Mon Sep 17 00:00:00 2001 From: Orinks <38449772+Orinks@users.noreply.github.com> Date: Tue, 7 Jul 2026 03:32:58 +0200 Subject: [PATCH] fix: add wind speed unit preference (#703) --- CHANGELOG.md | 1 + src/accessiweather/app_lifecycle.py | 2 + .../presentation/current_conditions.py | 23 +++++++++-- .../display/presentation/forecast.py | 9 ++++- .../display/presentation/forecast_hourly.py | 15 ++++++- .../presentation/measurement_formatters.py | 20 +++++++++- .../display/weather_presenter.py | 30 +++++++++++--- src/accessiweather/models/config_constants.py | 1 + .../models/config_serialization.py | 2 + src/accessiweather/models/config_settings.py | 1 + .../models/config_validation.py | 5 +++ src/accessiweather/taskbar_icon_updater.py | 25 +++++++++++- .../ui/dialogs/settings_dialog_handlers.py | 11 ++++++ .../ui/dialogs/settings_tabs/display.py | 39 +++++++++++++++++++ src/accessiweather/units.py | 19 +++++++++ tests/test_settings_dialog_tray_text.py | 16 ++++++++ tests/test_unit_utils.py | 30 ++++++++++++++ 17 files changed, 233 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b31897888..1d321bbfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/accessiweather/app_lifecycle.py b/src/accessiweather/app_lifecycle.py index 30e972cb1..df7fc2b55 100644 --- a/src/accessiweather/app_lifecycle.py +++ b/src/accessiweather/app_lifecycle.py @@ -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), ) @@ -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"), ) diff --git a/src/accessiweather/display/presentation/current_conditions.py b/src/accessiweather/display/presentation/current_conditions.py index e8dba7af0..0cc6ad5c3 100644 --- a/src/accessiweather/display/presentation/current_conditions.py +++ b/src/accessiweather/display/presentation/current_conditions.py @@ -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 @@ -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: @@ -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}")) @@ -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.""" @@ -410,6 +424,7 @@ def build_current_conditions( show_visibility, show_uv_index, unit_system=unit_system, + wind_unit_system=wind_unit_system, ) ) diff --git a/src/accessiweather/display/presentation/forecast.py b/src/accessiweather/display/presentation/forecast.py index a98380bc6..d60e8a4f4 100644 --- a/src/accessiweather/display/presentation/forecast.py +++ b/src/accessiweather/display/presentation/forecast.py @@ -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 @@ -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}" @@ -183,6 +185,7 @@ def build_forecast( unit_pref, settings=settings, location_timezone=location_timezone, + wind_unit_system=wind_unit_system, ) else: hourly = [] @@ -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 diff --git a/src/accessiweather/display/presentation/forecast_hourly.py b/src/accessiweather/display/presentation/forecast_hourly.py index f31e4c5fb..f9d61a91e 100644 --- a/src/accessiweather/display/presentation/forecast_hourly.py +++ b/src/accessiweather/display/presentation/forecast_hourly.py @@ -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 @@ -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 @@ -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( @@ -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 ) diff --git a/src/accessiweather/display/presentation/measurement_formatters.py b/src/accessiweather/display/presentation/measurement_formatters.py index d3755859a..fdf56e4e6 100644 --- a/src/accessiweather/display/presentation/measurement_formatters.py +++ b/src/accessiweather/display/presentation/measurement_formatters.py @@ -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: @@ -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 @@ -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: diff --git a/src/accessiweather/display/weather_presenter.py b/src/accessiweather/display/weather_presenter.py index b490c0bad..9601a5ca6 100644 --- a/src/accessiweather/display/weather_presenter.py +++ b/src/accessiweather/display/weather_presenter.py @@ -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 @@ -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( @@ -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 @@ -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 @@ -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 @@ -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( @@ -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, @@ -203,6 +212,7 @@ def present_forecast( confidence=confidence, mobility_briefing=mobility_briefing, marine=marine, + wind_unit_system=wind_unit_system, ) def present_alerts( @@ -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( @@ -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, ) @@ -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, @@ -266,6 +279,7 @@ def _build_forecast( marine=marine, confidence=confidence, mobility_briefing=mobility_briefing, + wind_unit_system=wind_unit_system, ) def _build_alerts( @@ -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: diff --git a/src/accessiweather/models/config_constants.py b/src/accessiweather/models/config_constants.py index 5cd4251fe..63b03d033 100644 --- a/src/accessiweather/models/config_constants.py +++ b/src/accessiweather/models/config_constants.py @@ -64,6 +64,7 @@ "pirate_weather_api_key", # Display preferences "round_values", + "wind_speed_unit", "enable_alerts", "minimize_to_tray", "minimize_on_startup", diff --git a/src/accessiweather/models/config_serialization.py b/src/accessiweather/models/config_serialization.py index 5d521b748..401b6ba69 100644 --- a/src/accessiweather/models/config_serialization.py +++ b/src/accessiweather/models/config_serialization.py @@ -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, @@ -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), diff --git a/src/accessiweather/models/config_settings.py b/src/accessiweather/models/config_settings.py index 3419679db..eb0800be5 100644 --- a/src/accessiweather/models/config_settings.py +++ b/src/accessiweather/models/config_settings.py @@ -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 diff --git a/src/accessiweather/models/config_validation.py b/src/accessiweather/models/config_validation.py index dfcfd34ac..ad8bcac1b 100644 --- a/src/accessiweather/models/config_validation.py +++ b/src/accessiweather/models/config_validation.py @@ -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: diff --git a/src/accessiweather/taskbar_icon_updater.py b/src/accessiweather/taskbar_icon_updater.py index 21707cc62..bad45c446 100644 --- a/src/accessiweather/taskbar_icon_updater.py +++ b/src/accessiweather/taskbar_icon_updater.py @@ -13,7 +13,11 @@ from typing import TYPE_CHECKING, Any from .format_string_parser import FormatStringParser -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.temperature_utils import ( TemperatureUnit, celsius_to_fahrenheit, @@ -63,6 +67,7 @@ def __init__( dynamic_enabled: bool = True, format_string: str = DEFAULT_TOOLTIP_FORMAT, temperature_unit: str = "both", + wind_speed_unit: str = "auto", verbosity_level: str = "standard", round_values: bool = False, ): @@ -71,6 +76,7 @@ def __init__( self.dynamic_enabled = dynamic_enabled self.format_string = format_string self.temperature_unit = temperature_unit + self.wind_speed_unit = wind_speed_unit self.verbosity_level = verbosity_level self.round_values = round_values self.parser = FormatStringParser() @@ -82,6 +88,7 @@ def update_settings( dynamic_enabled: bool | None = None, format_string: str | None = None, temperature_unit: str | None = None, + wind_speed_unit: str | None = None, verbosity_level: str | None = None, round_values: bool | None = None, ) -> None: @@ -94,6 +101,8 @@ def update_settings( self.format_string = format_string if temperature_unit is not None: self.temperature_unit = temperature_unit + if wind_speed_unit is not None: + self.wind_speed_unit = wind_speed_unit if verbosity_level is not None: self.verbosity_level = verbosity_level if round_values is not None: @@ -265,6 +274,18 @@ def _resolve_display_unit_system(self, location: Any | None = None) -> str | Non unit_system = resolve_display_unit_system(self.temperature_unit, resolved_location) return unit_system.value if unit_system is not None else None + def _resolve_wind_display_unit_system(self, location: Any | None = None) -> str | None: + """Resolve the effective wind-speed display system for the active location.""" + resolved_location = location + if resolved_location is None and getattr(self, "_active_weather_data", None) is not None: + resolved_location = getattr(self._active_weather_data, "location", None) + unit_system = resolve_wind_display_unit_system( + self.wind_speed_unit, + temperature_preference=self.temperature_unit, + location=resolved_location, + ) + return unit_system.value if unit_system is not None else None + def _format_numeric(self, value: float | int | None, suffix: str) -> str: """Format a numeric value with optional suffix.""" if value is None: @@ -311,7 +332,7 @@ def _format_wind_speed(self, current: Any) -> str: unit=self._resolve_temperature_unit(), wind_speed_kph=getattr(current, "wind_speed_kph", None), precision=precision, - unit_system=self._resolve_display_unit_system(), + unit_system=self._resolve_wind_display_unit_system(), ) def _format_pressure(self, current: Any) -> str: diff --git a/src/accessiweather/ui/dialogs/settings_dialog_handlers.py b/src/accessiweather/ui/dialogs/settings_dialog_handlers.py index 906d9f034..69215f1a2 100644 --- a/src/accessiweather/ui/dialogs/settings_dialog_handlers.py +++ b/src/accessiweather/ui/dialogs/settings_dialog_handlers.py @@ -484,6 +484,7 @@ def _on_edit_taskbar_text_format(self, event): dynamic_enabled=self._controls["taskbar_icon_dynamic_enabled"].GetValue(), format_string=self._controls["taskbar_icon_text_format"].GetValue(), temperature_unit=self._get_selected_temperature_unit(), + wind_speed_unit=self._get_selected_wind_speed_unit(), ) dialog = TrayTextFormatDialog( @@ -508,6 +509,16 @@ def _get_selected_temperature_unit(self) -> str: return "both" return temp_values[selection] + def _get_selected_wind_speed_unit(self) -> str: + """Return the wind speed unit selection currently shown in the dialog.""" + if hasattr(self, "_display_tab"): + return self._display_tab.get_selected_wind_speed_unit() + wind_speed_values = ["auto", "mph", "km/h", "m/s"] + selection = self._controls["wind_speed_unit"].GetSelection() + if selection < 0 or selection >= len(wind_speed_values): + return "auto" + return wind_speed_values[selection] + def _get_ai_model_preference(self) -> str: """Get the AI model preference based on UI selection.""" selection = self._controls["ai_model"].GetSelection() diff --git a/src/accessiweather/ui/dialogs/settings_tabs/display.py b/src/accessiweather/ui/dialogs/settings_tabs/display.py index 1dc611eea..56534855b 100644 --- a/src/accessiweather/ui/dialogs/settings_tabs/display.py +++ b/src/accessiweather/ui/dialogs/settings_tabs/display.py @@ -10,6 +10,18 @@ _TEMP_VALUES = ["auto", "f", "c", "both"] _TEMP_MAP = {"auto": 0, "f": 1, "fahrenheit": 1, "c": 2, "celsius": 2, "both": 3} +_WIND_SPEED_UNIT_VALUES = ["auto", "mph", "km/h", "m/s"] +_WIND_SPEED_UNIT_MAP = { + "auto": 0, + "mph": 1, + "mi/h": 1, + "km/h": 2, + "kmh": 2, + "kph": 2, + "m/s": 3, + "mps": 3, + "ms": 3, +} _FORECAST_DURATION_VALUES = [3, 5, 7, 10, 14, 15] _FORECAST_DURATION_MAP = {3: 0, 5: 1, 7: 2, 10: 3, 14: 4, 15: 5} _FORECAST_TIME_REF_VALUES = ["location", "user_local"] @@ -79,6 +91,20 @@ def create(self, page_label: str = "Display"): ], ), ) + controls["wind_speed_unit"] = self.dialog.add_labeled_control_row( + panel, + temperature_section, + "Wind speed units:", + lambda parent: wx.Choice( + parent, + choices=[ + "Match temperature setting/location", + "Miles per hour (mph)", + "Kilometers per hour (km/h)", + "Meters per second (m/s)", + ], + ), + ) controls["round_values"] = wx.CheckBox( panel, label="Show values as whole numbers when possible", @@ -315,6 +341,8 @@ def load(self, settings): temp_unit = getattr(settings, "temperature_unit", "both") controls["temp_unit"].SetSelection(_TEMP_MAP.get(temp_unit, 3)) + wind_speed_unit = getattr(settings, "wind_speed_unit", "auto") + controls["wind_speed_unit"].SetSelection(_WIND_SPEED_UNIT_MAP.get(wind_speed_unit, 0)) controls["show_dewpoint"].SetValue(getattr(settings, "show_dewpoint", True)) controls["show_visibility"].SetValue(getattr(settings, "show_visibility", True)) @@ -368,6 +396,9 @@ def save(self) -> dict: controls = self.dialog._controls return { "temperature_unit": _TEMP_VALUES[controls["temp_unit"].GetSelection()], + "wind_speed_unit": _WIND_SPEED_UNIT_VALUES[ + controls["wind_speed_unit"].GetSelection() + ], "show_dewpoint": controls["show_dewpoint"].GetValue(), "show_visibility": controls["show_visibility"].GetValue(), "show_uv_index": controls["show_uv_index"].GetValue(), @@ -404,11 +435,19 @@ def get_selected_temperature_unit(self) -> str: return "both" return _TEMP_VALUES[selection] + def get_selected_wind_speed_unit(self) -> str: + """Return the wind speed unit selection currently shown in the dialog.""" + selection = self.dialog._controls["wind_speed_unit"].GetSelection() + if selection < 0 or selection >= len(_WIND_SPEED_UNIT_VALUES): + return "auto" + return _WIND_SPEED_UNIT_VALUES[selection] + def setup_accessibility(self): """Set accessibility names for Display tab controls.""" controls = self.dialog._controls names = { "temp_unit": "Temperature units", + "wind_speed_unit": "Wind speed units", "show_dewpoint": "Show dew point", "show_visibility": "Show visibility", "show_uv_index": "Show UV index", diff --git a/src/accessiweather/units.py b/src/accessiweather/units.py index 406eada53..d8b46b903 100644 --- a/src/accessiweather/units.py +++ b/src/accessiweather/units.py @@ -61,3 +61,22 @@ def resolve_display_unit_system( if normalized == "auto": return resolve_auto_unit_system(location) return None + + +def resolve_wind_display_unit_system( + preference: str | None, + *, + temperature_preference: str | None = None, + location: Location | None = None, +) -> DisplayUnitSystem | None: + """Resolve a stored wind-speed preference to an explicit display system.""" + normalized = (preference or "auto").strip().lower() + if normalized in {"mph", "mi/h"}: + return DisplayUnitSystem.US + if normalized in {"km/h", "kmh", "kph"}: + return DisplayUnitSystem.CA + if normalized in {"m/s", "mps", "ms", "meter/s", "meters/s", "metre/s", "metres/s"}: + return DisplayUnitSystem.SI + if normalized == "auto": + return resolve_display_unit_system(temperature_preference, location) + return None diff --git a/tests/test_settings_dialog_tray_text.py b/tests/test_settings_dialog_tray_text.py index ab1e01fd0..ad9744aa4 100644 --- a/tests/test_settings_dialog_tray_text.py +++ b/tests/test_settings_dialog_tray_text.py @@ -123,6 +123,7 @@ def test_save_settings_persists_tray_text_fields(): dialog._controls["taskbar_icon_text_enabled"].SetValue(True) dialog._controls["taskbar_icon_dynamic_enabled"].SetValue(False) dialog._controls["taskbar_icon_text_format"].SetValue("{temp}") + dialog._controls["wind_speed_unit"].SetSelection(3) success = dialog._save_settings() @@ -131,6 +132,7 @@ def test_save_settings_persists_tray_text_fields(): assert kwargs["taskbar_icon_text_enabled"] is True assert kwargs["taskbar_icon_dynamic_enabled"] is False assert kwargs["taskbar_icon_text_format"] == "{temp}" + assert kwargs["wind_speed_unit"] == "m/s" def test_load_settings_populates_immediate_alert_popup_opt_in(): @@ -169,6 +171,20 @@ def test_get_selected_temperature_unit_returns_auto_for_first_choice(): assert dialog._get_selected_temperature_unit() == "auto" +def test_get_selected_wind_speed_unit_uses_current_choice(): + dialog = _make_dialog_for_settings(SimpleNamespace()) + dialog._controls["wind_speed_unit"].SetSelection(3) + + assert dialog._get_selected_wind_speed_unit() == "m/s" + + +def test_get_selected_wind_speed_unit_returns_auto_for_first_choice(): + dialog = _make_dialog_for_settings(SimpleNamespace()) + dialog._controls["wind_speed_unit"].SetSelection(0) + + assert dialog._get_selected_wind_speed_unit() == "auto" + + def test_load_settings_populates_saved_location_sort_order(): settings = SimpleNamespace(location_sort_order="nearest_current") dialog = _make_dialog_for_settings(settings) diff --git a/tests/test_unit_utils.py b/tests/test_unit_utils.py index 6cce0fa41..5777ed7b3 100644 --- a/tests/test_unit_utils.py +++ b/tests/test_unit_utils.py @@ -8,6 +8,7 @@ import pytest +from accessiweather.units import DisplayUnitSystem, resolve_wind_display_unit_system from accessiweather.utils.temperature_utils import TemperatureUnit from accessiweather.utils.unit_utils import ( convert_wind_direction_to_cardinal, @@ -64,6 +65,35 @@ def test_format_wind_speed_accepts_legacy_uk2_unit_system(self) -> None: assert result == "10.0 mph" + def test_format_wind_speed_si_unit_system_uses_meters_per_second(self) -> None: + """SI wind formatting should render meters per second.""" + result = format_wind_speed( + 10.0, + unit=TemperatureUnit.CELSIUS, + wind_speed_kph=16.0934, + precision=1, + unit_system="si", + ) + + assert result == "4.5 m/s" + + +class TestResolveWindDisplayUnitSystem: + def test_explicit_meters_per_second_maps_to_si(self) -> None: + assert resolve_wind_display_unit_system("m/s") == DisplayUnitSystem.SI + + def test_auto_falls_back_to_auto_location_unit_system(self) -> None: + location = type("Location", (), {"country_code": "CA"})() + + assert ( + resolve_wind_display_unit_system( + "auto", + temperature_preference="auto", + location=location, + ) + == DisplayUnitSystem.CA + ) + class TestFormatPressure: """Tests for pressure formatting."""