diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 4df91cf4e15..048ffdd237b 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_SENSORS, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription @@ -132,19 +132,3 @@ class OpenUvEntity(CoordinatorEntity): name="OpenUV", entry_type=DeviceEntryType.SERVICE, ) - - @callback - def _handle_coordinator_update(self) -> None: - """Respond to a DataUpdateCoordinator update.""" - self._update_from_latest_data() - self.async_write_ha_state() - - @callback - def _update_from_latest_data(self) -> None: - """Update the entity from the latest data.""" - raise NotImplementedError - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - self._update_from_latest_data() diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index e9f9ee99ff6..9c970f34dc3 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -45,7 +45,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): """Define a binary sensor for OpenUV.""" @callback - def _update_from_latest_data(self) -> None: + def _handle_coordinator_update(self) -> None: """Update the entity from the latest data.""" data = self.coordinator.data @@ -76,3 +76,5 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt), } ) + + super()._handle_coordinator_update() diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 90eefac594a..6c4bff855a4 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -1,6 +1,10 @@ """Support for OpenUV sensors.""" from __future__ import annotations +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Any + from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, @@ -8,7 +12,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UV_INDEX, UnitOfTime -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import as_local, parse_datetime @@ -40,79 +44,135 @@ EXPOSURE_TYPE_MAP = { TYPE_SAFE_EXPOSURE_TIME_6: "st6", } -UV_LEVEL_EXTREME = "Extreme" -UV_LEVEL_VHIGH = "Very High" -UV_LEVEL_HIGH = "High" -UV_LEVEL_MODERATE = "Moderate" -UV_LEVEL_LOW = "Low" + +@dataclass +class UvLabel: + """Define a friendly UV level label and its minimum UV index.""" + + value: str + minimum_index: int + + +UV_LABEL_DEFINITIONS = ( + UvLabel(value="Extreme", minimum_index=11), + UvLabel(value="Very High", minimum_index=8), + UvLabel(value="High", minimum_index=6), + UvLabel(value="Moderate", minimum_index=3), + UvLabel(value="Low", minimum_index=0), +) + + +def get_uv_label(uv_index: int) -> str: + """Return the UV label for the UV index.""" + label = next( + label for label in UV_LABEL_DEFINITIONS if uv_index >= label.minimum_index + ) + return label.value + + +@dataclass +class OpenUvSensorEntityDescriptionMixin: + """Define a mixin for OpenUV sensor descriptions.""" + + value_fn: Callable[[dict[str, Any]], int | str] + + +@dataclass +class OpenUvSensorEntityDescription( + SensorEntityDescription, OpenUvSensorEntityDescriptionMixin +): + """Define a class that describes OpenUV sensor entities.""" + SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, translation_key="current_ozone_level", native_unit_of_measurement="du", state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["ozone"], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_CURRENT_UV_INDEX, translation_key="current_uv_index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["uv"], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_CURRENT_UV_LEVEL, translation_key="current_uv_level", icon="mdi:weather-sunny", + value_fn=lambda data: get_uv_label(data["uv"]), ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_MAX_UV_INDEX, translation_key="max_uv_index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["uv_max"], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_1, translation_key="skin_type_1_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["safe_exposure_time"][ + EXPOSURE_TYPE_MAP[TYPE_SAFE_EXPOSURE_TIME_1] + ], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_2, translation_key="skin_type_2_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["safe_exposure_time"][ + EXPOSURE_TYPE_MAP[TYPE_SAFE_EXPOSURE_TIME_2] + ], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_3, translation_key="skin_type_3_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["safe_exposure_time"][ + EXPOSURE_TYPE_MAP[TYPE_SAFE_EXPOSURE_TIME_3] + ], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_4, translation_key="skin_type_4_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["safe_exposure_time"][ + EXPOSURE_TYPE_MAP[TYPE_SAFE_EXPOSURE_TIME_4] + ], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_5, translation_key="skin_type_5_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["safe_exposure_time"][ + EXPOSURE_TYPE_MAP[TYPE_SAFE_EXPOSURE_TIME_5] + ], ), - SensorEntityDescription( + OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_6, translation_key="skin_type_6_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["safe_exposure_time"][ + EXPOSURE_TYPE_MAP[TYPE_SAFE_EXPOSURE_TIME_6] + ], ), ) @@ -134,40 +194,18 @@ async def async_setup_entry( class OpenUvSensor(OpenUvEntity, SensorEntity): """Define a binary sensor for OpenUV.""" - @callback - def _update_from_latest_data(self) -> None: - """Update the state.""" - data = self.coordinator.data + entity_description: OpenUvSensorEntityDescription - if self.entity_description.key == TYPE_CURRENT_OZONE_LEVEL: - self._attr_native_value = data["ozone"] - elif self.entity_description.key == TYPE_CURRENT_UV_INDEX: - self._attr_native_value = data["uv"] - elif self.entity_description.key == TYPE_CURRENT_UV_LEVEL: - if data["uv"] >= 11: - self._attr_native_value = UV_LEVEL_EXTREME - elif data["uv"] >= 8: - self._attr_native_value = UV_LEVEL_VHIGH - elif data["uv"] >= 6: - self._attr_native_value = UV_LEVEL_HIGH - elif data["uv"] >= 3: - self._attr_native_value = UV_LEVEL_MODERATE - else: - self._attr_native_value = UV_LEVEL_LOW - elif self.entity_description.key == TYPE_MAX_UV_INDEX: - self._attr_native_value = data["uv_max"] - if uv_max_time := parse_datetime(data["uv_max_time"]): - self._attr_extra_state_attributes.update( - {ATTR_MAX_UV_TIME: as_local(uv_max_time)} - ) - elif self.entity_description.key in ( - TYPE_SAFE_EXPOSURE_TIME_1, - TYPE_SAFE_EXPOSURE_TIME_2, - TYPE_SAFE_EXPOSURE_TIME_3, - TYPE_SAFE_EXPOSURE_TIME_4, - TYPE_SAFE_EXPOSURE_TIME_5, - TYPE_SAFE_EXPOSURE_TIME_6, - ): - self._attr_native_value = data["safe_exposure_time"][ - EXPOSURE_TYPE_MAP[self.entity_description.key] - ] + @property + def extra_state_attributes(self) -> Mapping[str, Any]: + """Return entity specific state attributes.""" + attrs = {} + if self.entity_description.key == TYPE_MAX_UV_INDEX: + if uv_max_time := parse_datetime(self.coordinator.data["uv_max_time"]): + attrs[ATTR_MAX_UV_TIME] = as_local(uv_max_time) + return attrs + + @property + def native_value(self) -> int | str: + """Return the sensor value.""" + return self.entity_description.value_fn(self.coordinator.data)