Home Assistant Unofficial Reference 2024.12.1
unit_conversion.py
Go to the documentation of this file.
1 """Typing Helpers for Home Assistant."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from functools import lru_cache
7 
8 from homeassistant.const import (
9  CONCENTRATION_PARTS_PER_BILLION,
10  CONCENTRATION_PARTS_PER_MILLION,
11  PERCENTAGE,
12  UNIT_NOT_RECOGNIZED_TEMPLATE,
13  UnitOfArea,
14  UnitOfBloodGlucoseConcentration,
15  UnitOfConductivity,
16  UnitOfDataRate,
17  UnitOfElectricCurrent,
18  UnitOfElectricPotential,
19  UnitOfEnergy,
20  UnitOfInformation,
21  UnitOfLength,
22  UnitOfMass,
23  UnitOfPower,
24  UnitOfPressure,
25  UnitOfSpeed,
26  UnitOfTemperature,
27  UnitOfTime,
28  UnitOfVolume,
29  UnitOfVolumeFlowRate,
30  UnitOfVolumetricFlux,
31 )
32 from homeassistant.exceptions import HomeAssistantError
33 
34 # Distance conversion constants
35 _MM_TO_M = 0.001 # 1 mm = 0.001 m
36 _CM_TO_M = 0.01 # 1 cm = 0.01 m
37 _KM_TO_M = 1000 # 1 km = 1000 m
38 
39 _IN_TO_M = 0.0254 # 1 inch = 0.0254 m
40 _FOOT_TO_M = _IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m)
41 _YARD_TO_M = _FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m)
42 _MILE_TO_M = _YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m)
43 
44 _NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m
45 
46 # Area constants to square meters
47 _CM2_TO_M2 = _CM_TO_M**2 # 1 cm² = 0.0001 m²
48 _MM2_TO_M2 = _MM_TO_M**2 # 1 mm² = 0.000001 m²
49 _KM2_TO_M2 = _KM_TO_M**2 # 1 km² = 1,000,000 m²
50 
51 _IN2_TO_M2 = _IN_TO_M**2 # 1 in² = 0.00064516 m²
52 _FT2_TO_M2 = _FOOT_TO_M**2 # 1 ft² = 0.092903 m²
53 _YD2_TO_M2 = _YARD_TO_M**2 # 1 yd² = 0.836127 m²
54 _MI2_TO_M2 = _MILE_TO_M**2 # 1 mi² = 2,590,000 m²
55 
56 _ACRE_TO_M2 = 66 * 660 * _FT2_TO_M2 # 1 acre = 4,046.86 m²
57 _HECTARE_TO_M2 = 100 * 100 # 1 hectare = 10,000 m²
58 
59 # Duration conversion constants
60 _MIN_TO_SEC = 60 # 1 min = 60 seconds
61 _HRS_TO_MINUTES = 60 # 1 hr = 60 minutes
62 _HRS_TO_SECS = _HRS_TO_MINUTES * _MIN_TO_SEC # 1 hr = 60 minutes = 3600 seconds
63 _DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds
64 
65 # Energy conversion constants
66 _WH_TO_J = 3600 # 1 Wh = 3600 J
67 _WH_TO_CAL = _WH_TO_J / 4.184 # 1 Wh = 860.42065 cal
68 
69 # Mass conversion constants
70 _POUND_TO_G = 453.59237
71 _OUNCE_TO_G = _POUND_TO_G / 16 # 16 ounces to a pound
72 _STONE_TO_G = _POUND_TO_G * 14 # 14 pounds to a stone
73 
74 # Pressure conversion constants
75 _STANDARD_GRAVITY = 9.80665
76 _MERCURY_DENSITY = 13.5951
77 
78 # Volume conversion constants
79 _L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³
80 _ML_TO_CUBIC_METER = 0.001 * _L_TO_CUBIC_METER # 1 mL = 0.001 L
81 _GALLON_TO_CUBIC_METER = 231 * pow(_IN_TO_M, 3) # US gallon is 231 cubic inches
82 _FLUID_OUNCE_TO_CUBIC_METER = _GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon
83 _CUBIC_FOOT_TO_CUBIC_METER = pow(_FOOT_TO_M, 3)
84 
85 
87  """Define the format of a conversion utility."""
88 
89  UNIT_CLASS: str
90  VALID_UNITS: set[str | None]
91 
92  _UNIT_CONVERSION: dict[str | None, float]
93 
94  @classmethod
95  def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float:
96  """Convert one unit of measurement to another."""
97  return cls.converter_factoryconverter_factory(from_unit, to_unit)(value)
98 
99  @classmethod
100  @lru_cache
102  cls, from_unit: str | None, to_unit: str | None
103  ) -> Callable[[float], float]:
104  """Return a function to convert one unit of measurement to another."""
105  if from_unit == to_unit:
106  return lambda value: value
107  from_ratio, to_ratio = cls._get_from_to_ratio_get_from_to_ratio(from_unit, to_unit)
108  return lambda val: (val / from_ratio) * to_ratio
109 
110  @classmethod
112  cls, from_unit: str | None, to_unit: str | None
113  ) -> tuple[float, float]:
114  """Get unit ratio between units of measurement."""
115  unit_conversion = cls._UNIT_CONVERSION
116  try:
117  return unit_conversion[from_unit], unit_conversion[to_unit]
118  except KeyError as err:
119  raise HomeAssistantError(
120  UNIT_NOT_RECOGNIZED_TEMPLATE.format(err.args[0], cls.UNIT_CLASS)
121  ) from err
122 
123  @classmethod
124  @lru_cache
126  cls, from_unit: str | None, to_unit: str | None
127  ) -> Callable[[float | None], float | None]:
128  """Return a function to convert one unit of measurement to another which allows None."""
129  if from_unit == to_unit:
130  return lambda value: value
131  from_ratio, to_ratio = cls._get_from_to_ratio_get_from_to_ratio(from_unit, to_unit)
132  return lambda val: None if val is None else (val / from_ratio) * to_ratio
133 
134  @classmethod
135  @lru_cache
136  def get_unit_ratio(cls, from_unit: str | None, to_unit: str | None) -> float:
137  """Get unit ratio between units of measurement."""
138  from_ratio, to_ratio = cls._get_from_to_ratio_get_from_to_ratio(from_unit, to_unit)
139  return from_ratio / to_ratio
140 
141 
143  """Utility to convert data rate values."""
144 
145  UNIT_CLASS = "data_rate"
146  # Units in terms of bits
147  _UNIT_CONVERSION: dict[str | None, float] = {
148  UnitOfDataRate.BITS_PER_SECOND: 1,
149  UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3,
150  UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6,
151  UnitOfDataRate.GIGABITS_PER_SECOND: 1 / 1e9,
152  UnitOfDataRate.BYTES_PER_SECOND: 1 / 8,
153  UnitOfDataRate.KILOBYTES_PER_SECOND: 1 / 8e3,
154  UnitOfDataRate.MEGABYTES_PER_SECOND: 1 / 8e6,
155  UnitOfDataRate.GIGABYTES_PER_SECOND: 1 / 8e9,
156  UnitOfDataRate.KIBIBYTES_PER_SECOND: 1 / 2**13,
157  UnitOfDataRate.MEBIBYTES_PER_SECOND: 1 / 2**23,
158  UnitOfDataRate.GIBIBYTES_PER_SECOND: 1 / 2**33,
159  }
160  VALID_UNITS = set(UnitOfDataRate)
161 
162 
164  """Utility to convert area values."""
165 
166  UNIT_CLASS = "area"
167  _UNIT_CONVERSION: dict[str | None, float] = {
168  UnitOfArea.SQUARE_METERS: 1,
169  UnitOfArea.SQUARE_CENTIMETERS: 1 / _CM2_TO_M2,
170  UnitOfArea.SQUARE_MILLIMETERS: 1 / _MM2_TO_M2,
171  UnitOfArea.SQUARE_KILOMETERS: 1 / _KM2_TO_M2,
172  UnitOfArea.SQUARE_INCHES: 1 / _IN2_TO_M2,
173  UnitOfArea.SQUARE_FEET: 1 / _FT2_TO_M2,
174  UnitOfArea.SQUARE_YARDS: 1 / _YD2_TO_M2,
175  UnitOfArea.SQUARE_MILES: 1 / _MI2_TO_M2,
176  UnitOfArea.ACRES: 1 / _ACRE_TO_M2,
177  UnitOfArea.HECTARES: 1 / _HECTARE_TO_M2,
178  }
179  VALID_UNITS = set(UnitOfArea)
180 
181 
183  """Utility to convert distance values."""
184 
185  UNIT_CLASS = "distance"
186  _UNIT_CONVERSION: dict[str | None, float] = {
187  UnitOfLength.METERS: 1,
188  UnitOfLength.MILLIMETERS: 1 / _MM_TO_M,
189  UnitOfLength.CENTIMETERS: 1 / _CM_TO_M,
190  UnitOfLength.KILOMETERS: 1 / _KM_TO_M,
191  UnitOfLength.INCHES: 1 / _IN_TO_M,
192  UnitOfLength.FEET: 1 / _FOOT_TO_M,
193  UnitOfLength.YARDS: 1 / _YARD_TO_M,
194  UnitOfLength.MILES: 1 / _MILE_TO_M,
195  UnitOfLength.NAUTICAL_MILES: 1 / _NAUTICAL_MILE_TO_M,
196  }
197  VALID_UNITS = {
198  UnitOfLength.KILOMETERS,
199  UnitOfLength.MILES,
200  UnitOfLength.NAUTICAL_MILES,
201  UnitOfLength.FEET,
202  UnitOfLength.METERS,
203  UnitOfLength.CENTIMETERS,
204  UnitOfLength.MILLIMETERS,
205  UnitOfLength.INCHES,
206  UnitOfLength.YARDS,
207  }
208 
209 
211  """Utility to convert blood glucose concentration values."""
212 
213  UNIT_CLASS = "blood_glucose_concentration"
214  _UNIT_CONVERSION: dict[str | None, float] = {
215  UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER: 18,
216  UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER: 1,
217  }
218  VALID_UNITS = set(UnitOfBloodGlucoseConcentration)
219 
220 
222  """Utility to convert electric current values."""
223 
224  UNIT_CLASS = "conductivity"
225  _UNIT_CONVERSION: dict[str | None, float] = {
226  UnitOfConductivity.MICROSIEMENS_PER_CM: 1,
227  UnitOfConductivity.MILLISIEMENS_PER_CM: 1e-3,
228  UnitOfConductivity.SIEMENS_PER_CM: 1e-6,
229  }
230  VALID_UNITS = set(UnitOfConductivity)
231 
232 
234  """Utility to convert electric current values."""
235 
236  UNIT_CLASS = "electric_current"
237  _UNIT_CONVERSION: dict[str | None, float] = {
238  UnitOfElectricCurrent.AMPERE: 1,
239  UnitOfElectricCurrent.MILLIAMPERE: 1e3,
240  }
241  VALID_UNITS = set(UnitOfElectricCurrent)
242 
243 
245  """Utility to convert electric potential values."""
246 
247  UNIT_CLASS = "voltage"
248  _UNIT_CONVERSION: dict[str | None, float] = {
249  UnitOfElectricPotential.VOLT: 1,
250  UnitOfElectricPotential.MILLIVOLT: 1e3,
251  UnitOfElectricPotential.MICROVOLT: 1e6,
252  }
253  VALID_UNITS = {
254  UnitOfElectricPotential.VOLT,
255  UnitOfElectricPotential.MILLIVOLT,
256  UnitOfElectricPotential.MICROVOLT,
257  }
258 
259 
261  """Utility to convert energy values."""
262 
263  UNIT_CLASS = "energy"
264  _UNIT_CONVERSION: dict[str | None, float] = {
265  UnitOfEnergy.JOULE: _WH_TO_J * 1e3,
266  UnitOfEnergy.KILO_JOULE: _WH_TO_J,
267  UnitOfEnergy.MEGA_JOULE: _WH_TO_J / 1e3,
268  UnitOfEnergy.GIGA_JOULE: _WH_TO_J / 1e6,
269  UnitOfEnergy.WATT_HOUR: 1e3,
270  UnitOfEnergy.KILO_WATT_HOUR: 1,
271  UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1e3,
272  UnitOfEnergy.GIGA_WATT_HOUR: 1 / 1e6,
273  UnitOfEnergy.TERA_WATT_HOUR: 1 / 1e9,
274  UnitOfEnergy.CALORIE: _WH_TO_CAL * 1e3,
275  UnitOfEnergy.KILO_CALORIE: _WH_TO_CAL,
276  UnitOfEnergy.MEGA_CALORIE: _WH_TO_CAL / 1e3,
277  UnitOfEnergy.GIGA_CALORIE: _WH_TO_CAL / 1e6,
278  }
279  VALID_UNITS = set(UnitOfEnergy)
280 
281 
283  """Utility to convert information values."""
284 
285  UNIT_CLASS = "information"
286  # Units in terms of bits
287  _UNIT_CONVERSION: dict[str | None, float] = {
288  UnitOfInformation.BITS: 1,
289  UnitOfInformation.KILOBITS: 1 / 1e3,
290  UnitOfInformation.MEGABITS: 1 / 1e6,
291  UnitOfInformation.GIGABITS: 1 / 1e9,
292  UnitOfInformation.BYTES: 1 / 8,
293  UnitOfInformation.KILOBYTES: 1 / 8e3,
294  UnitOfInformation.MEGABYTES: 1 / 8e6,
295  UnitOfInformation.GIGABYTES: 1 / 8e9,
296  UnitOfInformation.TERABYTES: 1 / 8e12,
297  UnitOfInformation.PETABYTES: 1 / 8e15,
298  UnitOfInformation.EXABYTES: 1 / 8e18,
299  UnitOfInformation.ZETTABYTES: 1 / 8e21,
300  UnitOfInformation.YOTTABYTES: 1 / 8e24,
301  UnitOfInformation.KIBIBYTES: 1 / 2**13,
302  UnitOfInformation.MEBIBYTES: 1 / 2**23,
303  UnitOfInformation.GIBIBYTES: 1 / 2**33,
304  UnitOfInformation.TEBIBYTES: 1 / 2**43,
305  UnitOfInformation.PEBIBYTES: 1 / 2**53,
306  UnitOfInformation.EXBIBYTES: 1 / 2**63,
307  UnitOfInformation.ZEBIBYTES: 1 / 2**73,
308  UnitOfInformation.YOBIBYTES: 1 / 2**83,
309  }
310  VALID_UNITS = set(UnitOfInformation)
311 
312 
314  """Utility to convert mass values."""
315 
316  UNIT_CLASS = "mass"
317  _UNIT_CONVERSION: dict[str | None, float] = {
318  UnitOfMass.MICROGRAMS: 1 * 1000 * 1000,
319  UnitOfMass.MILLIGRAMS: 1 * 1000,
320  UnitOfMass.GRAMS: 1,
321  UnitOfMass.KILOGRAMS: 1 / 1000,
322  UnitOfMass.OUNCES: 1 / _OUNCE_TO_G,
323  UnitOfMass.POUNDS: 1 / _POUND_TO_G,
324  UnitOfMass.STONES: 1 / _STONE_TO_G,
325  }
326  VALID_UNITS = {
327  UnitOfMass.GRAMS,
328  UnitOfMass.KILOGRAMS,
329  UnitOfMass.MILLIGRAMS,
330  UnitOfMass.MICROGRAMS,
331  UnitOfMass.OUNCES,
332  UnitOfMass.POUNDS,
333  UnitOfMass.STONES,
334  }
335 
336 
338  """Utility to convert power values."""
339 
340  UNIT_CLASS = "power"
341  _UNIT_CONVERSION: dict[str | None, float] = {
342  UnitOfPower.WATT: 1,
343  UnitOfPower.KILO_WATT: 1 / 1000,
344  UnitOfPower.MEGA_WATT: 1 / 1e6,
345  UnitOfPower.GIGA_WATT: 1 / 1e9,
346  UnitOfPower.TERA_WATT: 1 / 1e12,
347  }
348  VALID_UNITS = {
349  UnitOfPower.WATT,
350  UnitOfPower.KILO_WATT,
351  UnitOfPower.MEGA_WATT,
352  UnitOfPower.GIGA_WATT,
353  UnitOfPower.TERA_WATT,
354  }
355 
356 
358  """Utility to convert pressure values."""
359 
360  UNIT_CLASS = "pressure"
361  _UNIT_CONVERSION: dict[str | None, float] = {
362  UnitOfPressure.PA: 1,
363  UnitOfPressure.HPA: 1 / 100,
364  UnitOfPressure.KPA: 1 / 1000,
365  UnitOfPressure.BAR: 1 / 100000,
366  UnitOfPressure.CBAR: 1 / 1000,
367  UnitOfPressure.MBAR: 1 / 100,
368  UnitOfPressure.INHG: 1
369  / (_IN_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
370  UnitOfPressure.PSI: 1 / 6894.757,
371  UnitOfPressure.MMHG: 1
372  / (_MM_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
373  }
374  VALID_UNITS = {
375  UnitOfPressure.PA,
376  UnitOfPressure.HPA,
377  UnitOfPressure.KPA,
378  UnitOfPressure.BAR,
379  UnitOfPressure.CBAR,
380  UnitOfPressure.MBAR,
381  UnitOfPressure.INHG,
382  UnitOfPressure.PSI,
383  UnitOfPressure.MMHG,
384  }
385 
386 
388  """Utility to convert speed values."""
389 
390  UNIT_CLASS = "speed"
391  _UNIT_CONVERSION: dict[str | None, float] = {
392  UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
393  UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
394  UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
395  UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: _HRS_TO_SECS / _MM_TO_M,
396  UnitOfSpeed.FEET_PER_SECOND: 1 / _FOOT_TO_M,
397  UnitOfSpeed.INCHES_PER_SECOND: 1 / _IN_TO_M,
398  UnitOfSpeed.KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M,
399  UnitOfSpeed.KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M,
400  UnitOfSpeed.METERS_PER_SECOND: 1,
401  UnitOfSpeed.MILLIMETERS_PER_SECOND: 1 / _MM_TO_M,
402  UnitOfSpeed.MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M,
403  UnitOfSpeed.BEAUFORT: 1,
404  }
405  VALID_UNITS = {
406  UnitOfVolumetricFlux.INCHES_PER_DAY,
407  UnitOfVolumetricFlux.INCHES_PER_HOUR,
408  UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
409  UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
410  UnitOfSpeed.INCHES_PER_SECOND,
411  UnitOfSpeed.FEET_PER_SECOND,
412  UnitOfSpeed.KILOMETERS_PER_HOUR,
413  UnitOfSpeed.KNOTS,
414  UnitOfSpeed.METERS_PER_SECOND,
415  UnitOfSpeed.MILES_PER_HOUR,
416  UnitOfSpeed.MILLIMETERS_PER_SECOND,
417  UnitOfSpeed.BEAUFORT,
418  }
419 
420  @classmethod
421  @lru_cache
423  cls, from_unit: str | None, to_unit: str | None
424  ) -> Callable[[float], float]:
425  """Return a function to convert a speed from one unit to another."""
426  if from_unit == to_unit:
427  # Return a function that does nothing. This is not
428  # in _converter_factory because we do not want to wrap
429  # it with the None check in converter_factory_allow_none.
430  return lambda value: value
431 
432  return cls._converter_factory_converter_factory(from_unit, to_unit)
433 
434  @classmethod
435  @lru_cache
437  cls, from_unit: str | None, to_unit: str | None
438  ) -> Callable[[float | None], float | None]:
439  """Return a function to convert a speed from one unit to another which allows None."""
440  if from_unit == to_unit:
441  # Return a function that does nothing. This is not
442  # in _converter_factory because we do not want to wrap
443  # it with the None check in this case.
444  return lambda value: value
445 
446  convert = cls._converter_factory_converter_factory(from_unit, to_unit)
447  return lambda value: None if value is None else convert(value)
448 
449  @classmethod
451  cls, from_unit: str | None, to_unit: str | None
452  ) -> Callable[[float], float]:
453  """Convert a speed from one unit to another, eg. 14m/s will return 7Bft."""
454  # We cannot use the implementation from BaseUnitConverter here because the
455  # Beaufort scale is not a constant value to divide or multiply with.
456  if (
457  from_unit not in SpeedConverter.VALID_UNITS
458  or to_unit not in SpeedConverter.VALID_UNITS
459  ):
460  raise HomeAssistantError(
461  UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASSUNIT_CLASS)
462  )
463 
464  if from_unit == UnitOfSpeed.BEAUFORT:
465  to_ratio = cls._UNIT_CONVERSION[to_unit]
466  return lambda val: cls._beaufort_to_ms_beaufort_to_ms(val) * to_ratio
467  if to_unit == UnitOfSpeed.BEAUFORT:
468  from_ratio = cls._UNIT_CONVERSION[from_unit]
469  return lambda val: cls._ms_to_beaufort_ms_to_beaufort(val / from_ratio)
470 
471  from_ratio, to_ratio = cls._get_from_to_ratio_get_from_to_ratio(from_unit, to_unit)
472  return lambda val: (val / from_ratio) * to_ratio
473 
474  @classmethod
475  def _ms_to_beaufort(cls, ms: float) -> float:
476  """Convert a speed in m/s to Beaufort."""
477  return float(round(((ms / 0.836) ** 2) ** (1 / 3)))
478 
479  @classmethod
480  def _beaufort_to_ms(cls, beaufort: float) -> float:
481  """Convert a speed in Beaufort to m/s."""
482  return float(0.836 * beaufort ** (3 / 2))
483 
484 
486  """Utility to convert temperature values."""
487 
488  UNIT_CLASS = "temperature"
489  VALID_UNITS = {
490  UnitOfTemperature.CELSIUS,
491  UnitOfTemperature.FAHRENHEIT,
492  UnitOfTemperature.KELVIN,
493  }
494  _UNIT_CONVERSION = {
495  UnitOfTemperature.CELSIUS: 1.0,
496  UnitOfTemperature.FAHRENHEIT: 1.8,
497  UnitOfTemperature.KELVIN: 1.0,
498  }
499 
500  @classmethod
501  @lru_cache
503  cls, from_unit: str | None, to_unit: str | None
504  ) -> Callable[[float], float]:
505  """Return a function to convert a temperature from one unit to another."""
506  if from_unit == to_unit:
507  # Return a function that does nothing. This is not
508  # in _converter_factory because we do not want to wrap
509  # it with the None check in converter_factory_allow_none.
510  return lambda value: value
511 
512  return cls._converter_factory_converter_factory(from_unit, to_unit)
513 
514  @classmethod
515  @lru_cache
517  cls, from_unit: str | None, to_unit: str | None
518  ) -> Callable[[float | None], float | None]:
519  """Return a function to convert a temperature from one unit to another which allows None."""
520  if from_unit == to_unit:
521  # Return a function that does nothing. This is not
522  # in _converter_factory because we do not want to wrap
523  # it with the None check in this case.
524  return lambda value: value
525  convert = cls._converter_factory_converter_factory(from_unit, to_unit)
526  return lambda value: None if value is None else convert(value)
527 
528  @classmethod
530  cls, from_unit: str | None, to_unit: str | None
531  ) -> Callable[[float], float]:
532  """Convert a temperature from one unit to another.
533 
534  eg. 10°C will return 50°F
535 
536  For converting an interval between two temperatures, please use
537  `convert_interval` instead.
538  """
539  # We cannot use the implementation from BaseUnitConverter here because the
540  # temperature units do not use the same floor: 0°C, 0°F and 0K do not align
541  if from_unit == UnitOfTemperature.CELSIUS:
542  if to_unit == UnitOfTemperature.FAHRENHEIT:
543  return cls._celsius_to_fahrenheit_celsius_to_fahrenheit
544  if to_unit == UnitOfTemperature.KELVIN:
545  return cls._celsius_to_kelvin_celsius_to_kelvin
546  raise HomeAssistantError(
547  UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASSUNIT_CLASS)
548  )
549 
550  if from_unit == UnitOfTemperature.FAHRENHEIT:
551  if to_unit == UnitOfTemperature.CELSIUS:
552  return cls._fahrenheit_to_celsius_fahrenheit_to_celsius
553  if to_unit == UnitOfTemperature.KELVIN:
554  return cls._fahrenheit_to_kelvin_fahrenheit_to_kelvin
555  raise HomeAssistantError(
556  UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASSUNIT_CLASS)
557  )
558 
559  if from_unit == UnitOfTemperature.KELVIN:
560  if to_unit == UnitOfTemperature.CELSIUS:
561  return cls._kelvin_to_celsius_kelvin_to_celsius
562  if to_unit == UnitOfTemperature.FAHRENHEIT:
563  return cls._kelvin_to_fahrenheit_kelvin_to_fahrenheit
564  raise HomeAssistantError(
565  UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASSUNIT_CLASS)
566  )
567  raise HomeAssistantError(
568  UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, cls.UNIT_CLASSUNIT_CLASS)
569  )
570 
571  @classmethod
572  def convert_interval(cls, interval: float, from_unit: str, to_unit: str) -> float:
573  """Convert a temperature interval from one unit to another.
574 
575  eg. a 10°C interval (10°C to 20°C) will return a 18°F (50°F to 68°F) interval
576 
577  For converting a temperature value, please use `convert` as this method
578  skips floor adjustment.
579  """
580  # We use BaseUnitConverter implementation here because we are only interested
581  # in the ratio between the units.
582  return super().converter_factory(from_unit, to_unit)(interval)
583 
584  @classmethod
585  def _kelvin_to_fahrenheit(cls, kelvin: float) -> float:
586  """Convert a temperature in Kelvin to Fahrenheit."""
587  return (kelvin - 273.15) * 1.8 + 32.0
588 
589  @classmethod
590  def _fahrenheit_to_kelvin(cls, fahrenheit: float) -> float:
591  """Convert a temperature in Fahrenheit to Kelvin."""
592  return 273.15 + ((fahrenheit - 32.0) / 1.8)
593 
594  @classmethod
595  def _fahrenheit_to_celsius(cls, fahrenheit: float) -> float:
596  """Convert a temperature in Fahrenheit to Celsius."""
597  return (fahrenheit - 32.0) / 1.8
598 
599  @classmethod
600  def _kelvin_to_celsius(cls, kelvin: float) -> float:
601  """Convert a temperature in Kelvin to Celsius."""
602  return kelvin - 273.15
603 
604  @classmethod
605  def _celsius_to_fahrenheit(cls, celsius: float) -> float:
606  """Convert a temperature in Celsius to Fahrenheit."""
607  return celsius * 1.8 + 32.0
608 
609  @classmethod
610  def _celsius_to_kelvin(cls, celsius: float) -> float:
611  """Convert a temperature in Celsius to Kelvin."""
612  return celsius + 273.15
613 
614 
616  """Utility to convert unitless ratios."""
617 
618  UNIT_CLASS = "unitless"
619  _UNIT_CONVERSION: dict[str | None, float] = {
620  None: 1,
621  CONCENTRATION_PARTS_PER_BILLION: 1000000000,
622  CONCENTRATION_PARTS_PER_MILLION: 1000000,
623  PERCENTAGE: 100,
624  }
625  VALID_UNITS = {
626  None,
627  PERCENTAGE,
628  }
629 
630 
632  """Utility to convert volume values."""
633 
634  UNIT_CLASS = "volume"
635  # Units in terms of m³
636  _UNIT_CONVERSION: dict[str | None, float] = {
637  UnitOfVolume.LITERS: 1 / _L_TO_CUBIC_METER,
638  UnitOfVolume.MILLILITERS: 1 / _ML_TO_CUBIC_METER,
639  UnitOfVolume.GALLONS: 1 / _GALLON_TO_CUBIC_METER,
640  UnitOfVolume.FLUID_OUNCES: 1 / _FLUID_OUNCE_TO_CUBIC_METER,
641  UnitOfVolume.CUBIC_METERS: 1,
642  UnitOfVolume.CUBIC_FEET: 1 / _CUBIC_FOOT_TO_CUBIC_METER,
643  UnitOfVolume.CENTUM_CUBIC_FEET: 1 / (100 * _CUBIC_FOOT_TO_CUBIC_METER),
644  }
645  VALID_UNITS = {
646  UnitOfVolume.LITERS,
647  UnitOfVolume.MILLILITERS,
648  UnitOfVolume.GALLONS,
649  UnitOfVolume.FLUID_OUNCES,
650  UnitOfVolume.CUBIC_METERS,
651  UnitOfVolume.CUBIC_FEET,
652  UnitOfVolume.CENTUM_CUBIC_FEET,
653  }
654 
655 
657  """Utility to convert volume values."""
658 
659  UNIT_CLASS = "volume_flow_rate"
660  # Units in terms of m³/h
661  _UNIT_CONVERSION: dict[str | None, float] = {
662  UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: 1,
663  UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: 1
664  / (_HRS_TO_MINUTES * _CUBIC_FOOT_TO_CUBIC_METER),
665  UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 1
666  / (_HRS_TO_MINUTES * _L_TO_CUBIC_METER),
667  UnitOfVolumeFlowRate.GALLONS_PER_MINUTE: 1
668  / (_HRS_TO_MINUTES * _GALLON_TO_CUBIC_METER),
669  UnitOfVolumeFlowRate.MILLILITERS_PER_SECOND: 1
670  / (_HRS_TO_SECS * _ML_TO_CUBIC_METER),
671  }
672  VALID_UNITS = {
673  UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE,
674  UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
675  UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
676  UnitOfVolumeFlowRate.GALLONS_PER_MINUTE,
677  UnitOfVolumeFlowRate.MILLILITERS_PER_SECOND,
678  }
679 
680 
682  """Utility to convert duration values."""
683 
684  UNIT_CLASS = "duration"
685  _UNIT_CONVERSION: dict[str | None, float] = {
686  UnitOfTime.MICROSECONDS: 1000000,
687  UnitOfTime.MILLISECONDS: 1000,
688  UnitOfTime.SECONDS: 1,
689  UnitOfTime.MINUTES: 1 / _MIN_TO_SEC,
690  UnitOfTime.HOURS: 1 / _HRS_TO_SECS,
691  UnitOfTime.DAYS: 1 / _DAYS_TO_SECS,
692  UnitOfTime.WEEKS: 1 / (7 * _DAYS_TO_SECS),
693  }
694  VALID_UNITS = {
695  UnitOfTime.MICROSECONDS,
696  UnitOfTime.MILLISECONDS,
697  UnitOfTime.SECONDS,
698  UnitOfTime.MINUTES,
699  UnitOfTime.HOURS,
700  UnitOfTime.DAYS,
701  UnitOfTime.WEEKS,
702  }
Callable[[float|None], float|None] converter_factory_allow_none(cls, str|None from_unit, str|None to_unit)
float get_unit_ratio(cls, str|None from_unit, str|None to_unit)
tuple[float, float] _get_from_to_ratio(cls, str|None from_unit, str|None to_unit)
Callable[[float], float] converter_factory(cls, str|None from_unit, str|None to_unit)
float convert(cls, float value, str|None from_unit, str|None to_unit)
Callable[[float], float] _converter_factory(cls, str|None from_unit, str|None to_unit)
Callable[[float], float] converter_factory(cls, str|None from_unit, str|None to_unit)
Callable[[float|None], float|None] converter_factory_allow_none(cls, str|None from_unit, str|None to_unit)
Callable[[float|None], float|None] converter_factory_allow_none(cls, str|None from_unit, str|None to_unit)
float convert_interval(cls, float interval, str from_unit, str to_unit)
Callable[[float], float] _converter_factory(cls, str|None from_unit, str|None to_unit)
Callable[[float], float] converter_factory(cls, str|None from_unit, str|None to_unit)