Migrate rest binary_sensor and switch to TemplateEntity (#73307)

This commit is contained in:
Erik Montnemery 2022-06-28 22:53:38 +02:00 committed by GitHub
parent abf67c3153
commit 146ff83a16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 178 additions and 142 deletions

View file

@ -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):

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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",
}

View file

@ -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",
}