From 146ff83a1612c8d8d3fe6641cefc1aeda439671a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 22:53:38 +0200 Subject: [PATCH] Migrate rest binary_sensor and switch to TemplateEntity (#73307) --- .../components/rest/binary_sensor.py | 52 ++++---- homeassistant/components/rest/entity.py | 23 +--- homeassistant/components/rest/schema.py | 9 +- homeassistant/components/rest/sensor.py | 6 +- homeassistant/components/rest/switch.py | 118 +++++++----------- .../components/template/binary_sensor.py | 2 - homeassistant/helpers/template_entity.py | 4 +- tests/components/rest/test_binary_sensor.py | 39 ++++++ tests/components/rest/test_switch.py | 67 ++++++++-- 9 files changed, 178 insertions(+), 142 deletions(-) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 2beed53522a..bc51433c3c5 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -11,18 +11,20 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, - CONF_NAME, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, + CONF_UNIQUE_ID, 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 TemplateEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import async_get_config_and_coordinator, create_rest_data_from_config +from .const import DEFAULT_BINARY_SENSOR_NAME from .entity import RestEntity from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA @@ -57,51 +59,55 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - name = conf.get(CONF_NAME) - device_class = conf.get(CONF_DEVICE_CLASS) - value_template = conf.get(CONF_VALUE_TEMPLATE) - force_update = conf.get(CONF_FORCE_UPDATE) - resource_template = conf.get(CONF_RESOURCE_TEMPLATE) - - if value_template is not None: - value_template.hass = hass + unique_id = conf.get(CONF_UNIQUE_ID) async_add_entities( [ RestBinarySensor( + hass, coordinator, rest, - name, - device_class, - value_template, - force_update, - resource_template, + conf, + unique_id, ) ], ) -class RestBinarySensor(RestEntity, BinarySensorEntity): +class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): """Representation of a REST binary sensor.""" def __init__( self, + hass, coordinator, rest, - name, - device_class, - value_template, - force_update, - resource_template, + config, + unique_id, ): """Initialize a REST binary sensor.""" - super().__init__(coordinator, rest, name, resource_template, force_update) + RestEntity.__init__( + self, + coordinator, + rest, + config.get(CONF_RESOURCE_TEMPLATE), + config.get(CONF_FORCE_UPDATE), + ) + TemplateEntity.__init__( + self, + hass, + config=config, + fallback_name=DEFAULT_BINARY_SENSOR_NAME, + unique_id=unique_id, + ) self._state = False self._previous_data = None - self._value_template = value_template + self._value_template = config.get(CONF_VALUE_TEMPLATE) + if (value_template := self._value_template) is not None: + value_template.hass = hass self._is_on = None - self._attr_device_class = device_class + self._attr_device_class = config.get(CONF_DEVICE_CLASS) @property def is_on(self): diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py index f0476dc7d33..5d7a65b3d48 100644 --- a/homeassistant/components/rest/entity.py +++ b/homeassistant/components/rest/entity.py @@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .data import RestData -class BaseRestEntity(Entity): +class RestEntity(Entity): """A class for entities using DataUpdateCoordinator or rest data directly.""" def __init__( @@ -72,24 +72,3 @@ class BaseRestEntity(Entity): @abstractmethod def _update_from_rest_data(self): """Update state from the rest data.""" - - -class RestEntity(BaseRestEntity): - """A class for entities using DataUpdateCoordinator or rest data directly.""" - - def __init__( - self, - coordinator: DataUpdateCoordinator[Any], - rest: RestData, - name, - resource_template, - force_update, - ) -> None: - """Create the entity that may have a coordinator.""" - self._name = name - super().__init__(coordinator, rest, resource_template, force_update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d25bb50167b..f881dc8b028 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_HEADERS, CONF_METHOD, - CONF_NAME, CONF_PARAMS, CONF_PASSWORD, CONF_PAYLOAD, @@ -28,12 +27,14 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA +from homeassistant.helpers.template_entity import ( + TEMPLATE_ENTITY_BASE_SCHEMA, + TEMPLATE_SENSOR_BASE_SCHEMA, +) from .const import ( CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, - DEFAULT_BINARY_SENSOR_NAME, DEFAULT_FORCE_UPDATE, DEFAULT_METHOD, DEFAULT_VERIFY_SSL, @@ -67,7 +68,7 @@ SENSOR_SCHEMA = { } BINARY_SENSOR_SCHEMA = { - vol.Optional(CONF_NAME, default=DEFAULT_BINARY_SENSOR_NAME): cv.string, + **TEMPLATE_ENTITY_BASE_SCHEMA.schema, vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 93a96aeb94f..ff571b0c9dc 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import async_get_config_and_coordinator, create_rest_data_from_config from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, DEFAULT_SENSOR_NAME -from .entity import BaseRestEntity +from .entity import RestEntity from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -82,7 +82,7 @@ async def async_setup_platform( ) -class RestSensor(BaseRestEntity, TemplateSensor): +class RestSensor(RestEntity, TemplateSensor): """Implementation of a REST sensor.""" def __init__( @@ -94,7 +94,7 @@ class RestSensor(BaseRestEntity, TemplateSensor): unique_id, ): """Initialize the REST sensor.""" - BaseRestEntity.__init__( + RestEntity.__init__( self, coordinator, rest, diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 10214970cce..c45eb581645 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -18,11 +18,11 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HEADERS, CONF_METHOD, - CONF_NAME, CONF_PARAMS, CONF_PASSWORD, CONF_RESOURCE, CONF_TIMEOUT, + CONF_UNIQUE_ID, CONF_USERNAME, CONF_VERIFY_SSL, ) @@ -30,6 +30,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template_entity import ( + TEMPLATE_ENTITY_BASE_SCHEMA, + TemplateEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -49,6 +53,7 @@ SUPPORT_REST_METHODS = ["post", "put", "patch"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { + **TEMPLATE_ENTITY_BASE_SCHEMA.schema, vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_STATE_RESOURCE): cv.url, vol.Optional(CONF_HEADERS): {cv.string: cv.template}, @@ -59,7 +64,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All( vol.Lower, vol.In(SUPPORT_REST_METHODS) ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, @@ -76,50 +80,11 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the RESTful switch.""" - body_off = config.get(CONF_BODY_OFF) - body_on = config.get(CONF_BODY_ON) - is_on_template = config.get(CONF_IS_ON_TEMPLATE) - method = config.get(CONF_METHOD) - headers = config.get(CONF_HEADERS) - params = config.get(CONF_PARAMS) - name = config.get(CONF_NAME) - device_class = config.get(CONF_DEVICE_CLASS) - username = config.get(CONF_USERNAME) resource = config.get(CONF_RESOURCE) - state_resource = config.get(CONF_STATE_RESOURCE) or resource - verify_ssl = config.get(CONF_VERIFY_SSL) - - auth = None - if username: - auth = aiohttp.BasicAuth(username, password=config[CONF_PASSWORD]) - - if is_on_template is not None: - is_on_template.hass = hass - if body_on is not None: - body_on.hass = hass - if body_off is not None: - body_off.hass = hass - - template.attach(hass, headers) - template.attach(hass, params) - timeout = config.get(CONF_TIMEOUT) + unique_id = config.get(CONF_UNIQUE_ID) try: - switch = RestSwitch( - name, - device_class, - resource, - state_resource, - method, - headers, - params, - auth, - body_on, - body_off, - is_on_template, - timeout, - verify_ssl, - ) + switch = RestSwitch(hass, config, unique_id) req = await switch.get_device_state(hass) if req.status >= HTTPStatus.BAD_REQUEST: @@ -135,46 +100,53 @@ async def async_setup_platform( _LOGGER.error("No route to resource/endpoint: %s", resource) -class RestSwitch(SwitchEntity): +class RestSwitch(TemplateEntity, SwitchEntity): """Representation of a switch that can be toggled using REST.""" def __init__( self, - name, - device_class, - resource, - state_resource, - method, - headers, - params, - auth, - body_on, - body_off, - is_on_template, - timeout, - verify_ssl, + hass, + config, + unique_id, ): """Initialize the REST switch.""" + TemplateEntity.__init__( + self, + hass, + config=config, + fallback_name=DEFAULT_NAME, + unique_id=unique_id, + ) + self._state = None - self._name = name - self._resource = resource - self._state_resource = state_resource - self._method = method - self._headers = headers - self._params = params + + auth = None + if username := config.get(CONF_USERNAME): + auth = aiohttp.BasicAuth(username, password=config[CONF_PASSWORD]) + + self._resource = config.get(CONF_RESOURCE) + self._state_resource = config.get(CONF_STATE_RESOURCE) or self._resource + self._method = config.get(CONF_METHOD) + self._headers = config.get(CONF_HEADERS) + self._params = config.get(CONF_PARAMS) self._auth = auth - self._body_on = body_on - self._body_off = body_off - self._is_on_template = is_on_template - self._timeout = timeout - self._verify_ssl = verify_ssl + self._body_on = config.get(CONF_BODY_ON) + self._body_off = config.get(CONF_BODY_OFF) + self._is_on_template = config.get(CONF_IS_ON_TEMPLATE) + self._timeout = config.get(CONF_TIMEOUT) + self._verify_ssl = config.get(CONF_VERIFY_SSL) - self._attr_device_class = device_class + self._attr_device_class = config.get(CONF_DEVICE_CLASS) - @property - def name(self): - """Return the name of the switch.""" - return self._name + if (is_on_template := self._is_on_template) is not None: + is_on_template.hass = hass + if (body_on := self._body_on) is not None: + body_on.hass = hass + if (body_off := self._body_off) is not None: + body_off.hass = hass + + template.attach(hass, self._headers) + template.attach(hass, self._params) @property def is_on(self): diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e91a2925b06..ab7c88e8b8c 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -82,9 +82,7 @@ BINARY_SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): cv.template, vol.Required(CONF_STATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index e92ba233121..83d321e3fa9 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -43,16 +43,16 @@ CONF_PICTURE = "picture" TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema( { vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_NAME): cv.template, vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): cv.template, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 8383d53b51f..a6655f6ddbc 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -19,6 +19,8 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -431,3 +433,40 @@ async def test_setup_query_params(hass): ) await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 1 + + +@respx.mock +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + Platform.BINARY_SENSOR: { + # REST configuration + "platform": "rest", + "method": "GET", + "resource": "http://localhost", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "name": "{{'REST' + ' ' + 'Binary Sensor'}}", + "unique_id": "very_unique", + }, + } + + respx.get("http://localhost") % HTTPStatus.OK + assert await async_setup_component(hass, Platform.BINARY_SENSOR, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get("binary_sensor.rest_binary_sensor").unique_id + == "very_unique" + ) + + state = hass.states.get("binary_sensor.rest_binary_sensor") + assert state.state == "off" + assert state.attributes == { + "entity_picture": "blabla.png", + "friendly_name": "REST Binary Sensor", + "icon": "mdi:one_two_three", + } diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 3dbef91ffb5..a3c0f78db1c 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -8,6 +8,7 @@ from homeassistant.components.rest import DOMAIN import homeassistant.components.rest.switch as rest from homeassistant.components.switch import SwitchDeviceClass from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_HEADERS, CONF_NAME, CONF_PARAMS, @@ -16,17 +17,19 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, Platform, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component from tests.common import assert_setup_component +from tests.test_util.aiohttp import AiohttpClientMocker NAME = "foo" DEVICE_CLASS = SwitchDeviceClass.SWITCH METHOD = "post" RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE -AUTH = None PARAMS = None @@ -187,19 +190,22 @@ def _setup_test_switch(hass): body_off = Template("off", hass) headers = {"Content-type": Template(CONTENT_TYPE_JSON, hass)} switch = rest.RestSwitch( - NAME, - DEVICE_CLASS, - RESOURCE, - STATE_RESOURCE, - METHOD, - headers, - PARAMS, - AUTH, - body_on, - body_off, + hass, + { + CONF_NAME: Template(NAME, hass), + CONF_DEVICE_CLASS: DEVICE_CLASS, + CONF_RESOURCE: RESOURCE, + rest.CONF_STATE_RESOURCE: STATE_RESOURCE, + rest.CONF_METHOD: METHOD, + rest.CONF_HEADERS: headers, + rest.CONF_PARAMS: PARAMS, + rest.CONF_BODY_ON: body_on, + rest.CONF_BODY_OFF: body_off, + rest.CONF_IS_ON_TEMPLATE: None, + rest.CONF_TIMEOUT: 10, + rest.CONF_VERIFY_SSL: True, + }, None, - 10, - True, ) switch.hass = hass return switch, body_on, body_off @@ -315,3 +321,38 @@ async def test_update_timeout(hass, aioclient_mock): await switch.async_update() assert switch.is_on is None + + +async def test_entity_config( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test entity configuration.""" + + aioclient_mock.get("http://localhost", status=HTTPStatus.OK) + config = { + Platform.SWITCH: { + # REST configuration + "platform": "rest", + "method": "POST", + "resource": "http://localhost", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "name": "{{'REST' + ' ' + 'Switch'}}", + "unique_id": "very_unique", + }, + } + + assert await async_setup_component(hass, Platform.SWITCH, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("switch.rest_switch").unique_id == "very_unique" + + state = hass.states.get("switch.rest_switch") + assert state.state == "unknown" + assert state.attributes == { + "entity_picture": "blabla.png", + "friendly_name": "REST Switch", + "icon": "mdi:one_two_three", + }