From e3695c1973e5333aacdb06f38fa3c1900c1d121e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 11:49:29 +0000 Subject: [PATCH 1/4] ManualTriggerEntity for rest sensor --- homeassistant/components/rest/schema.py | 1 + homeassistant/components/rest/sensor.py | 79 +++++++++++++++++++++---- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index c1f512866737c..2f447b1c08ca4 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -76,6 +76,7 @@ vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_AVAILABILITY): cv.template, } BINARY_SENSOR_SCHEMA = { diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 18d0b6c7e760d..a5e87528f9bfe 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -3,28 +3,40 @@ import logging import ssl +from typing import Any from jsonpath import jsonpath import voluptuous as vol from homeassistant.components.sensor import ( + CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorDeviceClass, + SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, + CONF_ICON, + CONF_NAME, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.template_entity import TemplateSensor +from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, + ManualTriggerEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.json import json_loads @@ -75,7 +87,24 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - unique_id: str | None = conf.get(CONF_UNIQUE_ID) + unit_of_measurement = conf.get(CONF_UNIT_OF_MEASUREMENT) + state_class = conf.get(CONF_STATE_CLASS) + + name = conf.get(CONF_NAME) + if not name: + name = Template(DEFAULT_SENSOR_NAME, hass) + + trigger_entity_config = { + CONF_NAME: name, + CONF_DEVICE_CLASS: conf.get(CONF_DEVICE_CLASS), + CONF_UNIQUE_ID: conf.get(CONF_UNIQUE_ID), + } + if available := conf.get(CONF_AVAILABILITY): + trigger_entity_config[CONF_AVAILABILITY] = available + if icon := conf.get(CONF_ICON): + trigger_entity_config[CONF_ICON] = icon + if picture := conf.get(CONF_PICTURE): + trigger_entity_config[CONF_PICTURE] = picture async_add_entities( [ @@ -84,13 +113,15 @@ async def async_setup_platform( coordinator, rest, conf, - unique_id, + trigger_entity_config, + unit_of_measurement, + state_class, ) ], ) -class RestSensor(RestEntity, TemplateSensor): +class RestSensor(ManualTriggerEntity, RestEntity, SensorEntity): """Implementation of a REST sensor.""" def __init__( @@ -99,9 +130,12 @@ def __init__( coordinator: DataUpdateCoordinator[None] | None, rest: RestData, config: ConfigType, - unique_id: str | None, + trigger_entity_config: ConfigType, + unit_of_measurement: str | None, + state_class: str | None, ) -> None: """Initialize the REST sensor.""" + ManualTriggerEntity.__init__(self, hass, trigger_entity_config) RestEntity.__init__( self, coordinator, @@ -109,25 +143,37 @@ def __init__( config.get(CONF_RESOURCE_TEMPLATE), config[CONF_FORCE_UPDATE], ) - TemplateSensor.__init__( - self, - hass, - config=config, - fallback_name=DEFAULT_SENSOR_NAME, - unique_id=unique_id, - ) + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_state_class = state_class self._value_template = config.get(CONF_VALUE_TEMPLATE) if (value_template := self._value_template) is not None: value_template.hass = hass self._json_attrs = config.get(CONF_JSON_ATTRS) self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) + self._attr_extra_state_attributes = {} + + async def async_added_to_hass(self) -> None: + """Ensure the data from the initial update is reflected in the state.""" + await RestEntity.async_added_to_hass(self) + await ManualTriggerEntity.async_added_to_hass(self) + + @property + def available(self) -> bool: + """Return if entity is available.""" + available1 = RestEntity.available.fget(self) # type: ignore[attr-defined] + available2 = ManualTriggerEntity.available.fget(self) # type: ignore[attr-defined] + return bool(available1 and available2) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return extra attributes.""" + return dict(self._attr_extra_state_attributes) def _update_from_rest_data(self) -> None: """Update state from the rest data.""" value = self.rest.data_without_xml() if self._json_attrs: - self._attr_extra_state_attributes = {} if value: try: json_dict = json_loads(value) @@ -155,6 +201,8 @@ def _update_from_rest_data(self) -> None: else: _LOGGER.warning("Empty reply found when expecting JSON data") + raw_value = value + if value is not None and self._value_template is not None: value = self._value_template.async_render_with_possible_json_value( value, None @@ -165,8 +213,13 @@ def _update_from_rest_data(self) -> None: SensorDeviceClass.TIMESTAMP, ): self._attr_native_value = value + self._process_manual_data(raw_value) + self.async_write_ha_state() return self._attr_native_value = async_parse_date_datetime( value, self.entity_id, self.device_class ) + + self._process_manual_data(raw_value) + self.async_write_ha_state() From 63cbb7b8761151ab64f483b55dffa731649aeb5a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Aug 2023 17:26:22 +0000 Subject: [PATCH 2/4] add availability test --- tests/components/rest/test_sensor.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index a7674937ab858..34e7233d33ca7 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -23,6 +23,7 @@ ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, SERVICE_RELOAD, + STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfInformation, UnitOfTemperature, @@ -1018,3 +1019,27 @@ async def test_entity_config(hass: HomeAssistant) -> None: "state_class": "measurement", "unit_of_measurement": "°C", } + + +@respx.mock +async def test_availability_in_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # REST configuration + "platform": DOMAIN, + "method": "GET", + "resource": "http://localhost", + # Entity configuration + "availability": "{{value==1}}", + "name": "{{'REST' + ' ' + 'Sensor'}}", + }, + } + + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, text="123") + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.rest_sensor") + assert state.state == STATE_UNAVAILABLE From 8dbf267d10ee05f19ce29a2be8f23ddc45010a32 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 10 Aug 2023 08:46:13 +0000 Subject: [PATCH 3/4] review comments --- homeassistant/components/rest/sensor.py | 49 +++++++++++------------- homeassistant/helpers/template_entity.py | 14 +++++++ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index a5e87528f9bfe..78be811250494 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -36,6 +36,7 @@ CONF_AVAILABILITY, CONF_PICTURE, ManualTriggerEntity, + ManualTriggerSensorEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -55,6 +56,16 @@ cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA ) +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) + async def async_setup_platform( hass: HomeAssistant, @@ -87,24 +98,14 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - unit_of_measurement = conf.get(CONF_UNIT_OF_MEASUREMENT) - state_class = conf.get(CONF_STATE_CLASS) - - name = conf.get(CONF_NAME) - if not name: - name = Template(DEFAULT_SENSOR_NAME, hass) - - trigger_entity_config = { - CONF_NAME: name, - CONF_DEVICE_CLASS: conf.get(CONF_DEVICE_CLASS), - CONF_UNIQUE_ID: conf.get(CONF_UNIQUE_ID), - } - if available := conf.get(CONF_AVAILABILITY): - trigger_entity_config[CONF_AVAILABILITY] = available - if icon := conf.get(CONF_ICON): - trigger_entity_config[CONF_ICON] = icon - if picture := conf.get(CONF_PICTURE): - trigger_entity_config[CONF_PICTURE] = picture + name = config.get(CONF_NAME) or Template(DEFAULT_SENSOR_NAME, hass) + + trigger_entity_config = {CONF_NAME: name} + + for key in TRIGGER_ENTITY_OPTIONS: + if key not in config: + continue + trigger_entity_config[key] = config[key] async_add_entities( [ @@ -114,14 +115,12 @@ async def async_setup_platform( rest, conf, trigger_entity_config, - unit_of_measurement, - state_class, ) ], ) -class RestSensor(ManualTriggerEntity, RestEntity, SensorEntity): +class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity): """Implementation of a REST sensor.""" def __init__( @@ -131,11 +130,9 @@ def __init__( rest: RestData, config: ConfigType, trigger_entity_config: ConfigType, - unit_of_measurement: str | None, - state_class: str | None, ) -> None: """Initialize the REST sensor.""" - ManualTriggerEntity.__init__(self, hass, trigger_entity_config) + ManualTriggerSensorEntity.__init__(self, hass, trigger_entity_config) RestEntity.__init__( self, coordinator, @@ -143,8 +140,6 @@ def __init__( config.get(CONF_RESOURCE_TEMPLATE), config[CONF_FORCE_UPDATE], ) - self._attr_native_unit_of_measurement = unit_of_measurement - self._attr_state_class = state_class self._value_template = config.get(CONF_VALUE_TEMPLATE) if (value_template := self._value_template) is not None: value_template.hass = hass @@ -161,7 +156,7 @@ async def async_added_to_hass(self) -> None: def available(self) -> bool: """Return if entity is available.""" available1 = RestEntity.available.fget(self) # type: ignore[attr-defined] - available2 = ManualTriggerEntity.available.fget(self) # type: ignore[attr-defined] + available2 = ManualTriggerSensorEntity.available.fget(self) # type: ignore[attr-defined] return bool(available1 and available2) @property diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index 07dd154922cb0..07e68152d64b1 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -653,3 +653,17 @@ def _process_manual_data(self, value: Any | None = None) -> None: variables = {"this": this, **(run_variables or {})} self._render_templates(variables) + + +class ManualTriggerSensorEntity(ManualTriggerEntity): + """Template entity based on manual trigger data for sensor.""" + + def __init__( + self, + hass: HomeAssistant, + config: dict, + ) -> None: + """Initialize the sensor entity.""" + ManualTriggerEntity.__init__(self, hass, config) + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_state_class = config.get(CONF_STATE_CLASS) From db5cbf5b1d3c025820c9b3b55123479973e9ceb7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 10 Aug 2023 11:03:33 +0000 Subject: [PATCH 4/4] last fixes --- homeassistant/components/rest/sensor.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 78be811250494..1a74735c670a4 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -35,7 +35,6 @@ from homeassistant.helpers.template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, - ManualTriggerEntity, ManualTriggerSensorEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -98,14 +97,14 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - name = config.get(CONF_NAME) or Template(DEFAULT_SENSOR_NAME, hass) + name = conf.get(CONF_NAME) or Template(DEFAULT_SENSOR_NAME, hass) trigger_entity_config = {CONF_NAME: name} for key in TRIGGER_ENTITY_OPTIONS: - if key not in config: + if key not in conf: continue - trigger_entity_config[key] = config[key] + trigger_entity_config[key] = conf[key] async_add_entities( [ @@ -147,11 +146,6 @@ def __init__( self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) self._attr_extra_state_attributes = {} - async def async_added_to_hass(self) -> None: - """Ensure the data from the initial update is reflected in the state.""" - await RestEntity.async_added_to_hass(self) - await ManualTriggerEntity.async_added_to_hass(self) - @property def available(self) -> bool: """Return if entity is available."""