Add support for using a single endpoint for rest data (#46711)

This commit is contained in:
J. Nick Koston 2021-02-19 19:44:15 -10:00 committed by GitHub
parent 71586b7661
commit 2f3c2f5f4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 858 additions and 273 deletions

View file

@ -1,4 +1,174 @@
"""The rest component.""" """The rest component."""
DOMAIN = "rest" import asyncio
import logging
import httpx
import voluptuous as vol
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_HEADERS,
CONF_METHOD,
CONF_PARAMS,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_RESOURCE,
CONF_RESOURCE_TEMPLATE,
CONF_SCAN_INTERVAL,
CONF_TIMEOUT,
CONF_USERNAME,
CONF_VERIFY_SSL,
HTTP_DIGEST_AUTHENTICATION,
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import discovery
from homeassistant.helpers.entity_component import (
DEFAULT_SCAN_INTERVAL,
EntityComponent,
)
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_IDX
from .data import RestData
from .schema import CONFIG_SCHEMA # noqa:F401 pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor", "notify", "sensor", "switch"] PLATFORMS = ["binary_sensor", "notify", "sensor", "switch"]
COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the rest platforms."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
_async_setup_shared_data(hass)
async def reload_service_handler(service):
"""Remove all user-defined groups and load new ones from config."""
conf = await component.async_prepare_reload()
if conf is None:
return
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
_async_setup_shared_data(hass)
await _async_process_config(hass, conf)
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
)
return await _async_process_config(hass, config)
@callback
def _async_setup_shared_data(hass: HomeAssistant):
"""Create shared data for platform config and rest coordinators."""
hass.data[DOMAIN] = {platform: {} for platform in COORDINATOR_AWARE_PLATFORMS}
async def _async_process_config(hass, config) -> bool:
"""Process rest configuration."""
if DOMAIN not in config:
return True
refresh_tasks = []
load_tasks = []
for rest_idx, conf in enumerate(config[DOMAIN]):
scan_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
resource_template = conf.get(CONF_RESOURCE_TEMPLATE)
rest = create_rest_data_from_config(hass, conf)
coordinator = _wrap_rest_in_coordinator(
hass, rest, resource_template, scan_interval
)
refresh_tasks.append(coordinator.async_refresh())
hass.data[DOMAIN][rest_idx] = {REST: rest, COORDINATOR: coordinator}
for platform_domain in COORDINATOR_AWARE_PLATFORMS:
if platform_domain not in conf:
continue
for platform_idx, platform_conf in enumerate(conf[platform_domain]):
hass.data[DOMAIN][platform_domain][platform_idx] = platform_conf
load = discovery.async_load_platform(
hass,
platform_domain,
DOMAIN,
{REST_IDX: rest_idx, PLATFORM_IDX: platform_idx},
config,
)
load_tasks.append(load)
if refresh_tasks:
await asyncio.gather(*refresh_tasks)
if load_tasks:
await asyncio.gather(*load_tasks)
return True
async def async_get_config_and_coordinator(hass, platform_domain, discovery_info):
"""Get the config and coordinator for the platform from discovery."""
shared_data = hass.data[DOMAIN][discovery_info[REST_IDX]]
conf = hass.data[DOMAIN][platform_domain][discovery_info[PLATFORM_IDX]]
coordinator = shared_data[COORDINATOR]
rest = shared_data[REST]
if rest.data is None:
await coordinator.async_request_refresh()
return conf, coordinator, rest
def _wrap_rest_in_coordinator(hass, rest, resource_template, update_interval):
"""Wrap a DataUpdateCoordinator around the rest object."""
if resource_template:
async def _async_refresh_with_resource_template():
rest.set_url(resource_template.async_render(parse_result=False))
await rest.async_update()
update_method = _async_refresh_with_resource_template
else:
update_method = rest.async_update
return DataUpdateCoordinator(
hass,
_LOGGER,
name="rest data",
update_method=update_method,
update_interval=update_interval,
)
def create_rest_data_from_config(hass, config):
"""Create RestData from config."""
resource = config.get(CONF_RESOURCE)
resource_template = config.get(CONF_RESOURCE_TEMPLATE)
method = config.get(CONF_METHOD)
payload = config.get(CONF_PAYLOAD)
verify_ssl = config.get(CONF_VERIFY_SSL)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
headers = config.get(CONF_HEADERS)
params = config.get(CONF_PARAMS)
timeout = config.get(CONF_TIMEOUT)
if resource_template is not None:
resource_template.hass = hass
resource = resource_template.async_render(parse_result=False)
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
auth = httpx.DigestAuth(username, password)
else:
auth = (username, password)
else:
auth = None
return RestData(
hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
)

