diff --git a/homeassistant/components/derivative/icons.json b/homeassistant/components/derivative/icons.json index d8f2a961c3a..6c48f0fec76 100644 --- a/homeassistant/components/derivative/icons.json +++ b/homeassistant/components/derivative/icons.json @@ -5,5 +5,10 @@ "default": "mdi:chart-line" } } + }, + "services": { + "reload": { + "service": "mdi:reload" + } } } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index e6d47cdcd1b..39df5fcf648 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, UnitOfTime, ) from homeassistant.core import ( @@ -45,6 +46,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_report_event, ) +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -54,6 +56,7 @@ from .const import ( CONF_UNIT, CONF_UNIT_PREFIX, CONF_UNIT_TIME, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) @@ -147,6 +150,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the derivative sensor.""" + await async_setup_reload_service(hass, DOMAIN, [Platform.SENSOR]) + derivative = DerivativeSensor( hass, name=config.get(CONF_NAME), @@ -288,14 +293,14 @@ class DerivativeSensor(RestoreSensor, SensorEntity): ) self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() + async def _handle_restore(self) -> None: restored_data = await self.async_get_last_sensor_data() if restored_data: - self._attr_native_unit_of_measurement = ( - restored_data.native_unit_of_measurement - ) + if self._attr_native_unit_of_measurement is None: + # Only restore the unit if it's not assigned from YAML + self._attr_native_unit_of_measurement = ( + restored_data.native_unit_of_measurement + ) try: self._attr_native_value = round( Decimal(restored_data.native_value), # type: ignore[arg-type] @@ -304,6 +309,11 @@ class DerivativeSensor(RestoreSensor, SensorEntity): except (InvalidOperation, TypeError): self._attr_native_value = None + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + await self._handle_restore() + source_state = self.hass.states.get(self._sensor_source_id) self._derive_and_set_attributes_from_state(source_state) diff --git a/homeassistant/components/derivative/services.yaml b/homeassistant/components/derivative/services.yaml new file mode 100644 index 00000000000..c983a105c93 --- /dev/null +++ b/homeassistant/components/derivative/services.yaml @@ -0,0 +1 @@ +reload: diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json index c57c672dc34..5fdecf5cdb6 100644 --- a/homeassistant/components/derivative/strings.json +++ b/homeassistant/components/derivative/strings.json @@ -58,5 +58,11 @@ } } }, + "services": { + "reload": { + "description": "Reloads derivative sensors from the YAML-configuration.", + "name": "[%key:common::action::reload%]" + } + }, "title": "Derivative sensor" } diff --git a/tests/components/derivative/fixtures/configuration.yaml b/tests/components/derivative/fixtures/configuration.yaml new file mode 100644 index 00000000000..7dc73abbf50 --- /dev/null +++ b/tests/components/derivative/fixtures/configuration.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: derivative + name: derivative + source: "sensor.energy" + unit: "W" + - platform: derivative + name: derivative_new + source: "sensor.energy" + unit: "W" diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 37118ae932c..4b282582789 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -4,13 +4,16 @@ from datetime import timedelta from math import sin import random from typing import Any +from unittest.mock import patch from freezegun import freeze_time import pytest +from homeassistant import config as hass_config, core as ha from homeassistant.components.derivative.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( + SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfPower, @@ -31,6 +34,7 @@ from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, async_fire_time_changed, + get_fixture_path, mock_restore_cache_with_extra_data, ) @@ -1000,6 +1004,64 @@ async def test_source_unit_change( assert state.attributes.get("unit_of_measurement") == "dogs/s" +async def test_reload(hass: HomeAssistant) -> None: + """Test hot-reloading derivative YAML sensors.""" + hass.state = ha.CoreState.not_running + hass.states.async_set("sensor.energy", "0.0") + + config = { + "sensor": [ + { + "platform": "derivative", + "name": "derivative", + "source": "sensor.energy", + "unit": "kW", + }, + { + "platform": "derivative", + "name": "derivative_remove", + "source": "sensor.energy", + "unit": "kW", + }, + ] + } + + assert await async_setup_component(hass, "sensor", config) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + state = hass.states.get("sensor.derivative") + assert state is not None + assert state.attributes.get("unit_of_measurement") == "kW" + assert hass.states.get("sensor.derivative_remove") + + yaml_path = get_fixture_path("configuration.yaml", "derivative") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + # Check that we can change the unit of an existing sensor + state = hass.states.get("sensor.derivative") + assert state is not None + assert state.attributes.get("unit_of_measurement") == "W" + + # Check that we can remove a derivative sensor + assert hass.states.get("sensor.derivative_remove") is None + + # Check that we can add a new derivative sensor + assert hass.states.get("sensor.derivative_new") + + async def test_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry,