Refactor Rest Sensor with ManualTriggerEntity (#97396)

* ManualTriggerEntity for rest sensor

* add availability test

* review comments

* last fixes
This commit is contained in:
G Johansson 2023-08-10 21:46:56 +02:00 committed by GitHub
parent 82ade574d8
commit aacb8aecfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 95 additions and 13 deletions

View file

@ -76,6 +76,7 @@ SENSOR_SCHEMA = {
vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, vol.Optional(CONF_JSON_ATTRS_PATH): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_AVAILABILITY): cv.template,
} }
BINARY_SENSOR_SCHEMA = { BINARY_SENSOR_SCHEMA = {

View file

@ -3,28 +3,40 @@ from __future__ import annotations
import logging import logging
import ssl import ssl
from typing import Any
from jsonpath import jsonpath from jsonpath import jsonpath
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN, DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
SensorDeviceClass, SensorDeviceClass,
SensorEntity,
) )
from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
CONF_ICON,
CONF_NAME,
CONF_RESOURCE, CONF_RESOURCE,
CONF_RESOURCE_TEMPLATE, CONF_RESOURCE_TEMPLATE,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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,
ManualTriggerSensorEntity,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.json import json_loads from homeassistant.util.json import json_loads
@ -43,6 +55,16 @@ PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA 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( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -75,7 +97,14 @@ async def async_setup_platform(
raise PlatformNotReady from rest.last_exception raise PlatformNotReady from rest.last_exception
raise PlatformNotReady raise PlatformNotReady
unique_id: str | None = conf.get(CONF_UNIQUE_ID) 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 conf:
continue
trigger_entity_config[key] = conf[key]
async_add_entities( async_add_entities(
[ [
@ -84,13 +113,13 @@ async def async_setup_platform(
coordinator, coordinator,
rest, rest,
conf, conf,
unique_id, trigger_entity_config,
) )
], ],
) )
class RestSensor(RestEntity, TemplateSensor): class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity):
"""Implementation of a REST sensor.""" """Implementation of a REST sensor."""
def __init__( def __init__(
@ -99,9 +128,10 @@ class RestSensor(RestEntity, TemplateSensor):
coordinator: DataUpdateCoordinator[None] | None, coordinator: DataUpdateCoordinator[None] | None,
rest: RestData, rest: RestData,
config: ConfigType, config: ConfigType,
unique_id: str | None, trigger_entity_config: ConfigType,
) -> None: ) -> None:
"""Initialize the REST sensor.""" """Initialize the REST sensor."""
ManualTriggerSensorEntity.__init__(self, hass, trigger_entity_config)
RestEntity.__init__( RestEntity.__init__(
self, self,
coordinator, coordinator,
@ -109,25 +139,30 @@ class RestSensor(RestEntity, TemplateSensor):
config.get(CONF_RESOURCE_TEMPLATE), config.get(CONF_RESOURCE_TEMPLATE),
config[CONF_FORCE_UPDATE], config[CONF_FORCE_UPDATE],
) )
TemplateSensor.__init__(
self,
hass,
config=config,
fallback_name=DEFAULT_SENSOR_NAME,
unique_id=unique_id,
)
self._value_template = config.get(CONF_VALUE_TEMPLATE) self._value_template = config.get(CONF_VALUE_TEMPLATE)
if (value_template := self._value_template) is not None: if (value_template := self._value_template) is not None:
value_template.hass = hass value_template.hass = hass
self._json_attrs = config.get(CONF_JSON_ATTRS) self._json_attrs = config.get(CONF_JSON_ATTRS)
self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH)
self._attr_extra_state_attributes = {}
@property
def available(self) -> bool:
"""Return if entity is available."""
available1 = RestEntity.available.fget(self) # type: ignore[attr-defined]
available2 = ManualTriggerSensorEntity.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: def _update_from_rest_data(self) -> None:
"""Update state from the rest data.""" """Update state from the rest data."""
value = self.rest.data_without_xml() value = self.rest.data_without_xml()
if self._json_attrs: if self._json_attrs:
self._attr_extra_state_attributes = {}
if value: if value:
try: try:
json_dict = json_loads(value) json_dict = json_loads(value)
@ -155,6 +190,8 @@ class RestSensor(RestEntity, TemplateSensor):
else: else:
_LOGGER.warning("Empty reply found when expecting JSON data") _LOGGER.warning("Empty reply found when expecting JSON data")
raw_value = value
if value is not None and self._value_template is not None: if value is not None and self._value_template is not None:
value = self._value_template.async_render_with_possible_json_value( value = self._value_template.async_render_with_possible_json_value(
value, None value, None
@ -165,8 +202,13 @@ class RestSensor(RestEntity, TemplateSensor):
SensorDeviceClass.TIMESTAMP, SensorDeviceClass.TIMESTAMP,
): ):
self._attr_native_value = value self._attr_native_value = value
self._process_manual_data(raw_value)
self.async_write_ha_state()
return return
self._attr_native_value = async_parse_date_datetime( self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class value, self.entity_id, self.device_class
) )
self._process_manual_data(raw_value)
self.async_write_ha_state()

View file

@ -653,3 +653,17 @@ class ManualTriggerEntity(TriggerBaseEntity):
variables = {"this": this, **(run_variables or {})} variables = {"this": this, **(run_variables or {})}
self._render_templates(variables) 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)

View file

@ -23,6 +23,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONTENT_TYPE_JSON, CONTENT_TYPE_JSON,
SERVICE_RELOAD, SERVICE_RELOAD,
STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
UnitOfInformation, UnitOfInformation,
UnitOfTemperature, UnitOfTemperature,
@ -1018,3 +1019,27 @@ async def test_entity_config(hass: HomeAssistant) -> None:
"state_class": "measurement", "state_class": "measurement",
"unit_of_measurement": "°C", "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