View file

@ -1,64 +1,27 @@
"""Support for RESTful binary sensors.""" """Support for RESTful binary sensors."""
import httpx
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
CONF_HEADERS,
CONF_METHOD,
CONF_NAME, CONF_NAME,
CONF_PARAMS,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_RESOURCE, CONF_RESOURCE,
CONF_RESOURCE_TEMPLATE, CONF_RESOURCE_TEMPLATE,
CONF_TIMEOUT,
CONF_USERNAME,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
) )
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.reload import async_setup_reload_service
from . import DOMAIN, PLATFORMS from . import async_get_config_and_coordinator, create_rest_data_from_config
from .data import DEFAULT_TIMEOUT, RestData from .entity import RestEntity
from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA
DEFAULT_METHOD = "GET" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA})
DEFAULT_NAME = "REST Binary Sensor"
DEFAULT_VERIFY_SSL = True
DEFAULT_FORCE_UPDATE = False
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url,
vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template,
vol.Optional(CONF_AUTHENTICATION): vol.In(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
),
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
vol.Optional(CONF_PARAMS): {cv.string: cv.string},
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(["POST", "GET"]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
)
PLATFORM_SCHEMA = vol.All( 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
@ -67,51 +30,34 @@ PLATFORM_SCHEMA = vol.All(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the REST binary sensor.""" """Set up the REST binary sensor."""
# Must update the sensor now (including fetching the rest resource) to
await async_setup_reload_service(hass, DOMAIN, PLATFORMS) # ensure it's updating its state.
if discovery_info is not None:
name = config.get(CONF_NAME) conf, coordinator, rest = await async_get_config_and_coordinator(
resource = config.get(CONF_RESOURCE) hass, BINARY_SENSOR_DOMAIN, discovery_info
resource_template = config.get(CONF_RESOURCE_TEMPLATE) )
method = config.get(CONF_METHOD)
payload = config.get(CONF_PAYLOAD)
verify_ssl = config.get(CONF_VERIFY_SSL)
timeout = config.get(CONF_TIMEOUT)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
headers = config.get(CONF_HEADERS)
params = config.get(CONF_PARAMS)
device_class = config.get(CONF_DEVICE_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
force_update = config.get(CONF_FORCE_UPDATE)
if resource_template is not None:
resource_template.hass = hass
resource = resource_template.async_render(parse_result=False)
if value_template is not None:
value_template.hass = hass
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
auth = httpx.DigestAuth(username, password)
else:
auth = (username, password)
else: else:
auth = None conf = config
coordinator = None
rest = RestData( rest = create_rest_data_from_config(hass, conf)
hass, method, resource, auth, headers, params, payload, verify_ssl, timeout await rest.async_update()
)
await rest.async_update()
if rest.data is None: if rest.data is None:
raise PlatformNotReady 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
async_add_entities( async_add_entities(
[ [
RestBinarySensor( RestBinarySensor(
hass, coordinator,
rest, rest,
name, name,
device_class, device_class,
@ -123,12 +69,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
) )
class RestBinarySensor(BinarySensorEntity): class RestBinarySensor(RestEntity, BinarySensorEntity):
"""Representation of a REST binary sensor.""" """Representation of a REST binary sensor."""
def __init__( def __init__(
self, self,
hass, coordinator,
rest, rest,
name, name,
device_class, device_class,
@ -137,36 +83,23 @@ class RestBinarySensor(BinarySensorEntity):
resource_template, resource_template,
): ):
"""Initialize a REST binary sensor.""" """Initialize a REST binary sensor."""
self._hass = hass super().__init__(
self.rest = rest coordinator, rest, name, device_class, resource_template, force_update
self._name = name )
self._device_class = device_class
self._state = False self._state = False
self._previous_data = None self._previous_data = None
self._value_template = value_template self._value_template = value_template
self._force_update = force_update self._is_on = None
self._resource_template = resource_template
@property
def name(self):
"""Return the name of the binary sensor."""
return self._name
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
@property
def available(self):
"""Return the availability of this sensor."""
return self.rest.data is not None
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return self._is_on
def _update_from_rest_data(self):
"""Update state from the rest data."""
if self.rest.data is None: if self.rest.data is None:
return False self._is_on = False
response = self.rest.data response = self.rest.data
@ -176,20 +109,8 @@ class RestBinarySensor(BinarySensorEntity):
) )
try: try:
return bool(int(response)) self._is_on = bool(int(response))
except ValueError: except ValueError:
return {"true": True, "on": True, "open": True, "yes": True}.get( self._is_on = {"true": True, "on": True, "open": True, "yes": True}.get(
response.lower(), False response.lower(), False
) )
@property
def force_update(self):
"""Force update."""
return self._force_update
async def async_update(self):
"""Get the latest data from REST API and updates the state."""
if self._resource_template is not None:
self.rest.set_url(self._resource_template.async_render(parse_result=False))
await self.rest.async_update()

View file

@ -0,0 +1,20 @@
"""The rest component constants."""
DOMAIN = "rest"
DEFAULT_METHOD = "GET"
DEFAULT_VERIFY_SSL = True
DEFAULT_FORCE_UPDATE = False
DEFAULT_BINARY_SENSOR_NAME = "REST Binary Sensor"
DEFAULT_SENSOR_NAME = "REST Sensor"
CONF_JSON_ATTRS = "json_attributes"
CONF_JSON_ATTRS_PATH = "json_attributes_path"
REST_IDX = "rest_idx"
PLATFORM_IDX = "platform_idx"
COORDINATOR = "coordinator"
REST = "rest"
METHODS = ["POST", "GET"]

View file

@ -0,0 +1,89 @@
"""The base entity for the rest component."""
from abc import abstractmethod
from typing import Any
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .data import RestData
class RestEntity(Entity):
"""A class for entities using DataUpdateCoordinator or rest data directly."""
def __init__(
self,
coordinator: DataUpdateCoordinator[Any],
rest: RestData,
name,
device_class,
resource_template,
force_update,
) -> None:
"""Create the entity that may have a coordinator."""
self.coordinator = coordinator
self.rest = rest
self._name = name
self._device_class = device_class
self._resource_template = resource_template
self._force_update = force_update
super().__init__()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
@property
def force_update(self):
"""Force update."""
return self._force_update
@property
def should_poll(self) -> bool:
"""Poll only if we do noty have a coordinator."""
return not self.coordinator
@property
def available(self):
"""Return the availability of this sensor."""
if self.coordinator and not self.coordinator.last_update_success:
return False
return self.rest.data is not None
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._update_from_rest_data()
if self.coordinator:
self.async_on_remove(
self.coordinator.async_add_listener(self._handle_coordinator_update)
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_from_rest_data()
self.async_write_ha_state()
async def async_update(self):
"""Get the latest data from REST API and update the state."""
if self.coordinator:
await self.coordinator.async_request_refresh()
return
if self._resource_template is not None:
self.rest.set_url(self._resource_template.async_render(parse_result=False))
await self.rest.async_update()
self._update_from_rest_data()
@abstractmethod
def _update_from_rest_data(self):
"""Update state from the rest data."""

View file

@ -29,11 +29,8 @@ from homeassistant.const import (
HTTP_OK, HTTP_OK,
) )
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import setup_reload_service
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from . import DOMAIN, PLATFORMS
CONF_DATA = "data" CONF_DATA = "data"
CONF_DATA_TEMPLATE = "data_template" CONF_DATA_TEMPLATE = "data_template"
CONF_MESSAGE_PARAMETER_NAME = "message_param_name" CONF_MESSAGE_PARAMETER_NAME = "message_param_name"
@ -73,8 +70,6 @@ _LOGGER = logging.getLogger(__name__)
def get_service(hass, config, discovery_info=None): def get_service(hass, config, discovery_info=None):
"""Get the RESTful notification service.""" """Get the RESTful notification service."""
setup_reload_service(hass, DOMAIN, PLATFORMS)
resource = config.get(CONF_RESOURCE) resource = config.get(CONF_RESOURCE)
method = config.get(CONF_METHOD) method = config.get(CONF_METHOD)
headers = config.get(CONF_HEADERS) headers = config.get(CONF_HEADERS)

View file

@ -0,0 +1,99 @@
"""The rest component schemas."""
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN,
)
from homeassistant.components.sensor import (
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
)
from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
CONF_HEADERS,
CONF_METHOD,
CONF_NAME,
CONF_PARAMS,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_RESOURCE,
CONF_RESOURCE_TEMPLATE,
CONF_SCAN_INTERVAL,
CONF_TIMEOUT,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
)
import homeassistant.helpers.config_validation as cv
from .const import (
CONF_JSON_ATTRS,
CONF_JSON_ATTRS_PATH,
DEFAULT_BINARY_SENSOR_NAME,
DEFAULT_FORCE_UPDATE,
DEFAULT_METHOD,
DEFAULT_SENSOR_NAME,
DEFAULT_VERIFY_SSL,
DOMAIN,
METHODS,
)
from .data import DEFAULT_TIMEOUT
RESOURCE_SCHEMA = {
vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url,
vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template,
vol.Optional(CONF_AUTHENTICATION): vol.In(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
),
vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
SENSOR_SCHEMA = {
vol.Optional(CONF_NAME, default=DEFAULT_SENSOR_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
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,
}
BINARY_SENSOR_SCHEMA = {
vol.Optional(CONF_NAME, default=DEFAULT_BINARY_SENSOR_NAME): cv.string,
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,
}
COMBINED_SCHEMA = vol.Schema(
{
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
**RESOURCE_SCHEMA,
vol.Optional(SENSOR_DOMAIN): vol.All(
cv.ensure_list, [vol.Schema(SENSOR_SCHEMA)]
),
vol.Optional(BINARY_SENSOR_DOMAIN): vol.All(
cv.ensure_list, [vol.Schema(BINARY_SENSOR_SCHEMA)]
),
}
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])},
extra=vol.ALLOW_EXTRA,
)

View file

@ -3,76 +3,31 @@ import json
import logging import logging
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
import httpx
from jsonpath import jsonpath from jsonpath import jsonpath
import voluptuous as vol import voluptuous as vol
import xmltodict import xmltodict
from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
CONF_HEADERS,
CONF_METHOD,
CONF_NAME, CONF_NAME,
CONF_PARAMS,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_RESOURCE, CONF_RESOURCE,
CONF_RESOURCE_TEMPLATE, CONF_RESOURCE_TEMPLATE,
CONF_TIMEOUT,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
) )
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 import Entity
from homeassistant.helpers.reload import async_setup_reload_service
from . import DOMAIN, PLATFORMS from . import async_get_config_and_coordinator, create_rest_data_from_config
from .data import DEFAULT_TIMEOUT, RestData from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH
from .entity import RestEntity
from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_METHOD = "GET" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA})
DEFAULT_NAME = "REST Sensor"
DEFAULT_VERIFY_SSL = True
DEFAULT_FORCE_UPDATE = False
CONF_JSON_ATTRS = "json_attributes"
CONF_JSON_ATTRS_PATH = "json_attributes_path"
METHODS = ["POST", "GET"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url,
vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template,
vol.Optional(CONF_AUTHENTICATION): vol.In(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
),
vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_JSON_ATTRS_PATH): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
)
PLATFORM_SCHEMA = vol.All( 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
@ -81,55 +36,37 @@ PLATFORM_SCHEMA = vol.All(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the RESTful sensor.""" """Set up the RESTful sensor."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS) # Must update the sensor now (including fetching the rest resource) to
# ensure it's updating its state.
name = config.get(CONF_NAME) if discovery_info is not None:
resource = config.get(CONF_RESOURCE) conf, coordinator, rest = await async_get_config_and_coordinator(
resource_template = config.get(CONF_RESOURCE_TEMPLATE) hass, SENSOR_DOMAIN, discovery_info
method = config.get(CONF_METHOD) )
payload = config.get(CONF_PAYLOAD)
verify_ssl = config.get(CONF_VERIFY_SSL)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
headers = config.get(CONF_HEADERS)
params = config.get(CONF_PARAMS)
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
device_class = config.get(CONF_DEVICE_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
json_attrs = config.get(CONF_JSON_ATTRS)
json_attrs_path = config.get(CONF_JSON_ATTRS_PATH)
force_update = config.get(CONF_FORCE_UPDATE)
timeout = config.get(CONF_TIMEOUT)
if value_template is not None:
value_template.hass = hass
if resource_template is not None:
resource_template.hass = hass
resource = resource_template.async_render(parse_result=False)
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
auth = httpx.DigestAuth(username, password)
else:
auth = (username, password)
else: else:
auth = None conf = config
rest = RestData( coordinator = None
hass, method, resource, auth, headers, params, payload, verify_ssl, timeout rest = create_rest_data_from_config(hass, conf)
) await rest.async_update()
await rest.async_update()
if rest.data is None: if rest.data is None:
raise PlatformNotReady raise PlatformNotReady
# Must update the sensor now (including fetching the rest resource) to name = conf.get(CONF_NAME)
# ensure it's updating its state. unit = conf.get(CONF_UNIT_OF_MEASUREMENT)
device_class = conf.get(CONF_DEVICE_CLASS)
json_attrs = conf.get(CONF_JSON_ATTRS)
json_attrs_path = conf.get(CONF_JSON_ATTRS_PATH)
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
async_add_entities( async_add_entities(
[ [
RestSensor( RestSensor(
hass, coordinator,
rest, rest,
name, name,
unit, unit,
@ -144,12 +81,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
) )
class RestSensor(Entity): class RestSensor(RestEntity):
"""Implementation of a REST sensor.""" """Implementation of a REST sensor."""
def __init__( def __init__(
self, self,
hass, coordinator,
rest, rest,
name, name,
unit_of_measurement, unit_of_measurement,
@ -161,60 +98,30 @@ class RestSensor(Entity):
json_attrs_path, json_attrs_path,
): ):
"""Initialize the REST sensor.""" """Initialize the REST sensor."""
self._hass = hass super().__init__(
self.rest = rest coordinator, rest, name, device_class, resource_template, force_update
self._name = name )
self._state = None self._state = None
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._device_class = device_class
self._value_template = value_template self._value_template = value_template
self._json_attrs = json_attrs self._json_attrs = json_attrs
self._attributes = None self._attributes = None
self._force_update = force_update
self._resource_template = resource_template
self._json_attrs_path = json_attrs_path self._json_attrs_path = json_attrs_path
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return self._unit_of_measurement return self._unit_of_measurement
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
@property
def available(self):
"""Return if the sensor data are available."""
return self.rest.data is not None
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return self._state
@property @property
def force_update(self): def device_state_attributes(self):
"""Force update.""" """Return the state attributes."""
return self._force_update return self._attributes
async def async_update(self):
"""Get the latest data from REST API and update the state."""
if self._resource_template is not None:
self.rest.set_url(self._resource_template.async_render(parse_result=False))
await self.rest.async_update()
self._update_from_rest_data()
async def async_added_to_hass(self):
"""Ensure the data from the initial update is reflected in the state."""
self._update_from_rest_data()
def _update_from_rest_data(self): def _update_from_rest_data(self):
"""Update state from the rest data.""" """Update state from the rest data."""
@ -273,8 +180,3 @@ class RestSensor(Entity):
) )
self._state = value self._state = value
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._attributes

View file

@ -22,12 +22,8 @@ from homeassistant.const import (
) )
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
from . import DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_BODY_OFF = "body_off" CONF_BODY_OFF = "body_off"
CONF_BODY_ON = "body_on" CONF_BODY_ON = "body_on"
CONF_IS_ON_TEMPLATE = "is_on_template" CONF_IS_ON_TEMPLATE = "is_on_template"
@ -65,9 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the RESTful switch.""" """Set up the RESTful switch."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
body_off = config.get(CONF_BODY_OFF) body_off = config.get(CONF_BODY_OFF)
body_on = config.get(CONF_BODY_ON) body_on = config.get(CONF_BODY_ON)
is_on_template = config.get(CONF_IS_ON_TEMPLATE) is_on_template = config.get(CONF_IS_ON_TEMPLATE)

View file

@ -0,0 +1,340 @@
"""Tests for rest component."""
import asyncio
from datetime import timedelta
from os import path
from unittest.mock import patch
import respx
from homeassistant import config as hass_config
from homeassistant.components.rest.const import DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
DATA_MEGABYTES,
SERVICE_RELOAD,
STATE_UNAVAILABLE,
)
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed
@respx.mock
async def test_setup_with_endpoint_timeout_with_recovery(hass):
"""Test setup with an endpoint that times out that recovers."""
await async_setup_component(hass, "homeassistant", {})
respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError())
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"method": "GET",
"verify_ssl": "false",
"timeout": 30,
"sensor": [
{
"unit_of_measurement": DATA_MEGABYTES,
"name": "sensor1",
"value_template": "{{ value_json.sensor1 }}",
},
{
"unit_of_measurement": DATA_MEGABYTES,
"name": "sensor2",
"value_template": "{{ value_json.sensor2 }}",
},
],
"binary_sensor": [
{
"name": "binary_sensor1",
"value_template": "{{ value_json.binary_sensor1 }}",
},
{
"name": "binary_sensor2",
"value_template": "{{ value_json.binary_sensor2 }}",
},
],
}
]
},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
respx.get("http://localhost").respond(
status_code=200,
json={
"sensor1": "1",
"sensor2": "2",
"binary_sensor1": "on",
"binary_sensor2": "off",
},
)
# Refresh the coordinator
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
await hass.async_block_till_done()
# Wait for platform setup retry
async_fire_time_changed(hass, utcnow() + timedelta(seconds=61))
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 4
assert hass.states.get("sensor.sensor1").state == "1"
assert hass.states.get("sensor.sensor2").state == "2"
assert hass.states.get("binary_sensor.binary_sensor1").state == "on"
assert hass.states.get("binary_sensor.binary_sensor2").state == "off"
# Now the end point flakes out again
respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError())
# Refresh the coordinator
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
await hass.async_block_till_done()
assert hass.states.get("sensor.sensor1").state == STATE_UNAVAILABLE
assert hass.states.get("sensor.sensor2").state == STATE_UNAVAILABLE
assert hass.states.get("binary_sensor.binary_sensor1").state == STATE_UNAVAILABLE
assert hass.states.get("binary_sensor.binary_sensor2").state == STATE_UNAVAILABLE
# We request a manual refresh when the
# endpoint is working again
respx.get("http://localhost").respond(
status_code=200,
json={
"sensor1": "1",
"sensor2": "2",
"binary_sensor1": "on",
"binary_sensor2": "off",
},
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["sensor.sensor1"]},
blocking=True,
)
assert hass.states.get("sensor.sensor1").state == "1"
assert hass.states.get("sensor.sensor2").state == "2"
assert hass.states.get("binary_sensor.binary_sensor1").state == "on"
assert hass.states.get("binary_sensor.binary_sensor2").state == "off"
@respx.mock
async def test_setup_minimum_resource_template(hass):
"""Test setup with minimum configuration (resource_template)."""
respx.get("http://localhost").respond(
status_code=200,
json={
"sensor1": "1",
"sensor2": "2",
"binary_sensor1": "on",
"binary_sensor2": "off",
},
)
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource_template": "{% set url = 'http://localhost' %}{{ url }}",
"method": "GET",
"verify_ssl": "false",
"timeout": 30,
"sensor": [
{
"unit_of_measurement": DATA_MEGABYTES,
"name": "sensor1",
"value_template": "{{ value_json.sensor1 }}",
},
{
"unit_of_measurement": DATA_MEGABYTES,
"name": "sensor2",
"value_template": "{{ value_json.sensor2 }}",
},
],
"binary_sensor": [
{
"name": "binary_sensor1",
"value_template": "{{ value_json.binary_sensor1 }}",
},
{
"name": "binary_sensor2",
"value_template": "{{ value_json.binary_sensor2 }}",
},
],
}
]
},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 4
assert hass.states.get("sensor.sensor1").state == "1"
assert hass.states.get("sensor.sensor2").state == "2"
assert hass.states.get("binary_sensor.binary_sensor1").state == "on"
assert hass.states.get("binary_sensor.binary_sensor2").state == "off"
@respx.mock
async def test_reload(hass):
"""Verify we can reload."""
respx.get("http://localhost") % 200
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"method": "GET",
"verify_ssl": "false",
"timeout": 30,
"sensor": [
{
"name": "mockrest",
},
],
}
]
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("sensor.mockrest")
yaml_path = path.join(
_get_fixtures_base_path(),
"fixtures",
"rest/configuration_top_level.yaml",
)
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
"rest",
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("sensor.mockreset") is None
assert hass.states.get("sensor.rollout")
assert hass.states.get("sensor.fallover")
@respx.mock
async def test_reload_and_remove_all(hass):
"""Verify we can reload and remove all."""
respx.get("http://localhost") % 200
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"method": "GET",
"verify_ssl": "false",
"timeout": 30,
"sensor": [
{
"name": "mockrest",
},
],
}
]
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("sensor.mockrest")
yaml_path = path.join(
_get_fixtures_base_path(),
"fixtures",
"rest/configuration_empty.yaml",
)
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
"rest",
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("sensor.mockreset") is None
@respx.mock
async def test_reload_fails_to_read_configuration(hass):
"""Verify reload when configuration is missing or broken."""
respx.get("http://localhost") % 200
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"method": "GET",
"verify_ssl": "false",
"timeout": 30,
"sensor": [
{
"name": "mockrest",
},
],
}
]
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
yaml_path = path.join(
_get_fixtures_base_path(),
"fixtures",
"rest/configuration_invalid.notyaml",
)
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
"rest",
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
def _get_fixtures_base_path():
return path.dirname(path.dirname(path.dirname(__file__)))

View file

@ -2,6 +2,8 @@
from os import path from os import path
from unittest.mock import patch from unittest.mock import patch
import respx
from homeassistant import config as hass_config from homeassistant import config as hass_config
import homeassistant.components.notify as notify import homeassistant.components.notify as notify
from homeassistant.components.rest import DOMAIN from homeassistant.components.rest import DOMAIN
@ -9,8 +11,10 @@ from homeassistant.const import SERVICE_RELOAD
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@respx.mock
async def test_reload_notify(hass): async def test_reload_notify(hass):
"""Verify we can reload the notify service.""" """Verify we can reload the notify service."""
respx.get("http://localhost") % 200
assert await async_setup_component( assert await async_setup_component(
hass, hass,

View file

@ -91,6 +91,38 @@ async def test_setup_minimum(hass):
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
@respx.mock
async def test_manual_update(hass):
"""Test setup with minimum configuration."""
await async_setup_component(hass, "homeassistant", {})
respx.get("http://localhost").respond(status_code=200, json={"data": "first"})
assert await async_setup_component(
hass,
sensor.DOMAIN,
{
"sensor": {
"name": "mysensor",
"value_template": "{{ value_json.data }}",
"platform": "rest",
"resource_template": "{% set url = 'http://localhost' %}{{ url }}",
"method": "GET",
}
},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("sensor.mysensor").state == "first"
respx.get("http://localhost").respond(status_code=200, json={"data": "second"})
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["sensor.mysensor"]},
blocking=True,
)
assert hass.states.get("sensor.mysensor").state == "second"
@respx.mock @respx.mock
async def test_setup_minimum_resource_template(hass): async def test_setup_minimum_resource_template(hass):
"""Test setup with minimum configuration (resource_template).""" """Test setup with minimum configuration (resource_template)."""

View file

@ -3,6 +3,7 @@ import asyncio
import aiohttp import aiohttp
from homeassistant.components.rest import DOMAIN
import homeassistant.components.rest.switch as rest import homeassistant.components.rest.switch as rest
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
@ -34,14 +35,14 @@ PARAMS = None
async def test_setup_missing_config(hass): async def test_setup_missing_config(hass):
"""Test setup with configuration missing required entries.""" """Test setup with configuration missing required entries."""
assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: rest.DOMAIN}, None) assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None)
async def test_setup_missing_schema(hass): async def test_setup_missing_schema(hass):
"""Test setup with resource missing schema.""" """Test setup with resource missing schema."""
assert not await rest.async_setup_platform( assert not await rest.async_setup_platform(
hass, hass,
{CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "localhost"}, {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"},
None, None,
) )
@ -51,7 +52,7 @@ async def test_setup_failed_connect(hass, aioclient_mock):
aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) aioclient_mock.get("http://localhost", exc=aiohttp.ClientError)
assert not await rest.async_setup_platform( assert not await rest.async_setup_platform(
hass, hass,
{CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"}, {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"},
None, None,
) )
@ -61,7 +62,7 @@ async def test_setup_timeout(hass, aioclient_mock):
aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError())
assert not await rest.async_setup_platform( assert not await rest.async_setup_platform(
hass, hass,
{CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"}, {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"},
None, None,
) )
@ -75,11 +76,12 @@ async def test_setup_minimum(hass, aioclient_mock):
SWITCH_DOMAIN, SWITCH_DOMAIN,
{ {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: rest.DOMAIN, CONF_PLATFORM: DOMAIN,
CONF_RESOURCE: "http://localhost", CONF_RESOURCE: "http://localhost",
} }
}, },
) )
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert aioclient_mock.call_count == 1
@ -92,12 +94,14 @@ async def test_setup_query_params(hass, aioclient_mock):
SWITCH_DOMAIN, SWITCH_DOMAIN,
{ {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: rest.DOMAIN, CONF_PLATFORM: DOMAIN,
CONF_RESOURCE: "http://localhost", CONF_RESOURCE: "http://localhost",
CONF_PARAMS: {"search": "something"}, CONF_PARAMS: {"search": "something"},
} }
}, },
) )
await hass.async_block_till_done()
print(aioclient_mock) print(aioclient_mock)
assert aioclient_mock.call_count == 1 assert aioclient_mock.call_count == 1
@ -110,7 +114,7 @@ async def test_setup(hass, aioclient_mock):
SWITCH_DOMAIN, SWITCH_DOMAIN,
{ {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: rest.DOMAIN, CONF_PLATFORM: DOMAIN,
CONF_NAME: "foo", CONF_NAME: "foo",
CONF_RESOURCE: "http://localhost", CONF_RESOURCE: "http://localhost",
CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON},
@ -119,6 +123,7 @@ async def test_setup(hass, aioclient_mock):
} }
}, },
) )
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert aioclient_mock.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN) assert_setup_component(1, SWITCH_DOMAIN)
@ -132,7 +137,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock):
SWITCH_DOMAIN, SWITCH_DOMAIN,
{ {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: rest.DOMAIN, CONF_PLATFORM: DOMAIN,
CONF_NAME: "foo", CONF_NAME: "foo",
CONF_RESOURCE: "http://localhost", CONF_RESOURCE: "http://localhost",
rest.CONF_STATE_RESOURCE: "http://localhost/state", rest.CONF_STATE_RESOURCE: "http://localhost/state",
@ -142,6 +147,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock):
} }
}, },
) )
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert aioclient_mock.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN) assert_setup_component(1, SWITCH_DOMAIN)

View file

View file

@ -0,0 +1,2 @@
*!* NOT YAML

View file

@ -0,0 +1,12 @@
rest:
- method: GET
resource: "http://localhost"
sensor:
name: fallover
sensor:
- platform: rest
resource: "http://localhost"
method: GET
name: rollout