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
16 changes: 16 additions & 0 deletions src/accessiweather/thermal_comfort.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
APPARENT_TEMPERATURE_SOLAR_ALLOWANCE_PER_DEGREE_F = 0.15
HEAT_INDEX_MIN_TEMP_F = 80.0
HEAT_INDEX_MIN_HUMIDITY = 40
WIND_CHILL_MAX_TEMP_F = 50.0
WIND_CHILL_MIN_WIND_MPH = 3.0


@dataclass(frozen=True)
Expand Down Expand Up @@ -115,6 +117,20 @@ def calculate_heat_index_f(temperature_f: float, humidity: int | float) -> float
return heat_index


def calculate_wind_chill_f(temperature_f: float, wind_speed_mph: float) -> float | None:
"""Calculate the standard NWS wind chill in Fahrenheit when applicable."""
if temperature_f > WIND_CHILL_MAX_TEMP_F or wind_speed_mph <= WIND_CHILL_MIN_WIND_MPH:
return None

wind_factor = wind_speed_mph**0.16
return (
35.74
+ (0.6215 * temperature_f)
- (35.75 * wind_factor)
+ (0.4275 * temperature_f * wind_factor)
)


def warm_apparent_temperature_is_coherent(
temperature_f: float | None,
humidity: int | float | None,
Expand Down
23 changes: 20 additions & 3 deletions src/accessiweather/weather_client_nws_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
normalize_pressure_pair,
normalize_temperature_pair,
)
from .thermal_comfort import sanitize_thermal_comfort_readings
from .thermal_comfort import (
calculate_heat_index_f,
calculate_wind_chill_f,
sanitize_thermal_comfort_readings,
)
from .weather_client_nws_common import * # noqa: F403
from .weather_client_parsers import normalize_pressure

Expand Down Expand Up @@ -81,13 +85,26 @@ def parse_nws_current_conditions(
props.get("heatIndex", {}).get("unitCode") or "wmoUnit:degC",
)

derived_wind_chill_f = None
if wind_chill.fahrenheit is None and temperature.fahrenheit is not None and wind_speed_mph is not None:
derived_wind_chill_f = calculate_wind_chill_f(temperature.fahrenheit, wind_speed_mph)

derived_heat_index_f = None
if heat_index.fahrenheit is None and temperature.fahrenheit is not None and humidity is not None:
derived_heat_index_f = calculate_heat_index_f(temperature.fahrenheit, humidity)

derived_feels_like_f = (
derived_wind_chill_f if derived_wind_chill_f is not None else derived_heat_index_f
)

comfort = sanitize_thermal_comfort_readings(
temperature_f=temperature.fahrenheit,
temperature_c=temperature.celsius,
humidity=humidity,
wind_chill_f=wind_chill.fahrenheit,
feels_like_f=derived_feels_like_f,
wind_chill_f=wind_chill.fahrenheit if wind_chill.fahrenheit is not None else derived_wind_chill_f,
wind_chill_c=wind_chill.celsius,
heat_index_f=heat_index.fahrenheit,
heat_index_f=heat_index.fahrenheit if heat_index.fahrenheit is not None else derived_heat_index_f,
heat_index_c=heat_index.celsius,
)

Expand Down
43 changes: 43 additions & 0 deletions tests/test_nws_current_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

from accessiweather.thermal_comfort import calculate_heat_index_f, calculate_wind_chill_f
from accessiweather.weather_client_nws import parse_nws_current_conditions


Expand Down Expand Up @@ -42,3 +43,45 @@ def test_parse_nws_current_conditions_normalizes_measurements_to_current_model()
assert current.heat_index_f is None
assert current.condition == "Mostly Cloudy"
assert current.uv_index == 2.5


def test_parse_nws_current_conditions_derives_wind_chill_when_field_missing():
payload = {
"properties": {
"temperature": {"value": 0.0, "unitCode": "wmoUnit:degC"},
"relativeHumidity": {"value": 72.4, "unitCode": "wmoUnit:percent"},
"windSpeed": {"value": 16.09344, "unitCode": "wmoUnit:km_h-1"},
"windChill": {"value": None, "unitCode": "wmoUnit:degC"},
"heatIndex": {"value": None, "unitCode": "wmoUnit:degC"},
"textDescription": "Mostly Cloudy",
}
}

current = parse_nws_current_conditions(payload)

expected = calculate_wind_chill_f(32.0, 10.0)
assert expected is not None
assert current.feels_like_f == pytest.approx(expected, abs=0.1)
assert current.wind_chill_f == pytest.approx(expected, abs=0.1)
assert current.heat_index_f is None


def test_parse_nws_current_conditions_derives_heat_index_when_field_missing():
payload = {
"properties": {
"temperature": {"value": 32.2222222, "unitCode": "wmoUnit:degC"},
"relativeHumidity": {"value": 70.0, "unitCode": "wmoUnit:percent"},
"windSpeed": {"value": 7.0, "unitCode": "wmoUnit:mi_h-1"},
"windChill": {"value": None, "unitCode": "wmoUnit:degC"},
"heatIndex": {"value": None, "unitCode": "wmoUnit:degC"},
"textDescription": "Hot",
}
}

current = parse_nws_current_conditions(payload)

expected = calculate_heat_index_f(90.0, 70)
assert expected is not None
assert current.feels_like_f == pytest.approx(expected, abs=0.1)
assert current.heat_index_f == pytest.approx(expected, abs=0.1)
assert current.wind_chill_f is None
Loading