Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Rest Sensor with ManualTriggerEntity #97396

Merged
merged 4 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions homeassistant/components/rest/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
68 changes: 55 additions & 13 deletions homeassistant/components/rest/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
ManualTriggerSensorEntity,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.json import json_loads
Expand All @@ -43,6 +55,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,
Expand Down Expand Up @@ -75,7 +97,14 @@ async def async_setup_platform(
raise PlatformNotReady from rest.last_exception
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(
[
Expand All @@ -84,13 +113,13 @@ async def async_setup_platform(
coordinator,
rest,
conf,
unique_id,
trigger_entity_config,
)
],
)


class RestSensor(RestEntity, TemplateSensor):
class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity):
"""Implementation of a REST sensor."""

def __init__(
Expand All @@ -99,35 +128,41 @@ def __init__(
coordinator: DataUpdateCoordinator[None] | None,
rest: RestData,
config: ConfigType,
unique_id: str | None,
trigger_entity_config: ConfigType,
) -> None:
"""Initialize the REST sensor."""
ManualTriggerSensorEntity.__init__(self, hass, trigger_entity_config)
RestEntity.__init__(
self,
coordinator,
rest,
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._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 = {}

@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:
"""Update state from the rest data."""
value = self.rest.data_without_xml()

if self._json_attrs:
self._attr_extra_state_attributes = {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: this changes the behavior in case of attribute parsing failure.

  • previously, a failure would cause the attributes to a blank dict
  • now, a failure causes the previous attributes to be kept

What this change on purpose? (also see #97526)

Copy link
Member Author

Choose a reason for hiding this comment

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

We should not change this behavior in this PR at least.
So I see you have fixed that in the other PR so it comes back.
Thanks 👍

if value:
try:
json_dict = json_loads(value)
Expand Down Expand Up @@ -155,6 +190,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
Expand All @@ -165,8 +202,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()
14 changes: 14 additions & 0 deletions homeassistant/helpers/template_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
25 changes: 25 additions & 0 deletions tests/components/rest/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
ATTR_UNIT_OF_MEASUREMENT,
CONTENT_TYPE_JSON,
SERVICE_RELOAD,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfInformation,
UnitOfTemperature,
Expand Down Expand Up @@ -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