Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions homeassistant/components/config/entity_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def websocket_get_entity(hass, connection, msg):
er.RegistryEntryHider.USER.value,
),
),
vol.Inclusive("options_domain", "entity_option"): str,
vol.Inclusive("options", "entity_option"): vol.Any(None, dict),
}
)
@callback
Expand All @@ -96,7 +98,8 @@ def websocket_update_entity(hass, connection, msg):
"""
registry = er.async_get(hass)

if msg["entity_id"] not in registry.entities:
entity_id = msg["entity_id"]
if entity_id not in registry.entities:

Copy link
Copy Markdown
Member

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_by if ?

connection.send_message(
websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found")
)
Expand All @@ -108,7 +111,7 @@ def websocket_update_entity(hass, connection, msg):
if key in msg:
changes[key] = msg[key]

if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]:
if "new_entity_id" in msg and msg["new_entity_id"] != entity_id:
changes["new_entity_id"] = msg["new_entity_id"]
if hass.states.get(msg["new_entity_id"]) is not None:
connection.send_message(
Expand All @@ -122,7 +125,7 @@ def websocket_update_entity(hass, connection, msg):

if "disabled_by" in msg and msg["disabled_by"] is None:
# Don't allow enabling an entity of a disabled device
entity = registry.entities[msg["entity_id"]]
entity = registry.entities[entity_id]
if entity.device_id:
device_registry = dr.async_get(hass)
device = device_registry.async_get(entity.device_id)
Expand All @@ -136,12 +139,28 @@ def websocket_update_entity(hass, connection, msg):

try:
if changes:
entry = registry.async_update_entity(msg["entity_id"], **changes)
registry.async_update_entity(entity_id, **changes)
except ValueError as err:
connection.send_message(
websocket_api.error_message(msg["id"], "invalid_info", str(err))
)
return

if "new_entity_id" in msg:
entity_id = msg["new_entity_id"]

try:
if "options_domain" in msg:
registry.async_update_entity_options(
entity_id, msg["options_domain"], msg["options"]
)
except ValueError as err:
connection.send_message(
websocket_api.error_message(msg["id"], "invalid_info", str(err))
)
return

entry = registry.async_get(entity_id)
result = {"entity_entry": _entry_ext_dict(entry)}
if "disabled_by" in changes and changes["disabled_by"] is None:
# Enabling an entity requires a config entry reload, or HA restart
Expand Down Expand Up @@ -201,6 +220,7 @@ def _entry_ext_dict(entry):
data = _entry_dict(entry)
data["capabilities"] = entry.capabilities
data["device_class"] = entry.device_class
data["options"] = entry.options
data["original_device_class"] = entry.original_device_class
data["original_icon"] = entry.original_icon
data["original_name"] = entry.original_name
Expand Down
110 changes: 98 additions & 12 deletions homeassistant/components/sensor/__init__.py
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,
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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")
Expand All @@ -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

Expand Down Expand Up @@ -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
Comment thread
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 (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be an elif?

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
Comment thread
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(
Expand All @@ -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
Expand All @@ -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):
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/helpers/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,13 @@ async def async_will_remove_from_hass(self) -> None:
To be extended by integrations.
"""

@callback
def async_registry_entry_updated(self) -> None:
"""Run when the entity registry entry has been updated.

To be extended by integrations.
"""

async def async_internal_added_to_hass(self) -> None:
"""Run when entity about to be added to hass.

Expand Down Expand Up @@ -888,6 +895,7 @@ async def _async_registry_updated(self, event: Event) -> None:

assert old is not None
if self.registry_entry.entity_id == old.entity_id:
self.async_registry_entry_updated()
self.async_write_ha_state()
return

Expand Down
6 changes: 6 additions & 0 deletions homeassistant/util/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
UNIT_NOT_RECOGNIZED_TEMPLATE,
)

VALID_UNITS: tuple[str, ...] = (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
TEMP_KELVIN,
)


def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float:
"""Convert a temperature in Fahrenheit to Celsius."""
Expand Down
Loading