-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Allow customizing unit for temperature and pressure sensors #64366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
fb8e703
Allow customizing unit for temperature and pressure sensors
emontnemery 649c972
pylint
emontnemery 23fe430
Adjust google_wifi tests
emontnemery b9fc4fd
Address review comments and add tests
emontnemery be60f14
Improve rounding when scaling
emontnemery 33708c9
Tweak rounding
emontnemery cea05bd
Further tweak rounding
emontnemery 9834b9b
Allow setting entity options with config/entity_registry/update
emontnemery fdc9dad
Address review comments
emontnemery 46ca5e9
Tweak tests
emontnemery 5c350b3
Load custom unit when sensor is added
emontnemery 358fd31
Override async_internal_added_to_hass
emontnemery b42aaaf
Adjust tests after rebase
emontnemery 9626282
Apply suggestions from code review
emontnemery cc69138
Address review comments
emontnemery File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,21 @@ | ||
| """Component to interface with various sensors that can be monitored.""" | ||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Mapping | ||
| from collections.abc import Callable, Mapping | ||
| from contextlib import suppress | ||
| from dataclasses import dataclass | ||
| from datetime import date, datetime, timedelta, timezone | ||
| import inspect | ||
| import logging | ||
| from math import floor, log10 | ||
| from typing import Any, Final, cast, final | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.backports.enum import StrEnum | ||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import ( # noqa: F401 | ||
| CONF_UNIT_OF_MEASUREMENT, | ||
| DEVICE_CLASS_AQI, | ||
| DEVICE_CLASS_BATTERY, | ||
| DEVICE_CLASS_CO, | ||
|
|
@@ -44,8 +46,9 @@ | |
| DEVICE_CLASS_VOLTAGE, | ||
| TEMP_CELSIUS, | ||
| TEMP_FAHRENHEIT, | ||
| TEMP_KELVIN, | ||
| ) | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.core import HomeAssistant, callback | ||
| from homeassistant.helpers.config_validation import ( # noqa: F401 | ||
| PLATFORM_SCHEMA, | ||
| PLATFORM_SCHEMA_BASE, | ||
|
|
@@ -54,7 +57,11 @@ | |
| from homeassistant.helpers.entity_component import EntityComponent | ||
| from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity | ||
| from homeassistant.helpers.typing import ConfigType, StateType | ||
| from homeassistant.util import dt as dt_util | ||
| from homeassistant.util import ( | ||
| dt as dt_util, | ||
| pressure as pressure_util, | ||
| temperature as temperature_util, | ||
| ) | ||
|
|
||
| from .const import CONF_STATE_CLASS # noqa: F401 | ||
|
|
||
|
|
@@ -194,6 +201,25 @@ class SensorStateClass(StrEnum): | |
| STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" | ||
| STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] | ||
|
|
||
| UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { | ||
| SensorDeviceClass.PRESSURE: pressure_util.convert, | ||
| SensorDeviceClass.TEMPERATURE: temperature_util.convert, | ||
| } | ||
|
|
||
| UNIT_RATIOS: dict[str, dict[str, float]] = { | ||
| SensorDeviceClass.PRESSURE: pressure_util.UNIT_CONVERSION, | ||
| SensorDeviceClass.TEMPERATURE: { | ||
| TEMP_CELSIUS: 1.0, | ||
| TEMP_FAHRENHEIT: 1.8, | ||
| TEMP_KELVIN: 1.0, | ||
| }, | ||
| } | ||
|
|
||
| VALID_UNITS: dict[str, tuple[str, ...]] = { | ||
| SensorDeviceClass.PRESSURE: pressure_util.VALID_UNITS, | ||
| SensorDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, | ||
| } | ||
|
|
||
|
|
||
| async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||
| """Track states and offer events for sensors.""" | ||
|
|
@@ -264,10 +290,18 @@ class SensorEntity(Entity): | |
| ) | ||
| _last_reset_reported = False | ||
| _temperature_conversion_reported = False | ||
| _sensor_option_unit_of_measurement: str | None = None | ||
|
|
||
| # Temporary private attribute to track if deprecation has been logged. | ||
| __datetime_as_string_deprecation_logged = False | ||
|
|
||
| async def async_internal_added_to_hass(self) -> None: | ||
| """Call when the sensor entity is added to hass.""" | ||
| await super().async_internal_added_to_hass() | ||
| if not self.registry_entry: | ||
| return | ||
| self.async_registry_entry_updated() | ||
|
|
||
| @property | ||
| def device_class(self) -> SensorDeviceClass | str | None: | ||
| """Return the class of this entity.""" | ||
|
|
@@ -350,6 +384,9 @@ def native_unit_of_measurement(self) -> str | None: | |
| @property | ||
| def unit_of_measurement(self) -> str | None: | ||
| """Return the unit of measurement of the entity, after unit conversion.""" | ||
| if self._sensor_option_unit_of_measurement: | ||
| return self._sensor_option_unit_of_measurement | ||
|
|
||
| # Support for _attr_unit_of_measurement will be removed in Home Assistant 2021.11 | ||
| if ( | ||
| hasattr(self, "_attr_unit_of_measurement") | ||
|
|
@@ -368,7 +405,8 @@ def unit_of_measurement(self) -> str | None: | |
| @property | ||
| def state(self) -> Any: | ||
| """Return the state of the sensor and perform unit conversions, if needed.""" | ||
| unit_of_measurement = self.native_unit_of_measurement | ||
| native_unit_of_measurement = self.native_unit_of_measurement | ||
| unit_of_measurement = self.unit_of_measurement | ||
| value = self.native_value | ||
| device_class = self.device_class | ||
|
|
||
|
|
@@ -407,16 +445,48 @@ def state(self) -> Any: | |
| f"but does not provide a date state but {type(value)}" | ||
| ) from err | ||
|
|
||
| if ( | ||
| value is not None | ||
| and self.device_class in UNIT_CONVERSIONS | ||
| and native_unit_of_measurement != unit_of_measurement | ||
|
emontnemery marked this conversation as resolved.
Outdated
|
||
| ): | ||
| assert unit_of_measurement | ||
| assert native_unit_of_measurement | ||
|
|
||
| value_s = str(value) | ||
| prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 | ||
|
|
||
| # Scale the precision when converting to a larger unit | ||
| # For example 1.1 kWh should be rendered as 0.0011 kWh, not 0.0 kWh | ||
| ratio_log = max( | ||
| 0, | ||
| log10( | ||
| UNIT_RATIOS[self.device_class][native_unit_of_measurement] | ||
| / UNIT_RATIOS[self.device_class][unit_of_measurement] | ||
| ), | ||
| ) | ||
| prec = prec + floor(ratio_log) | ||
|
|
||
| # Suppress ValueError (Could not convert sensor_value to float) | ||
| with suppress(ValueError): | ||
| value_f = float(value) # type: ignore[arg-type] | ||
| value_f_new = UNIT_CONVERSIONS[self.device_class]( | ||
| value_f, | ||
| native_unit_of_measurement, | ||
| unit_of_measurement, | ||
| ) | ||
|
|
||
| # Round to the wanted precision | ||
| value = round(value_f_new) if prec == 0 else round(value_f_new, prec) | ||
|
|
||
| units = self.hass.config.units | ||
| if ( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be an |
||
| value is not None | ||
| and unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT) | ||
| and unit_of_measurement != units.temperature_unit | ||
| and self.device_class != DEVICE_CLASS_TEMPERATURE | ||
| and native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT) | ||
| and native_unit_of_measurement != units.temperature_unit | ||
|
emontnemery marked this conversation as resolved.
Outdated
|
||
| ): | ||
| if ( | ||
| self.device_class != DEVICE_CLASS_TEMPERATURE | ||
| and not self._temperature_conversion_reported | ||
| ): | ||
| if not self._temperature_conversion_reported: | ||
| self._temperature_conversion_reported = True | ||
| report_issue = self._suggest_report_issue() | ||
| _LOGGER.warning( | ||
|
|
@@ -429,15 +499,15 @@ def state(self) -> Any: | |
| self.entity_id, | ||
| type(self), | ||
| self.device_class, | ||
| unit_of_measurement, | ||
| native_unit_of_measurement, | ||
| units.temperature_unit, | ||
| report_issue, | ||
| ) | ||
| value_s = str(value) | ||
| prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 | ||
| # Suppress ValueError (Could not convert sensor_value to float) | ||
| with suppress(ValueError): | ||
| temp = units.temperature(float(value), unit_of_measurement) # type: ignore[arg-type] | ||
| temp = units.temperature(float(value), native_unit_of_measurement) # type: ignore[arg-type] | ||
| value = round(temp) if prec == 0 else round(temp, prec) | ||
|
|
||
| return value | ||
|
|
@@ -453,6 +523,22 @@ def __repr__(self) -> str: | |
|
|
||
| return super().__repr__() | ||
|
|
||
| @callback | ||
| def async_registry_entry_updated(self) -> None: | ||
| """Run when the entity registry entry has been updated.""" | ||
| assert self.registry_entry | ||
| if ( | ||
| (sensor_options := self.registry_entry.options.get(DOMAIN)) | ||
| and (custom_unit := sensor_options.get(CONF_UNIT_OF_MEASUREMENT)) | ||
| and (device_class := self.device_class) in UNIT_CONVERSIONS | ||
| and self.native_unit_of_measurement in VALID_UNITS[device_class] | ||
| and custom_unit in VALID_UNITS[device_class] | ||
| ): | ||
| self._sensor_option_unit_of_measurement = custom_unit | ||
| return | ||
|
|
||
| self._sensor_option_unit_of_measurement = None | ||
|
|
||
|
|
||
| @dataclass | ||
| class SensorExtraStoredData(ExtraStoredData): | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you just store the entry here in a var so you don't need to fetch it inside the
disabled_byif ?