Home Assistant Unofficial Reference 2024.12.1
unit_system.py
Go to the documentation of this file.
1 """Unit system helper class and methods."""
2 
3 from __future__ import annotations
4 
5 from numbers import Number
6 from typing import TYPE_CHECKING, Final
7 
8 import voluptuous as vol
9 
10 from homeassistant.const import (
11  ACCUMULATED_PRECIPITATION,
12  AREA,
13  LENGTH,
14  MASS,
15  PRESSURE,
16  TEMPERATURE,
17  UNIT_NOT_RECOGNIZED_TEMPLATE,
18  VOLUME,
19  WIND_SPEED,
20  UnitOfArea,
21  UnitOfLength,
22  UnitOfMass,
23  UnitOfPrecipitationDepth,
24  UnitOfPressure,
25  UnitOfSpeed,
26  UnitOfTemperature,
27  UnitOfVolume,
28  UnitOfVolumetricFlux,
29 )
30 
31 from .unit_conversion import (
32  AreaConverter,
33  DistanceConverter,
34  PressureConverter,
35  SpeedConverter,
36  TemperatureConverter,
37  VolumeConverter,
38 )
39 
40 if TYPE_CHECKING:
41  from homeassistant.components.sensor import SensorDeviceClass
42 
43 _CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial"
44 _CONF_UNIT_SYSTEM_METRIC: Final = "metric"
45 _CONF_UNIT_SYSTEM_US_CUSTOMARY: Final = "us_customary"
46 
47 AREA_UNITS = AreaConverter.VALID_UNITS
48 
49 LENGTH_UNITS = DistanceConverter.VALID_UNITS
50 
51 MASS_UNITS: set[str] = {
52  UnitOfMass.POUNDS,
53  UnitOfMass.OUNCES,
54  UnitOfMass.KILOGRAMS,
55  UnitOfMass.GRAMS,
56 }
57 
58 PRESSURE_UNITS = PressureConverter.VALID_UNITS
59 
60 VOLUME_UNITS = VolumeConverter.VALID_UNITS
61 
62 WIND_SPEED_UNITS = SpeedConverter.VALID_UNITS
63 
64 TEMPERATURE_UNITS: set[str] = {UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS}
65 
66 _VALID_BY_TYPE: dict[str, set[str] | set[str | None]] = {
67  LENGTH: LENGTH_UNITS,
68  ACCUMULATED_PRECIPITATION: LENGTH_UNITS,
69  WIND_SPEED: WIND_SPEED_UNITS,
70  TEMPERATURE: TEMPERATURE_UNITS,
71  MASS: MASS_UNITS,
72  VOLUME: VOLUME_UNITS,
73  PRESSURE: PRESSURE_UNITS,
74  AREA: AREA_UNITS,
75 }
76 
77 
78 def _is_valid_unit(unit: str, unit_type: str) -> bool:
79  """Check if the unit is valid for it's type."""
80  if units := _VALID_BY_TYPE.get(unit_type):
81  return unit in units
82  return False
83 
84 
85 class UnitSystem:
86  """A container for units of measure."""
87 
88  def __init__(
89  self,
90  name: str,
91  *,
92  accumulated_precipitation: UnitOfPrecipitationDepth,
93  area: UnitOfArea,
94  conversions: dict[tuple[SensorDeviceClass | str | None, str | None], str],
95  length: UnitOfLength,
96  mass: UnitOfMass,
97  pressure: UnitOfPressure,
98  temperature: UnitOfTemperature,
99  volume: UnitOfVolume,
100  wind_speed: UnitOfSpeed,
101  ) -> None:
102  """Initialize the unit system object."""
103  errors: str = ", ".join(
104  UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit, unit_type)
105  for unit, unit_type in (
106  (accumulated_precipitation, ACCUMULATED_PRECIPITATION),
107  (area, AREA),
108  (temperature, TEMPERATURE),
109  (length, LENGTH),
110  (wind_speed, WIND_SPEED),
111  (volume, VOLUME),
112  (mass, MASS),
113  (pressure, PRESSURE),
114  )
115  if not _is_valid_unit(unit, unit_type)
116  )
117 
118  if errors:
119  raise ValueError(errors)
120 
121  self._name_name = name
122  self.accumulated_precipitation_unitaccumulated_precipitation_unit = accumulated_precipitation
123  self.area_unitarea_unit = area
124  self.length_unitlength_unit = length
125  self.mass_unitmass_unit = mass
126  self.pressure_unitpressure_unit = pressure
127  self.temperature_unittemperature_unit = temperature
128  self.volume_unitvolume_unit = volume
129  self.wind_speed_unitwind_speed_unit = wind_speed
130  self._conversions_conversions = conversions
131 
132  def temperature(self, temperature: float, from_unit: str) -> float:
133  """Convert the given temperature to this unit system."""
134  if not isinstance(temperature, Number):
135  raise TypeError(f"{temperature!s} is not a numeric value.")
136 
137  return TemperatureConverter.convert(
138  temperature, from_unit, self.temperature_unittemperature_unit
139  )
140 
141  def length(self, length: float | None, from_unit: str) -> float:
142  """Convert the given length to this unit system."""
143  if not isinstance(length, Number):
144  raise TypeError(f"{length!s} is not a numeric value.")
145 
146  # type ignore: https://github.com/python/mypy/issues/7207
147  return DistanceConverter.convert( # type: ignore[unreachable]
148  length, from_unit, self.length_unitlength_unit
149  )
150 
151  def accumulated_precipitation(self, precip: float | None, from_unit: str) -> float:
152  """Convert the given length to this unit system."""
153  if not isinstance(precip, Number):
154  raise TypeError(f"{precip!s} is not a numeric value.")
155 
156  # type ignore: https://github.com/python/mypy/issues/7207
157  return DistanceConverter.convert( # type: ignore[unreachable]
158  precip, from_unit, self.accumulated_precipitation_unitaccumulated_precipitation_unit
159  )
160 
161  def area(self, area: float | None, from_unit: str) -> float:
162  """Convert the given area to this unit system."""
163  if not isinstance(area, Number):
164  raise TypeError(f"{area!s} is not a numeric value.")
165 
166  # type ignore: https://github.com/python/mypy/issues/7207
167  return AreaConverter.convert( # type: ignore[unreachable]
168  area, from_unit, self.area_unitarea_unit
169  )
170 
171  def pressure(self, pressure: float | None, from_unit: str) -> float:
172  """Convert the given pressure to this unit system."""
173  if not isinstance(pressure, Number):
174  raise TypeError(f"{pressure!s} is not a numeric value.")
175 
176  # type ignore: https://github.com/python/mypy/issues/7207
177  return PressureConverter.convert( # type: ignore[unreachable]
178  pressure, from_unit, self.pressure_unitpressure_unit
179  )
180 
181  def wind_speed(self, wind_speed: float | None, from_unit: str) -> float:
182  """Convert the given wind_speed to this unit system."""
183  if not isinstance(wind_speed, Number):
184  raise TypeError(f"{wind_speed!s} is not a numeric value.")
185 
186  # type ignore: https://github.com/python/mypy/issues/7207
187  return SpeedConverter.convert( # type: ignore[unreachable]
188  wind_speed, from_unit, self.wind_speed_unitwind_speed_unit
189  )
190 
191  def volume(self, volume: float | None, from_unit: str) -> float:
192  """Convert the given volume to this unit system."""
193  if not isinstance(volume, Number):
194  raise TypeError(f"{volume!s} is not a numeric value.")
195 
196  # type ignore: https://github.com/python/mypy/issues/7207
197  return VolumeConverter.convert( # type: ignore[unreachable]
198  volume, from_unit, self.volume_unitvolume_unit
199  )
200 
201  def as_dict(self) -> dict[str, str]:
202  """Convert the unit system to a dictionary."""
203  return {
204  LENGTH: self.length_unitlength_unit,
205  ACCUMULATED_PRECIPITATION: self.accumulated_precipitation_unitaccumulated_precipitation_unit,
206  AREA: self.area_unitarea_unit,
207  MASS: self.mass_unitmass_unit,
208  PRESSURE: self.pressure_unitpressure_unit,
209  TEMPERATURE: self.temperature_unittemperature_unit,
210  VOLUME: self.volume_unitvolume_unit,
211  WIND_SPEED: self.wind_speed_unitwind_speed_unit,
212  }
213 
215  self,
216  device_class: SensorDeviceClass | str | None,
217  original_unit: str | None,
218  ) -> str | None:
219  """Return converted unit given a device class or an original unit."""
220  return self._conversions_conversions.get((device_class, original_unit))
221 
222 
223 def get_unit_system(key: str) -> UnitSystem:
224  """Get unit system based on key."""
225  if key == _CONF_UNIT_SYSTEM_US_CUSTOMARY:
226  return US_CUSTOMARY_SYSTEM
227  if key == _CONF_UNIT_SYSTEM_METRIC:
228  return METRIC_SYSTEM
229  raise ValueError(f"`{key}` is not a valid unit system key")
230 
231 
232 def _deprecated_unit_system(value: str) -> str:
233  """Convert deprecated unit system."""
234 
235  if value == _CONF_UNIT_SYSTEM_IMPERIAL:
236  # need to add warning in 2023.1
237  return _CONF_UNIT_SYSTEM_US_CUSTOMARY
238  return value
239 
240 
241 validate_unit_system = vol.All(
242  vol.Lower,
243  _deprecated_unit_system,
244  vol.Any(_CONF_UNIT_SYSTEM_METRIC, _CONF_UNIT_SYSTEM_US_CUSTOMARY),
245 )
246 
247 METRIC_SYSTEM = UnitSystem(
248  _CONF_UNIT_SYSTEM_METRIC,
249  accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
250  conversions={
251  # Force atmospheric pressures to hPa
252  **{
253  ("atmospheric_pressure", unit): UnitOfPressure.HPA
254  for unit in UnitOfPressure
255  if unit != UnitOfPressure.HPA
256  },
257  # Convert non-metric area
258  ("area", UnitOfArea.SQUARE_INCHES): UnitOfArea.SQUARE_CENTIMETERS,
259  ("area", UnitOfArea.SQUARE_FEET): UnitOfArea.SQUARE_METERS,
260  ("area", UnitOfArea.SQUARE_MILES): UnitOfArea.SQUARE_KILOMETERS,
261  ("area", UnitOfArea.SQUARE_YARDS): UnitOfArea.SQUARE_METERS,
262  ("area", UnitOfArea.ACRES): UnitOfArea.HECTARES,
263  # Convert non-metric distances
264  ("distance", UnitOfLength.FEET): UnitOfLength.METERS,
265  ("distance", UnitOfLength.INCHES): UnitOfLength.MILLIMETERS,
266  ("distance", UnitOfLength.MILES): UnitOfLength.KILOMETERS,
267  ("distance", UnitOfLength.NAUTICAL_MILES): UnitOfLength.KILOMETERS,
268  ("distance", UnitOfLength.YARDS): UnitOfLength.METERS,
269  # Convert non-metric volumes of gas meters
270  ("gas", UnitOfVolume.CENTUM_CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
271  ("gas", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
272  # Convert non-metric precipitation
273  ("precipitation", UnitOfLength.INCHES): UnitOfLength.MILLIMETERS,
274  # Convert non-metric precipitation intensity
275  (
276  "precipitation_intensity",
277  UnitOfVolumetricFlux.INCHES_PER_DAY,
278  ): UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
279  (
280  "precipitation_intensity",
281  UnitOfVolumetricFlux.INCHES_PER_HOUR,
282  ): UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
283  # Convert non-metric pressure
284  ("pressure", UnitOfPressure.PSI): UnitOfPressure.KPA,
285  ("pressure", UnitOfPressure.INHG): UnitOfPressure.HPA,
286  # Convert non-metric speeds except knots to km/h
287  ("speed", UnitOfSpeed.FEET_PER_SECOND): UnitOfSpeed.KILOMETERS_PER_HOUR,
288  ("speed", UnitOfSpeed.INCHES_PER_SECOND): UnitOfSpeed.MILLIMETERS_PER_SECOND,
289  ("speed", UnitOfSpeed.MILES_PER_HOUR): UnitOfSpeed.KILOMETERS_PER_HOUR,
290  (
291  "speed",
292  UnitOfVolumetricFlux.INCHES_PER_DAY,
293  ): UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
294  (
295  "speed",
296  UnitOfVolumetricFlux.INCHES_PER_HOUR,
297  ): UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
298  # Convert non-metric volumes
299  ("volume", UnitOfVolume.CENTUM_CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
300  ("volume", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
301  ("volume", UnitOfVolume.FLUID_OUNCES): UnitOfVolume.MILLILITERS,
302  ("volume", UnitOfVolume.GALLONS): UnitOfVolume.LITERS,
303  # Convert non-metric volumes of water meters
304  ("water", UnitOfVolume.CENTUM_CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
305  ("water", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
306  ("water", UnitOfVolume.GALLONS): UnitOfVolume.LITERS,
307  # Convert wind speeds except knots to km/h
308  **{
309  ("wind_speed", unit): UnitOfSpeed.KILOMETERS_PER_HOUR
310  for unit in UnitOfSpeed
311  if unit not in (UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.KNOTS)
312  },
313  },
314  area=UnitOfArea.SQUARE_METERS,
315  length=UnitOfLength.KILOMETERS,
316  mass=UnitOfMass.GRAMS,
317  pressure=UnitOfPressure.PA,
318  temperature=UnitOfTemperature.CELSIUS,
319  volume=UnitOfVolume.LITERS,
320  wind_speed=UnitOfSpeed.METERS_PER_SECOND,
321 )
322 
323 US_CUSTOMARY_SYSTEM = UnitSystem(
324  _CONF_UNIT_SYSTEM_US_CUSTOMARY,
325  accumulated_precipitation=UnitOfPrecipitationDepth.INCHES,
326  conversions={
327  # Force atmospheric pressures to inHg
328  **{
329  ("atmospheric_pressure", unit): UnitOfPressure.INHG
330  for unit in UnitOfPressure
331  if unit != UnitOfPressure.INHG
332  },
333  # Convert non-USCS areas
334  ("area", UnitOfArea.SQUARE_METERS): UnitOfArea.SQUARE_FEET,
335  ("area", UnitOfArea.SQUARE_CENTIMETERS): UnitOfArea.SQUARE_INCHES,
336  ("area", UnitOfArea.SQUARE_MILLIMETERS): UnitOfArea.SQUARE_INCHES,
337  ("area", UnitOfArea.SQUARE_KILOMETERS): UnitOfArea.SQUARE_MILES,
338  ("area", UnitOfArea.HECTARES): UnitOfArea.ACRES,
339  # Convert non-USCS distances
340  ("distance", UnitOfLength.CENTIMETERS): UnitOfLength.INCHES,
341  ("distance", UnitOfLength.KILOMETERS): UnitOfLength.MILES,
342  ("distance", UnitOfLength.METERS): UnitOfLength.FEET,
343  ("distance", UnitOfLength.MILLIMETERS): UnitOfLength.INCHES,
344  # Convert non-USCS volumes of gas meters
345  ("gas", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET,
346  # Convert non-USCS precipitation
347  ("precipitation", UnitOfLength.CENTIMETERS): UnitOfLength.INCHES,
348  ("precipitation", UnitOfLength.MILLIMETERS): UnitOfLength.INCHES,
349  # Convert non-USCS precipitation intensity
350  (
351  "precipitation_intensity",
352  UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
353  ): UnitOfVolumetricFlux.INCHES_PER_DAY,
354  (
355  "precipitation_intensity",
356  UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
357  ): UnitOfVolumetricFlux.INCHES_PER_HOUR,
358  # Convert non-USCS pressure
359  ("pressure", UnitOfPressure.MBAR): UnitOfPressure.PSI,
360  ("pressure", UnitOfPressure.CBAR): UnitOfPressure.PSI,
361  ("pressure", UnitOfPressure.BAR): UnitOfPressure.PSI,
362  ("pressure", UnitOfPressure.PA): UnitOfPressure.PSI,
363  ("pressure", UnitOfPressure.HPA): UnitOfPressure.PSI,
364  ("pressure", UnitOfPressure.KPA): UnitOfPressure.PSI,
365  ("pressure", UnitOfPressure.MMHG): UnitOfPressure.INHG,
366  # Convert non-USCS speeds, except knots, to mph
367  ("speed", UnitOfSpeed.METERS_PER_SECOND): UnitOfSpeed.MILES_PER_HOUR,
368  ("speed", UnitOfSpeed.MILLIMETERS_PER_SECOND): UnitOfSpeed.INCHES_PER_SECOND,
369  ("speed", UnitOfSpeed.KILOMETERS_PER_HOUR): UnitOfSpeed.MILES_PER_HOUR,
370  (
371  "speed",
372  UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
373  ): UnitOfVolumetricFlux.INCHES_PER_DAY,
374  (
375  "speed",
376  UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
377  ): UnitOfVolumetricFlux.INCHES_PER_HOUR,
378  # Convert non-USCS volumes
379  ("volume", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET,
380  ("volume", UnitOfVolume.LITERS): UnitOfVolume.GALLONS,
381  ("volume", UnitOfVolume.MILLILITERS): UnitOfVolume.FLUID_OUNCES,
382  # Convert non-USCS volumes of water meters
383  ("water", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET,
384  ("water", UnitOfVolume.LITERS): UnitOfVolume.GALLONS,
385  # Convert wind speeds except knots to mph
386  **{
387  ("wind_speed", unit): UnitOfSpeed.MILES_PER_HOUR
388  for unit in UnitOfSpeed
389  if unit not in (UnitOfSpeed.KNOTS, UnitOfSpeed.MILES_PER_HOUR)
390  },
391  },
392  area=UnitOfArea.SQUARE_FEET,
393  length=UnitOfLength.MILES,
394  mass=UnitOfMass.POUNDS,
395  pressure=UnitOfPressure.PSI,
396  temperature=UnitOfTemperature.FAHRENHEIT,
397  volume=UnitOfVolume.GALLONS,
398  wind_speed=UnitOfSpeed.MILES_PER_HOUR,
399 )
400 
401 IMPERIAL_SYSTEM = US_CUSTOMARY_SYSTEM
402 """IMPERIAL_SYSTEM is deprecated. Please use US_CUSTOMARY_SYSTEM instead."""
float volume(self, float|None volume, str from_unit)
Definition: unit_system.py:191
float temperature(self, float temperature, str from_unit)
Definition: unit_system.py:132
float accumulated_precipitation(self, float|None precip, str from_unit)
Definition: unit_system.py:151
float length(self, float|None length, str from_unit)
Definition: unit_system.py:141
float pressure(self, float|None pressure, str from_unit)
Definition: unit_system.py:171
float wind_speed(self, float|None wind_speed, str from_unit)
Definition: unit_system.py:181
None __init__(self, str name, *UnitOfPrecipitationDepth accumulated_precipitation, UnitOfArea area, dict[tuple[SensorDeviceClass|str|None, str|None], str] conversions, UnitOfLength length, UnitOfMass mass, UnitOfPressure pressure, UnitOfTemperature temperature, UnitOfVolume volume, UnitOfSpeed wind_speed)
Definition: unit_system.py:101
str|None get_converted_unit(self, SensorDeviceClass|str|None device_class, str|None original_unit)
Definition: unit_system.py:218
float area(self, float|None area, str from_unit)
Definition: unit_system.py:161
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
UnitSystem get_unit_system(str key)
Definition: unit_system.py:223
str _deprecated_unit_system(str value)
Definition: unit_system.py:232
bool _is_valid_unit(str unit, str unit_type)
Definition: unit_system.py:78