aioambient/util/climate_utils.py
"""Climate utility functions."""
from __future__ import annotations
from math import log, sqrt
from typing import cast
MAGNUS_A = 17.27
MAGNUS_B = 237.7
class ClimateUtils:
"""Climate utility functions."""
@staticmethod
def convert_celsius_to_fahrenheit(celsius: float) -> float:
"""Convert celsius to fahrenheit.
Args:
----
celsius: Temperature measured in Celsius.
Returns:
-------
Converted temperature measured in Fahrenheit.
"""
return celsius * 9.0 / 5.0 + 32.0
@staticmethod
def convert_fahrenheit_to_celsius(fahrenheit: float) -> float:
"""Convert fahrenheit to celsius.
Args:
----
fahrenheit: Temperature measured in Fahrenheit.
Returns:
-------
Converted temperature measured in Celsius.
"""
return (fahrenheit - 32.0) * 5.0 / 9.0
@staticmethod
def convert_kph_to_mph(kph: float) -> float:
"""Convert kilometer per hour to miles per hour.
Args:
----
kph: Speed measured in kilometers per hour.
Returns:
-------
Converted speed measured in miles per hour.
"""
return kph * 0.621371192
@staticmethod
def dew_point_celsius(temp_celsius: float, humidity: float) -> float:
"""Calculate the dew point in Celsius.
Args:
----
temp_celsius: Temperature measured in Celsius.
humidity: Relative humidity measured in percent.
Returns:
-------
Calculated dew point measured in Celsius.
"""
g = MAGNUS_A * temp_celsius / (MAGNUS_B + temp_celsius) + log(humidity / 100.0)
return MAGNUS_B * g / (MAGNUS_A - g)
@staticmethod
def dew_point_fahrenheit(
temp_fahrenheit: float | None, humidity: float | None
) -> float | None:
"""Calculate the dew point in Fahrenheit.
Args:
----
temp_fahrenheit: Temperature measured in Fahrenheit.
humidity: Relative humidity measured in percent.
Returns:
-------
Calculated dew point measured in Fahrenheit.
"""
if temp_fahrenheit is None or humidity is None:
return None
humidity = min(humidity, 100)
return ClimateUtils.convert_celsius_to_fahrenheit(
ClimateUtils.dew_point_celsius(
ClimateUtils.convert_fahrenheit_to_celsius(temp_fahrenheit),
humidity,
)
)
@staticmethod
def _wind_chill_fahrenheit(temp_fahrenheit: float, wind_speed_mph: float) -> float:
"""Calculate the wind chill temperature in Fahrenheit.
Args:
----
temp_fahrenheit: Temperature measured in Fahrenheit.
wind_speed_mph: Wind speed measured in miles per hour.
Returns:
-------
Calculated wind chill measured in Fahrenheit.
"""
return cast(
float,
35.74
+ 0.6215 * temp_fahrenheit
- 35.75 * wind_speed_mph**0.16
+ 0.4275 * temp_fahrenheit * wind_speed_mph**0.16,
)
@staticmethod
def _heat_index_fahrenheit(temp_fahrenheit: float, humidity: float) -> float:
"""Calculate the heat index temperature in Fahrenheit.
Args:
----
temp_fahrenheit: Temperature measured in Fahrenheit.
humidity: Relative humidity measured in percent.
Returns:
-------
Calculated heat index measured in Fahrenheit.
"""
result = 0.5 * (
temp_fahrenheit + 61 + (temp_fahrenheit - 68) * 1.2 + humidity * 0.094
)
if temp_fahrenheit < 80:
return result
heat_index_base = (
-42.379
+ 2.04901523 * temp_fahrenheit
+ 10.14333127 * humidity
+ -0.22475541 * temp_fahrenheit * humidity
+ -0.00683783 * temp_fahrenheit * temp_fahrenheit
+ -0.05481717 * humidity * humidity
+ 0.00122874 * temp_fahrenheit * temp_fahrenheit * humidity
+ 0.00085282 * temp_fahrenheit * humidity * humidity
+ -0.00000199 * temp_fahrenheit * temp_fahrenheit * humidity * humidity
)
if humidity < 13 and temp_fahrenheit <= 112:
return heat_index_base - (13 - humidity) / 4 * sqrt(
(17 - (abs(temp_fahrenheit - 95))) / 17
)
if humidity > 85 and temp_fahrenheit <= 87:
return heat_index_base + (humidity - 85) / 10 * ((87 - temp_fahrenheit) / 5)
return heat_index_base
@staticmethod
def feels_like_fahrenheit(
temp_fahrenheit: float | None,
humidity: float | None,
wind_speed_mph: float | None,
) -> float | None:
"""Calculate the feels like temperature in Fahrenheit.
Args:
----
temp_fahrenheit: Temperature measured in Fahrenheit.
humidity: Relative humidity measured in percent.
wind_speed_mph: Wind speed measured in miles per hour.
Returns:
-------
Calculated feels like temperature measured in Fahrenheit.
"""
if temp_fahrenheit is None or humidity is None or wind_speed_mph is None:
return None
if temp_fahrenheit < 50 and wind_speed_mph > 3:
return ClimateUtils._wind_chill_fahrenheit(temp_fahrenheit, wind_speed_mph)
if temp_fahrenheit > 68:
return ClimateUtils._heat_index_fahrenheit(temp_fahrenheit, humidity)
return temp_fahrenheit
@staticmethod
def feels_like_celsius(
temp_celsius: float | None, humidity: float | None, wind_speed_kph: float | None
) -> float | None:
"""Calculate the feels like temperature in Celsius.
Args:
----
temp_celsius: Temperature measured in Celsius.
humidity: Relative humidity measured in percent.
wind_speed_kph: Wind speed measured in kilometers per hour.
Returns:
-------
Calculated feels like temperature measured in Celsius.
"""
if temp_celsius is None or humidity is None or wind_speed_kph is None:
return None
temp_fahrenheit = ClimateUtils.convert_celsius_to_fahrenheit(temp_celsius)
wind_speed_mph = ClimateUtils.convert_kph_to_mph(wind_speed_kph)
return ClimateUtils.convert_fahrenheit_to_celsius(
# Result cannot be None, so cast it to float to avoid the mypy error:
cast(
float,
ClimateUtils.feels_like_fahrenheit(
temp_fahrenheit, humidity, wind_speed_mph
),
)
)