Add support for using a single endpoint for rest data (#46711)
This commit is contained in:
parent
71586b7661
commit
2f3c2f5f4d
15 changed files with 858 additions and 273 deletions
|
@ -1,4 +1,174 @@
|
|||
"""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"]
|
||||
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
|
||||
)
|
||||
|
|
|
@ -1,64 +1,27 @@
|
|||
"""Support for RESTful binary sensors."""
|
||||
import httpx
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
PLATFORM_SCHEMA,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
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_TIMEOUT,
|
||||
CONF_USERNAME,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_VERIFY_SSL,
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
|
||||
from . import DOMAIN, PLATFORMS
|
||||
from .data import DEFAULT_TIMEOUT, RestData
|
||||
from . import async_get_config_and_coordinator, create_rest_data_from_config
|
||||
from .entity import RestEntity
|
||||
from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA
|
||||
|
||||
DEFAULT_METHOD = "GET"
|
||||
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 = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA})
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
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):
|
||||
"""Set up the REST binary sensor."""
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
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)
|
||||
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:
|
||||
auth = None
|
||||
|
||||
rest = RestData(
|
||||
hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
|
||||
# Must update the sensor now (including fetching the rest resource) to
|
||||
# ensure it's updating its state.
|
||||
if discovery_info is not None:
|
||||
conf, coordinator, rest = await async_get_config_and_coordinator(
|
||||
hass, BINARY_SENSOR_DOMAIN, discovery_info
|
||||
)
|
||||
else:
|
||||
conf = config
|
||||
coordinator = None
|
||||
rest = create_rest_data_from_config(hass, conf)
|
||||
await rest.async_update()
|
||||
|
||||
if rest.data is None:
|
||||
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(
|
||||
[
|
||||
RestBinarySensor(
|
||||
hass,
|
||||
coordinator,
|
||||
rest,
|
||||
name,
|
||||
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."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
coordinator,
|
||||
rest,
|
||||
name,
|
||||
device_class,
|
||||
|
@ -137,36 +83,23 @@ class RestBinarySensor(BinarySensorEntity):
|
|||
resource_template,
|
||||
):
|
||||
"""Initialize a REST binary sensor."""
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
super().__init__(
|
||||
coordinator, rest, name, device_class, resource_template, force_update
|
||||
)
|
||||
self._state = False
|
||||
self._previous_data = None
|
||||
self._value_template = value_template
|
||||
self._force_update = force_update
|
||||
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
|
||||
self._is_on = None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""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:
|
||||
return False
|
||||
self._is_on = False
|
||||
|
||||
response = self.rest.data
|
||||
|
||||
|
@ -176,20 +109,8 @@ class RestBinarySensor(BinarySensorEntity):
|
|||
)
|
||||
|
||||
try:
|
||||
return bool(int(response))
|
||||
self._is_on = bool(int(response))
|
||||
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
|
||||
)
|
||||
|
||||
@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()
|
||||
|
|
20
homeassistant/components/rest/const.py
Normal file
20
homeassistant/components/rest/const.py
Normal 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"]
|
89
homeassistant/components/rest/entity.py
Normal file
89
homeassistant/components/rest/entity.py
Normal 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."""
|
|
@ -29,11 +29,8 @@ from homeassistant.const import (
|
|||
HTTP_OK,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.reload import setup_reload_service
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from . import DOMAIN, PLATFORMS
|
||||
|
||||
CONF_DATA = "data"
|
||||
CONF_DATA_TEMPLATE = "data_template"
|
||||
CONF_MESSAGE_PARAMETER_NAME = "message_param_name"
|
||||
|
@ -73,8 +70,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the RESTful notification service."""
|
||||
setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
method = config.get(CONF_METHOD)
|
||||
headers = config.get(CONF_HEADERS)
|
||||
|
|
99
homeassistant/components/rest/schema.py
Normal file
99
homeassistant/components/rest/schema.py
Normal 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,
|
||||
)
|
|
@ -3,76 +3,31 @@ import json
|
|||
import logging
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
import httpx
|
||||
from jsonpath import jsonpath
|
||||
import voluptuous as vol
|
||||
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 (
|
||||
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_TIMEOUT,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USERNAME,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_VERIFY_SSL,
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
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 .data import DEFAULT_TIMEOUT, RestData
|
||||
from . import async_get_config_and_coordinator, create_rest_data_from_config
|
||||
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__)
|
||||
|
||||
DEFAULT_METHOD = "GET"
|
||||
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 = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA})
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
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):
|
||||
"""Set up the RESTful sensor."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
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)
|
||||
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:
|
||||
auth = None
|
||||
rest = RestData(
|
||||
hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
|
||||
# Must update the sensor now (including fetching the rest resource) to
|
||||
# ensure it's updating its state.
|
||||
if discovery_info is not None:
|
||||
conf, coordinator, rest = await async_get_config_and_coordinator(
|
||||
hass, SENSOR_DOMAIN, discovery_info
|
||||
)
|
||||
|
||||
else:
|
||||
conf = config
|
||||
coordinator = None
|
||||
rest = create_rest_data_from_config(hass, conf)
|
||||
await rest.async_update()
|
||||
|
||||
if rest.data is None:
|
||||
raise PlatformNotReady
|
||||
|
||||
# Must update the sensor now (including fetching the rest resource) to
|
||||
# ensure it's updating its state.
|
||||
name = conf.get(CONF_NAME)
|
||||
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(
|
||||
[
|
||||
RestSensor(
|
||||
hass,
|
||||
coordinator,
|
||||
rest,
|
||||
name,
|
||||
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."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
coordinator,
|
||||
rest,
|
||||
name,
|
||||
unit_of_measurement,
|
||||
|
@ -161,60 +98,30 @@ class RestSensor(Entity):
|
|||
json_attrs_path,
|
||||
):
|
||||
"""Initialize the REST sensor."""
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
super().__init__(
|
||||
coordinator, rest, name, device_class, resource_template, force_update
|
||||
)
|
||||
self._state = None
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._device_class = device_class
|
||||
self._value_template = value_template
|
||||
self._json_attrs = json_attrs
|
||||
self._attributes = None
|
||||
self._force_update = force_update
|
||||
self._resource_template = resource_template
|
||||
self._json_attrs_path = json_attrs_path
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
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
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
"""Force update."""
|
||||
return self._force_update
|
||||
|
||||
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 device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._attributes
|
||||
|
||||
def _update_from_rest_data(self):
|
||||
"""Update state from the rest data."""
|
||||
|
@ -273,8 +180,3 @@ class RestSensor(Entity):
|
|||
)
|
||||
|
||||
self._state = value
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._attributes
|
||||
|
|
|
@ -22,12 +22,8 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
|
||||
from . import DOMAIN, PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BODY_OFF = "body_off"
|
||||
CONF_BODY_ON = "body_on"
|
||||
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):
|
||||
"""Set up the RESTful switch."""
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
body_off = config.get(CONF_BODY_OFF)
|
||||
body_on = config.get(CONF_BODY_ON)
|
||||
is_on_template = config.get(CONF_IS_ON_TEMPLATE)
|
||||
|
|
340
tests/components/rest/test_init.py
Normal file
340
tests/components/rest/test_init.py
Normal 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__)))
|
|
@ -2,6 +2,8 @@
|
|||
from os import path
|
||||
from unittest.mock import patch
|
||||
|
||||
import respx
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
import homeassistant.components.notify as notify
|
||||
from homeassistant.components.rest import DOMAIN
|
||||
|
@ -9,8 +11,10 @@ from homeassistant.const import SERVICE_RELOAD
|
|||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_reload_notify(hass):
|
||||
"""Verify we can reload the notify service."""
|
||||
respx.get("http://localhost") % 200
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
|
|
|
@ -91,6 +91,38 @@ async def test_setup_minimum(hass):
|
|||
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
|
||||
async def test_setup_minimum_resource_template(hass):
|
||||
"""Test setup with minimum configuration (resource_template)."""
|
||||
|
|
|
@ -3,6 +3,7 @@ import asyncio
|
|||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.rest import DOMAIN
|
||||
import homeassistant.components.rest.switch as rest
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import (
|
||||
|
@ -34,14 +35,14 @@ PARAMS = None
|
|||
|
||||
async def test_setup_missing_config(hass):
|
||||
"""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):
|
||||
"""Test setup with resource missing schema."""
|
||||
assert not await rest.async_setup_platform(
|
||||
hass,
|
||||
{CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "localhost"},
|
||||
{CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"},
|
||||
None,
|
||||
)
|
||||
|
||||
|
@ -51,7 +52,7 @@ async def test_setup_failed_connect(hass, aioclient_mock):
|
|||
aioclient_mock.get("http://localhost", exc=aiohttp.ClientError)
|
||||
assert not await rest.async_setup_platform(
|
||||
hass,
|
||||
{CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"},
|
||||
{CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"},
|
||||
None,
|
||||
)
|
||||
|
||||
|
@ -61,7 +62,7 @@ async def test_setup_timeout(hass, aioclient_mock):
|
|||
aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError())
|
||||
assert not await rest.async_setup_platform(
|
||||
hass,
|
||||
{CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"},
|
||||
{CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"},
|
||||
None,
|
||||
)
|
||||
|
||||
|
@ -75,11 +76,12 @@ async def test_setup_minimum(hass, aioclient_mock):
|
|||
SWITCH_DOMAIN,
|
||||
{
|
||||
SWITCH_DOMAIN: {
|
||||
CONF_PLATFORM: rest.DOMAIN,
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_RESOURCE: "http://localhost",
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
||||
|
||||
|
@ -92,12 +94,14 @@ async def test_setup_query_params(hass, aioclient_mock):
|
|||
SWITCH_DOMAIN,
|
||||
{
|
||||
SWITCH_DOMAIN: {
|
||||
CONF_PLATFORM: rest.DOMAIN,
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_RESOURCE: "http://localhost",
|
||||
CONF_PARAMS: {"search": "something"},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
print(aioclient_mock)
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
||||
|
@ -110,7 +114,7 @@ async def test_setup(hass, aioclient_mock):
|
|||
SWITCH_DOMAIN,
|
||||
{
|
||||
SWITCH_DOMAIN: {
|
||||
CONF_PLATFORM: rest.DOMAIN,
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_NAME: "foo",
|
||||
CONF_RESOURCE: "http://localhost",
|
||||
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_setup_component(1, SWITCH_DOMAIN)
|
||||
|
||||
|
@ -132,7 +137,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock):
|
|||
SWITCH_DOMAIN,
|
||||
{
|
||||
SWITCH_DOMAIN: {
|
||||
CONF_PLATFORM: rest.DOMAIN,
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_NAME: "foo",
|
||||
CONF_RESOURCE: "http://localhost",
|
||||
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_setup_component(1, SWITCH_DOMAIN)
|
||||
|
||||
|
|
0
tests/fixtures/rest/configuration_empty.yaml
vendored
Normal file
0
tests/fixtures/rest/configuration_empty.yaml
vendored
Normal file
2
tests/fixtures/rest/configuration_invalid.notyaml
vendored
Normal file
2
tests/fixtures/rest/configuration_invalid.notyaml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*!* NOT YAML
|
||||
|
12
tests/fixtures/rest/configuration_top_level.yaml
vendored
Normal file
12
tests/fixtures/rest/configuration_top_level.yaml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
rest:
|
||||
- method: GET
|
||||
resource: "http://localhost"
|
||||
sensor:
|
||||
name: fallover
|
||||
|
||||
sensor:
|
||||
- platform: rest
|
||||
resource: "http://localhost"
|
||||
method: GET
|
||||
name: rollout
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue