"""Typing Helpers for Home Assistant.""" from __future__ import annotations from homeassistant.const import ( LENGTH_CENTIMETERS, LENGTH_FEET, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, LENGTH_MILLIMETERS, LENGTH_YARD, MASS_GRAMS, MASS_KILOGRAMS, MASS_MICROGRAMS, MASS_MILLIGRAMS, MASS_OUNCES, MASS_POUNDS, POWER_KILO_WATT, POWER_WATT, PRESSURE_BAR, PRESSURE_CBAR, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_KPA, PRESSURE_MBAR, PRESSURE_MMHG, PRESSURE_PA, PRESSURE_PSI, SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR, SPEED_KNOTS, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, UNIT_NOT_RECOGNIZED_TEMPLATE, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, VOLUME_GALLONS, VOLUME_LITERS, VOLUME_MILLILITERS, UnitOfEnergy, UnitOfVolumetricFlux, ) from homeassistant.exceptions import HomeAssistantError # Distance conversion constants _MM_TO_M = 0.001 # 1 mm = 0.001 m _CM_TO_M = 0.01 # 1 cm = 0.01 m _KM_TO_M = 1000 # 1 km = 1000 m _IN_TO_M = 0.0254 # 1 inch = 0.0254 m _FOOT_TO_M = _IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m) _YARD_TO_M = _FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m) _MILE_TO_M = _YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m) _NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m # Duration conversion constants _HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds _DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds # Mass conversion constants _POUND_TO_G = 453.59237 _OUNCE_TO_G = _POUND_TO_G / 16 # Pressure conversion constants _STANDARD_GRAVITY = 9.80665 _MERCURY_DENSITY = 13.5951 # Volume conversion constants _L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³ _ML_TO_CUBIC_METER = 0.001 * _L_TO_CUBIC_METER # 1 mL = 0.001 L _GALLON_TO_CUBIC_METER = 231 * pow(_IN_TO_M, 3) # US gallon is 231 cubic inches _FLUID_OUNCE_TO_CUBIC_METER = _GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon _CUBIC_FOOT_TO_CUBIC_METER = pow(_FOOT_TO_M, 3) class BaseUnitConverter: """Define the format of a conversion utility.""" UNIT_CLASS: str NORMALIZED_UNIT: str VALID_UNITS: set[str] _UNIT_CONVERSION: dict[str, float] @classmethod def convert(cls, value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" if from_unit == to_unit: return value try: from_ratio = cls._UNIT_CONVERSION[from_unit] except KeyError as err: raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, cls.UNIT_CLASS) ) from err try: to_ratio = cls._UNIT_CONVERSION[to_unit] except KeyError as err: raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) from err new_value = value / from_ratio return new_value * to_ratio @classmethod def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float: """Get unit ratio between units of measurement.""" return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit] class DistanceConverter(BaseUnitConverter): """Utility to convert distance values.""" UNIT_CLASS = "distance" NORMALIZED_UNIT = LENGTH_METERS _UNIT_CONVERSION: dict[str, float] = { LENGTH_METERS: 1, LENGTH_MILLIMETERS: 1 / _MM_TO_M, LENGTH_CENTIMETERS: 1 / _CM_TO_M, LENGTH_KILOMETERS: 1 / _KM_TO_M, LENGTH_INCHES: 1 / _IN_TO_M, LENGTH_FEET: 1 / _FOOT_TO_M, LENGTH_YARD: 1 / _YARD_TO_M, LENGTH_MILES: 1 / _MILE_TO_M, } VALID_UNITS = { LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_FEET, LENGTH_METERS, LENGTH_CENTIMETERS, LENGTH_MILLIMETERS, LENGTH_INCHES, LENGTH_YARD, } class EnergyConverter(BaseUnitConverter): """Utility to convert energy values.""" UNIT_CLASS = "energy" NORMALIZED_UNIT = UnitOfEnergy.KILO_WATT_HOUR _UNIT_CONVERSION: dict[str, float] = { UnitOfEnergy.WATT_HOUR: 1 * 1000, UnitOfEnergy.KILO_WATT_HOUR: 1, UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000, UnitOfEnergy.GIGA_JOULE: 3.6 / 1000, } VALID_UNITS = { UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.GIGA_JOULE, } class MassConverter(BaseUnitConverter): """Utility to convert mass values.""" UNIT_CLASS = "mass" NORMALIZED_UNIT = MASS_GRAMS _UNIT_CONVERSION: dict[str, float] = { MASS_MICROGRAMS: 1 * 1000 * 1000, MASS_MILLIGRAMS: 1 * 1000, MASS_GRAMS: 1, MASS_KILOGRAMS: 1 / 1000, MASS_OUNCES: 1 / _OUNCE_TO_G, MASS_POUNDS: 1 / _POUND_TO_G, } VALID_UNITS = { MASS_GRAMS, MASS_KILOGRAMS, MASS_MILLIGRAMS, MASS_MICROGRAMS, MASS_OUNCES, MASS_POUNDS, } class PowerConverter(BaseUnitConverter): """Utility to convert power values.""" UNIT_CLASS = "power" NORMALIZED_UNIT = POWER_WATT _UNIT_CONVERSION: dict[str, float] = { POWER_WATT: 1, POWER_KILO_WATT: 1 / 1000, } VALID_UNITS = { POWER_WATT, POWER_KILO_WATT, } class PressureConverter(BaseUnitConverter): """Utility to convert pressure values.""" UNIT_CLASS = "pressure" NORMALIZED_UNIT = PRESSURE_PA _UNIT_CONVERSION: dict[str, float] = { PRESSURE_PA: 1, PRESSURE_HPA: 1 / 100, PRESSURE_KPA: 1 / 1000, PRESSURE_BAR: 1 / 100000, PRESSURE_CBAR: 1 / 1000, PRESSURE_MBAR: 1 / 100, PRESSURE_INHG: 1 / (_IN_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY), PRESSURE_PSI: 1 / 6894.757, PRESSURE_MMHG: 1 / (_MM_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY), } VALID_UNITS = { PRESSURE_PA, PRESSURE_HPA, PRESSURE_KPA, PRESSURE_BAR, PRESSURE_CBAR, PRESSURE_MBAR, PRESSURE_INHG, PRESSURE_PSI, PRESSURE_MMHG, } class SpeedConverter(BaseUnitConverter): """Utility to convert speed values.""" UNIT_CLASS = "speed" NORMALIZED_UNIT = SPEED_METERS_PER_SECOND _UNIT_CONVERSION: dict[str, float] = { UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M, UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: _HRS_TO_SECS / _MM_TO_M, SPEED_FEET_PER_SECOND: 1 / _FOOT_TO_M, SPEED_KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M, SPEED_KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M, SPEED_METERS_PER_SECOND: 1, SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M, } VALID_UNITS = { UnitOfVolumetricFlux.INCHES_PER_DAY, UnitOfVolumetricFlux.INCHES_PER_HOUR, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR, SPEED_KNOTS, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, } class TemperatureConverter(BaseUnitConverter): """Utility to convert temperature values.""" UNIT_CLASS = "temperature" NORMALIZED_UNIT = TEMP_CELSIUS VALID_UNITS = { TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, } _UNIT_CONVERSION = { TEMP_CELSIUS: 1.0, TEMP_FAHRENHEIT: 1.8, TEMP_KELVIN: 1.0, } @classmethod def convert(cls, value: float, from_unit: str, to_unit: str) -> float: """Convert a temperature from one unit to another. eg. 10°C will return 50°F For converting an interval between two temperatures, please use `convert_interval` instead. """ # We cannot use the implementation from BaseUnitConverter here because the temperature # units do not use the same floor: 0°C, 0°F and 0K do not align if from_unit == to_unit: return value if from_unit == TEMP_CELSIUS: if to_unit == TEMP_FAHRENHEIT: return cls._celsius_to_fahrenheit(value) if to_unit == TEMP_KELVIN: return cls._celsius_to_kelvin(value) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_FAHRENHEIT: if to_unit == TEMP_CELSIUS: return cls._fahrenheit_to_celsius(value) if to_unit == TEMP_KELVIN: return cls._celsius_to_kelvin(cls._fahrenheit_to_celsius(value)) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_KELVIN: if to_unit == TEMP_CELSIUS: return cls._kelvin_to_celsius(value) if to_unit == TEMP_FAHRENHEIT: return cls._celsius_to_fahrenheit(cls._kelvin_to_celsius(value)) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, cls.UNIT_CLASS) ) @classmethod def convert_interval(cls, interval: float, from_unit: str, to_unit: str) -> float: """Convert a temperature interval from one unit to another. eg. a 10°C interval (10°C to 20°C) will return a 18°F (50°F to 68°F) interval For converting a temperature value, please use `convert` as this method skips floor adjustment. """ # We use BaseUnitConverter implementation here because we are only interested # in the ratio between the units. return super().convert(interval, from_unit, to_unit) @classmethod def _fahrenheit_to_celsius(cls, fahrenheit: float) -> float: """Convert a temperature in Fahrenheit to Celsius.""" return (fahrenheit - 32.0) / 1.8 @classmethod def _kelvin_to_celsius(cls, kelvin: float) -> float: """Convert a temperature in Kelvin to Celsius.""" return kelvin - 273.15 @classmethod def _celsius_to_fahrenheit(cls, celsius: float) -> float: """Convert a temperature in Celsius to Fahrenheit.""" return celsius * 1.8 + 32.0 @classmethod def _celsius_to_kelvin(cls, celsius: float) -> float: """Convert a temperature in Celsius to Kelvin.""" return celsius + 273.15 class VolumeConverter(BaseUnitConverter): """Utility to convert volume values.""" UNIT_CLASS = "volume" NORMALIZED_UNIT = VOLUME_CUBIC_METERS # Units in terms of m³ _UNIT_CONVERSION: dict[str, float] = { VOLUME_LITERS: 1 / _L_TO_CUBIC_METER, VOLUME_MILLILITERS: 1 / _ML_TO_CUBIC_METER, VOLUME_GALLONS: 1 / _GALLON_TO_CUBIC_METER, VOLUME_FLUID_OUNCE: 1 / _FLUID_OUNCE_TO_CUBIC_METER, VOLUME_CUBIC_METERS: 1, VOLUME_CUBIC_FEET: 1 / _CUBIC_FOOT_TO_CUBIC_METER, } VALID_UNITS = { VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET, }