diff --git a/.coveragerc b/.coveragerc
index 6f0a5e5e6b0..e45ee866613 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -721,7 +721,6 @@ omit =
homeassistant/components/repetier/__init__.py
homeassistant/components/repetier/sensor.py
homeassistant/components/remote_rpi_gpio/*
- homeassistant/components/rest/binary_sensor.py
homeassistant/components/rest/notify.py
homeassistant/components/rest/switch.py
homeassistant/components/ring/camera.py
diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py
index 169086af3fc..188c49ca6ee 100644
--- a/homeassistant/components/pvoutput/sensor.py
+++ b/homeassistant/components/pvoutput/sensor.py
@@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the PVOutput sensor."""
name = config.get(CONF_NAME)
api_key = config.get(CONF_API_KEY)
@@ -54,13 +54,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id}
rest = RestData(method, _ENDPOINT, auth, headers, payload, verify_ssl)
- rest.update()
+ await rest.async_update()
if rest.data is None:
_LOGGER.error("Unable to fetch data from PVOutput")
return False
- add_entities([PvoutputSensor(rest, name)], True)
+ async_add_entities([PvoutputSensor(rest, name)], True)
class PvoutputSensor(Entity):
@@ -112,11 +112,15 @@ class PvoutputSensor(Entity):
ATTR_VOLTAGE: self.pvcoutput.voltage,
}
- def update(self):
+ async def async_update(self):
"""Get the latest data from the PVOutput API and updates the state."""
try:
- self.rest.update()
+ await self.rest.async_update()
self.pvcoutput = self.status._make(self.rest.data.split(","))
except TypeError:
self.pvcoutput = None
_LOGGER.error("Unable to fetch data from PVOutput. %s", self.rest.data)
+
+ async def async_will_remove_from_hass(self):
+ """Shutdown the session."""
+ await self.rest.async_remove()
diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py
index c5b8f16162a..82df088b01a 100644
--- a/homeassistant/components/rest/binary_sensor.py
+++ b/homeassistant/components/rest/binary_sensor.py
@@ -1,7 +1,7 @@
"""Support for RESTful binary sensors."""
import logging
-from requests.auth import HTTPBasicAuth, HTTPDigestAuth
+import httpx
import voluptuous as vol
from homeassistant.components.binary_sensor import (
@@ -29,7 +29,7 @@ from homeassistant.const import (
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.reload import setup_reload_service
+from homeassistant.helpers.reload import async_setup_reload_service
from . import DOMAIN, PLATFORMS
from .sensor import RestData
@@ -68,10 +68,10 @@ PLATFORM_SCHEMA = vol.All(
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the REST binary sensor."""
- setup_reload_service(hass, DOMAIN, PLATFORMS)
+ await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
name = config.get(CONF_NAME)
resource = config.get(CONF_RESOURCE)
@@ -96,18 +96,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
- auth = HTTPDigestAuth(username, password)
+ auth = httpx.DigestAuth(username, password)
else:
- auth = HTTPBasicAuth(username, password)
+ auth = (username, password)
else:
auth = None
rest = RestData(method, resource, auth, headers, payload, verify_ssl, timeout)
- rest.update()
+ await rest.async_update()
if rest.data is None:
raise PlatformNotReady
- add_entities(
+ async_add_entities(
[
RestBinarySensor(
hass,
@@ -118,7 +118,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
force_update,
resource_template,
)
- ]
+ ],
+ True,
)
@@ -186,9 +187,13 @@ class RestBinarySensor(BinarySensorEntity):
"""Force update."""
return self._force_update
- def update(self):
+ async def async_will_remove_from_hass(self):
+ """Shutdown the session."""
+ await self.rest.async_remove()
+
+ 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.render())
- self.rest.update()
+ await self.rest.async_update()
diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json
index 3ab926a3b13..198bc971f21 100644
--- a/homeassistant/components/rest/manifest.json
+++ b/homeassistant/components/rest/manifest.json
@@ -2,6 +2,6 @@
"domain": "rest",
"name": "RESTful",
"documentation": "https://www.home-assistant.io/integrations/rest",
- "requirements": ["jsonpath==0.82", "xmltodict==0.12.0"],
+ "requirements": ["jsonpath==0.82", "xmltodict==0.12.0", "httpx==0.16.1"],
"codeowners": []
}
diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py
index 9925eb016cb..0a7fa6ef90f 100644
--- a/homeassistant/components/rest/sensor.py
+++ b/homeassistant/components/rest/sensor.py
@@ -3,10 +3,8 @@ import json
import logging
from xml.parsers.expat import ExpatError
+import httpx
from jsonpath import jsonpath
-import requests
-from requests import Session
-from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
import xmltodict
@@ -33,7 +31,7 @@ from homeassistant.const import (
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.reload import setup_reload_service
+from homeassistant.helpers.reload import async_setup_reload_service
from . import DOMAIN, PLATFORMS
@@ -79,9 +77,9 @@ PLATFORM_SCHEMA = vol.All(
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the RESTful sensor."""
- setup_reload_service(hass, DOMAIN, PLATFORMS)
+ await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
name = config.get(CONF_NAME)
resource = config.get(CONF_RESOURCE)
@@ -109,19 +107,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
- auth = HTTPDigestAuth(username, password)
+ auth = httpx.DigestAuth(username, password)
else:
- auth = HTTPBasicAuth(username, password)
+ auth = (username, password)
else:
auth = None
rest = RestData(method, resource, auth, headers, payload, verify_ssl, timeout)
- rest.update()
+ 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.
- add_entities(
+ async_add_entities(
[
RestSensor(
hass,
@@ -200,12 +199,13 @@ class RestSensor(Entity):
"""Force update."""
return self._force_update
- def update(self):
+ 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.render())
- self.rest.update()
+ await self.rest.async_update()
+
value = self.rest.data
_LOGGER.debug("Data fetched from resource: %s", value)
if self.rest.headers is not None:
@@ -250,13 +250,21 @@ class RestSensor(Entity):
except ValueError:
_LOGGER.warning("REST result could not be parsed as JSON")
_LOGGER.debug("Erroneous JSON: %s", value)
+
else:
_LOGGER.warning("Empty reply found when expecting JSON data")
+
if value is not None and self._value_template is not None:
- value = self._value_template.render_with_possible_json_value(value, None)
+ value = self._value_template.async_render_with_possible_json_value(
+ value, None
+ )
self._state = value
+ async def async_will_remove_from_hass(self):
+ """Shutdown the session."""
+ await self.rest.async_remove()
+
@property
def device_state_attributes(self):
"""Return the state attributes."""
@@ -267,7 +275,14 @@ class RestData:
"""Class for handling the data retrieval."""
def __init__(
- self, method, resource, auth, headers, data, verify_ssl, timeout=DEFAULT_TIMEOUT
+ self,
+ method,
+ resource,
+ auth,
+ headers,
+ data,
+ verify_ssl,
+ timeout=DEFAULT_TIMEOUT,
):
"""Initialize the data object."""
self._method = method
@@ -275,36 +290,39 @@ class RestData:
self._auth = auth
self._headers = headers
self._request_data = data
- self._verify_ssl = verify_ssl
self._timeout = timeout
- self._http_session = Session()
+ self._verify_ssl = verify_ssl
+ self._async_client = None
self.data = None
self.headers = None
- def __del__(self):
+ async def async_remove(self):
"""Destroy the http session on destroy."""
- self._http_session.close()
+ if self._async_client:
+ await self._async_client.aclose()
def set_url(self, url):
"""Set url."""
self._resource = url
- def update(self):
+ async def async_update(self):
"""Get the latest data from REST service with provided method."""
+ if not self._async_client:
+ self._async_client = httpx.AsyncClient(verify=self._verify_ssl)
+
_LOGGER.debug("Updating from %s", self._resource)
try:
- response = self._http_session.request(
+ response = await self._async_client.request(
self._method,
self._resource,
headers=self._headers,
auth=self._auth,
data=self._request_data,
timeout=self._timeout,
- verify=self._verify_ssl,
)
self.data = response.text
self.headers = response.headers
- except requests.exceptions.RequestException as ex:
+ except httpx.RequestError as ex:
_LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex)
self.data = None
self.headers = None
diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index 3d25e4a34ae..1979f6f744d 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -53,7 +53,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Web scrape sensor."""
name = config.get(CONF_NAME)
resource = config.get(CONF_RESOURCE)
@@ -79,12 +79,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else:
auth = None
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
- rest.update()
+ await rest.async_update()
if rest.data is None:
raise PlatformNotReady
- add_entities(
+ async_add_entities(
[ScrapeSensor(rest, name, select, attr, index, value_template, unit)], True
)
@@ -118,9 +118,9 @@ class ScrapeSensor(Entity):
"""Return the state of the device."""
return self._state
- def update(self):
+ async def async_update(self):
"""Get the latest data from the source and updates the state."""
- self.rest.update()
+ await self.rest.async_update()
if self.rest.data is None:
_LOGGER.error("Unable to retrieve data for %s", self.name)
return
@@ -143,8 +143,12 @@ class ScrapeSensor(Entity):
return
if self._value_template is not None:
- self._state = self._value_template.render_with_possible_json_value(
+ self._state = self._value_template.async_render_with_possible_json_value(
value, None
)
else:
self._state = value
+
+ async def async_will_remove_from_hass(self):
+ """Shutdown the session."""
+ await self.rest.async_remove()
diff --git a/requirements_all.txt b/requirements_all.txt
index 71250de125b..380b13d391f 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -777,6 +777,9 @@ horimote==0.4.1
# homeassistant.components.remember_the_milk
httplib2==0.10.3
+# homeassistant.components.rest
+httpx==0.16.1
+
# homeassistant.components.huawei_lte
huawei-lte-api==1.4.12
diff --git a/requirements_test.txt b/requirements_test.txt
index 3ec0782ca70..744765bec77 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -24,5 +24,6 @@ pytest-xdist==2.1.0
pytest==6.0.2
requests_mock==1.8.0
responses==0.12.0
+respx==0.14.0
stdlib-list==0.7.0
tqdm==4.49.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 79e2525b4d9..c63473b4464 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -400,6 +400,9 @@ homematicip==0.11.0
# homeassistant.components.remember_the_milk
httplib2==0.10.3
+# homeassistant.components.rest
+httpx==0.16.1
+
# homeassistant.components.huawei_lte
huawei-lte-api==1.4.12
diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py
index b18d8f300cf..276dc293f87 100644
--- a/tests/components/rest/test_binary_sensor.py
+++ b/tests/components/rest/test_binary_sensor.py
@@ -1,267 +1,381 @@
"""The tests for the REST binary sensor platform."""
-import unittest
-import pytest
-from pytest import raises
-import requests
-from requests.exceptions import Timeout
-import requests_mock
+import asyncio
+from os import path
+import httpx
+import respx
+
+from homeassistant import config as hass_config
import homeassistant.components.binary_sensor as binary_sensor
-import homeassistant.components.rest.binary_sensor as rest
-from homeassistant.const import CONTENT_TYPE_JSON, STATE_OFF, STATE_ON
-from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers import template
-from homeassistant.setup import setup_component
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONTENT_TYPE_JSON,
+ SERVICE_RELOAD,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.setup import async_setup_component
from tests.async_mock import Mock, patch
-from tests.common import assert_setup_component, get_test_home_assistant
-class TestRestBinarySensorSetup(unittest.TestCase):
- """Tests for setting up the REST binary sensor platform."""
-
- DEVICES = []
-
- def add_devices(self, devices, update_before_add=False):
- """Mock add devices."""
- for device in devices:
- self.DEVICES.append(device)
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- # Reset for this test.
- self.DEVICES = []
- self.addCleanup(self.hass.stop)
-
- def test_setup_missing_config(self):
- """Test setup with configuration missing required entries."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "rest"}}
- )
-
- def test_setup_missing_schema(self):
- """Test setup with resource missing schema."""
- with pytest.raises(PlatformNotReady):
- rest.setup_platform(
- self.hass,
- {"platform": "rest", "resource": "localhost", "method": "GET"},
- None,
- )
-
- @patch("requests.Session.send", side_effect=requests.exceptions.ConnectionError())
- def test_setup_failed_connect(self, mock_req):
- """Test setup when connection error occurs."""
- with raises(PlatformNotReady):
- rest.setup_platform(
- self.hass,
- {"platform": "rest", "resource": "http://localhost", "method": "GET"},
- self.add_devices,
- None,
- )
- assert len(self.DEVICES) == 0
-
- @patch("requests.Session.send", side_effect=Timeout())
- def test_setup_timeout(self, mock_req):
- """Test setup when connection timeout occurs."""
- with raises(PlatformNotReady):
- rest.setup_platform(
- self.hass,
- {"platform": "rest", "resource": "http://localhost", "method": "GET"},
- self.add_devices,
- None,
- )
- assert len(self.DEVICES) == 0
-
- @requests_mock.Mocker()
- def test_setup_minimum(self, mock_req):
- """Test setup with minimum configuration."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(1, "binary_sensor"):
- assert setup_component(
- self.hass,
- "binary_sensor",
- {"binary_sensor": {"platform": "rest", "resource": "http://localhost"}},
- )
- self.hass.block_till_done()
- assert 1 == mock_req.call_count
-
- @requests_mock.Mocker()
- def test_setup_minimum_resource_template(self, mock_req):
- """Test setup with minimum configuration (resource_template)."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(1, "binary_sensor"):
- assert setup_component(
- self.hass,
- "binary_sensor",
- {
- "binary_sensor": {
- "platform": "rest",
- "resource_template": "http://localhost",
- }
- },
- )
- self.hass.block_till_done()
- assert mock_req.call_count == 1
-
- @requests_mock.Mocker()
- def test_setup_duplicate_resource(self, mock_req):
- """Test setup with duplicate resources."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(0, "binary_sensor"):
- assert setup_component(
- self.hass,
- "binary_sensor",
- {
- "binary_sensor": {
- "platform": "rest",
- "resource": "http://localhost",
- "resource_template": "http://localhost",
- }
- },
- )
- self.hass.block_till_done()
-
- @requests_mock.Mocker()
- def test_setup_get(self, mock_req):
- """Test setup with valid configuration."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(1, "binary_sensor"):
- assert setup_component(
- self.hass,
- "binary_sensor",
- {
- "binary_sensor": {
- "platform": "rest",
- "resource": "http://localhost",
- "method": "GET",
- "value_template": "{{ value_json.key }}",
- "name": "foo",
- "verify_ssl": "true",
- "authentication": "basic",
- "username": "my username",
- "password": "my password",
- "headers": {"Accept": CONTENT_TYPE_JSON},
- }
- },
- )
- self.hass.block_till_done()
- assert 1 == mock_req.call_count
-
- @requests_mock.Mocker()
- def test_setup_post(self, mock_req):
- """Test setup with valid configuration."""
- mock_req.post("http://localhost", status_code=200)
- with assert_setup_component(1, "binary_sensor"):
- assert setup_component(
- self.hass,
- "binary_sensor",
- {
- "binary_sensor": {
- "platform": "rest",
- "resource": "http://localhost",
- "method": "POST",
- "value_template": "{{ value_json.key }}",
- "payload": '{ "device": "toaster"}',
- "name": "foo",
- "verify_ssl": "true",
- "authentication": "basic",
- "username": "my username",
- "password": "my password",
- "headers": {"Accept": CONTENT_TYPE_JSON},
- }
- },
- )
- self.hass.block_till_done()
- assert 1 == mock_req.call_count
+async def test_setup_missing_basic_config(hass):
+ """Test setup with configuration missing required entries."""
+ assert await async_setup_component(
+ hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "rest"}}
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
-class TestRestBinarySensor(unittest.TestCase):
- """Tests for REST binary sensor platform."""
+async def test_setup_missing_config(hass):
+ """Test setup with configuration missing required entries."""
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "localhost",
+ "method": "GET",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.rest = Mock("RestData")
- self.rest.update = Mock(
- "RestData.update", side_effect=self.update_side_effect('{ "key": false }')
+
+@respx.mock
+async def test_setup_failed_connect(hass):
+ """Test setup when connection error occurs."""
+ respx.get(
+ "http://localhost", content=httpx.RequestError(message="any", request=Mock())
+ )
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+@respx.mock
+async def test_setup_timeout(hass):
+ """Test setup when connection timeout occurs."""
+ respx.get("http://localhost", content=asyncio.TimeoutError())
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "localhost",
+ "method": "GET",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+@respx.mock
+async def test_setup_minimum(hass):
+ """Test setup with minimum configuration."""
+ respx.get("http://localhost", status_code=200)
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_minimum_resource_template(hass):
+ """Test setup with minimum configuration (resource_template)."""
+ respx.get("http://localhost", status_code=200)
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource_template": "http://localhost",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_duplicate_resource_template(hass):
+ """Test setup with duplicate resources."""
+ respx.get("http://localhost", status_code=200)
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "resource_template": "http://localhost",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+@respx.mock
+async def test_setup_get(hass):
+ """Test setup with valid configuration."""
+ respx.get("http://localhost", status_code=200, content="{}")
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "name": "foo",
+ "verify_ssl": "true",
+ "timeout": 30,
+ "authentication": "basic",
+ "username": "my username",
+ "password": "my password",
+ "headers": {"Accept": CONTENT_TYPE_JSON},
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_get_digest_auth(hass):
+ """Test setup with valid configuration."""
+ respx.get("http://localhost", status_code=200, content="{}")
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "name": "foo",
+ "verify_ssl": "true",
+ "timeout": 30,
+ "authentication": "digest",
+ "username": "my username",
+ "password": "my password",
+ "headers": {"Accept": CONTENT_TYPE_JSON},
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_post(hass):
+ """Test setup with valid configuration."""
+ respx.post("http://localhost", status_code=200, content="{}")
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "POST",
+ "value_template": "{{ value_json.key }}",
+ "payload": '{ "device": "toaster"}',
+ "name": "foo",
+ "verify_ssl": "true",
+ "timeout": 30,
+ "authentication": "basic",
+ "username": "my username",
+ "password": "my password",
+ "headers": {"Accept": CONTENT_TYPE_JSON},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_get_off(hass):
+ """Test setup with valid off configuration."""
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "text/json"},
+ content='{"dog": false}',
+ )
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.dog }}",
+ "name": "foo",
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("binary_sensor.foo")
+ assert state.state == STATE_OFF
+
+
+@respx.mock
+async def test_setup_get_on(hass):
+ """Test setup with valid on configuration."""
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "text/json"},
+ content='{"dog": true}',
+ )
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.dog }}",
+ "name": "foo",
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("binary_sensor.foo")
+ assert state.state == STATE_ON
+
+
+@respx.mock
+async def test_setup_with_exception(hass):
+ """Test setup with exception."""
+ respx.get("http://localhost", status_code=200, content="{}")
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.dog }}",
+ "name": "foo",
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("binary_sensor.foo")
+ assert state.state == STATE_OFF
+
+ await async_setup_component(hass, "homeassistant", {})
+ await hass.async_block_till_done()
+
+ respx.clear()
+ respx.get(
+ "http://localhost", content=httpx.RequestError(message="any", request=Mock())
+ )
+ await hass.services.async_call(
+ "homeassistant",
+ "update_entity",
+ {ATTR_ENTITY_ID: ["binary_sensor.foo"]},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.foo")
+ assert state.state == STATE_UNAVAILABLE
+
+
+@respx.mock
+async def test_reload(hass):
+ """Verify we can reload reset sensors."""
+
+ respx.get("http://localhost", status_code=200)
+
+ await async_setup_component(
+ hass,
+ "binary_sensor",
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "method": "GET",
+ "name": "mockrest",
+ "resource": "http://localhost",
+ }
+ },
+ )
+ 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("binary_sensor.mockrest")
+
+ yaml_path = path.join(
+ _get_fixtures_base_path(),
+ "fixtures",
+ "rest/configuration.yaml",
+ )
+ with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
+ await hass.services.async_call(
+ "rest",
+ SERVICE_RELOAD,
+ {},
+ blocking=True,
)
- self.name = "foo"
- self.device_class = "light"
- self.value_template = template.Template("{{ value_json.key }}", self.hass)
- self.force_update = False
- self.resource_template = None
+ await hass.async_block_till_done()
- self.binary_sensor = rest.RestBinarySensor(
- self.hass,
- self.rest,
- self.name,
- self.device_class,
- self.value_template,
- self.force_update,
- self.resource_template,
- )
- self.addCleanup(self.hass.stop)
+ assert hass.states.get("binary_sensor.mockreset") is None
+ assert hass.states.get("binary_sensor.rollout")
- def update_side_effect(self, data):
- """Side effect function for mocking RestData.update()."""
- self.rest.data = data
- def test_name(self):
- """Test the name."""
- assert self.name == self.binary_sensor.name
-
- def test_device_class(self):
- """Test the device class."""
- assert self.device_class == self.binary_sensor.device_class
-
- def test_initial_state(self):
- """Test the initial state."""
- self.binary_sensor.update()
- assert STATE_OFF == self.binary_sensor.state
-
- def test_update_when_value_is_none(self):
- """Test state gets updated to unknown when sensor returns no data."""
- self.rest.update = Mock(
- "RestData.update", side_effect=self.update_side_effect(None)
- )
- self.binary_sensor.update()
- assert not self.binary_sensor.available
-
- def test_update_when_value_changed(self):
- """Test state gets updated when sensor returns a new status."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect('{ "key": true }'),
- )
- self.binary_sensor.update()
- assert STATE_ON == self.binary_sensor.state
- assert self.binary_sensor.available
-
- def test_update_when_failed_request(self):
- """Test state gets updated when sensor returns a new status."""
- self.rest.update = Mock(
- "rest.RestData.update", side_effect=self.update_side_effect(None)
- )
- self.binary_sensor.update()
- assert not self.binary_sensor.available
-
- def test_update_with_no_template(self):
- """Test update when there is no value template."""
- self.rest.update = Mock(
- "rest.RestData.update", side_effect=self.update_side_effect("true")
- )
- self.binary_sensor = rest.RestBinarySensor(
- self.hass,
- self.rest,
- self.name,
- self.device_class,
- None,
- self.force_update,
- self.resource_template,
- )
- self.binary_sensor.update()
- assert STATE_ON == self.binary_sensor.state
- assert self.binary_sensor.available
+def _get_fixtures_base_path():
+ return path.dirname(path.dirname(path.dirname(__file__)))
diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py
index 5ffa12c6167..6a7e444cedd 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -1,696 +1,665 @@
"""The tests for the REST sensor platform."""
+import asyncio
from os import path
-import unittest
-import pytest
-from pytest import raises
-import requests
-from requests.exceptions import RequestException, Timeout
-from requests.structures import CaseInsensitiveDict
-import requests_mock
+import httpx
+import respx
from homeassistant import config as hass_config
-import homeassistant.components.rest.sensor as rest
import homeassistant.components.sensor as sensor
from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
CONTENT_TYPE_JSON,
- CONTENT_TYPE_TEXT_PLAIN,
DATA_MEGABYTES,
SERVICE_RELOAD,
+ STATE_UNKNOWN,
)
-from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.config_validation import template
-from homeassistant.setup import async_setup_component, setup_component
+from homeassistant.setup import async_setup_component
from tests.async_mock import Mock, patch
-from tests.common import assert_setup_component, get_test_home_assistant
-class TestRestSensorSetup(unittest.TestCase):
- """Tests for setting up the REST sensor platform."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- def test_setup_missing_config(self):
- """Test setup with configuration missing required entries."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass, sensor.DOMAIN, {"sensor": {"platform": "rest"}}
- )
-
- def test_setup_missing_schema(self):
- """Test setup with resource missing schema."""
- with pytest.raises(PlatformNotReady):
- rest.setup_platform(
- self.hass,
- {"platform": "rest", "resource": "localhost", "method": "GET"},
- None,
- )
-
- @patch("requests.Session.send", side_effect=requests.exceptions.ConnectionError())
- def test_setup_failed_connect(self, mock_req):
- """Test setup when connection error occurs."""
- with raises(PlatformNotReady):
- rest.setup_platform(
- self.hass,
- {"platform": "rest", "resource": "http://localhost", "method": "GET"},
- lambda devices, update=True: None,
- )
-
- @patch("requests.Session.send", side_effect=Timeout())
- def test_setup_timeout(self, mock_req):
- """Test setup when connection timeout occurs."""
- with raises(PlatformNotReady):
- rest.setup_platform(
- self.hass,
- {"platform": "rest", "resource": "http://localhost", "method": "GET"},
- lambda devices, update=True: None,
- )
-
- @requests_mock.Mocker()
- def test_setup_minimum(self, mock_req):
- """Test setup with minimum configuration."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(1, "sensor"):
- assert setup_component(
- self.hass,
- "sensor",
- {"sensor": {"platform": "rest", "resource": "http://localhost"}},
- )
- self.hass.block_till_done()
- assert 2 == mock_req.call_count
-
- @requests_mock.Mocker()
- def test_setup_minimum_resource_template(self, mock_req):
- """Test setup with minimum configuration (resource_template)."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(1, "sensor"):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "rest",
- "resource_template": "http://localhost",
- }
- },
- )
- self.hass.block_till_done()
- assert mock_req.call_count == 2
-
- @requests_mock.Mocker()
- def test_setup_duplicate_resource(self, mock_req):
- """Test setup with duplicate resources."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(0, "sensor"):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "rest",
- "resource": "http://localhost",
- "resource_template": "http://localhost",
- }
- },
- )
- self.hass.block_till_done()
-
- @requests_mock.Mocker()
- def test_setup_get(self, mock_req):
- """Test setup with valid configuration."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(1, "sensor"):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "rest",
- "resource": "http://localhost",
- "method": "GET",
- "value_template": "{{ value_json.key }}",
- "name": "foo",
- "unit_of_measurement": DATA_MEGABYTES,
- "verify_ssl": "true",
- "timeout": 30,
- "authentication": "basic",
- "username": "my username",
- "password": "my password",
- "headers": {"Accept": CONTENT_TYPE_JSON},
- }
- },
- )
- self.hass.block_till_done()
- assert 2 == mock_req.call_count
-
- @requests_mock.Mocker()
- def test_setup_post(self, mock_req):
- """Test setup with valid configuration."""
- mock_req.post("http://localhost", status_code=200)
- with assert_setup_component(1, "sensor"):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "rest",
- "resource": "http://localhost",
- "method": "POST",
- "value_template": "{{ value_json.key }}",
- "payload": '{ "device": "toaster"}',
- "name": "foo",
- "unit_of_measurement": DATA_MEGABYTES,
- "verify_ssl": "true",
- "timeout": 30,
- "authentication": "basic",
- "username": "my username",
- "password": "my password",
- "headers": {"Accept": CONTENT_TYPE_JSON},
- }
- },
- )
- self.hass.block_till_done()
- assert 2 == mock_req.call_count
-
- @requests_mock.Mocker()
- def test_setup_get_xml(self, mock_req):
- """Test setup with valid configuration."""
- mock_req.get("http://localhost", status_code=200)
- with assert_setup_component(1, "sensor"):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "rest",
- "resource": "http://localhost",
- "method": "GET",
- "value_template": "{{ value_json.key }}",
- "name": "foo",
- "unit_of_measurement": DATA_MEGABYTES,
- "verify_ssl": "true",
- "timeout": 30,
- "authentication": "basic",
- "username": "my username",
- "password": "my password",
- "headers": {"Accept": "text/xml"},
- }
- },
- )
- self.hass.block_till_done()
- assert 2 == mock_req.call_count
+async def test_setup_missing_config(hass):
+ """Test setup with configuration missing required entries."""
+ assert await async_setup_component(
+ hass, sensor.DOMAIN, {"sensor": {"platform": "rest"}}
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
-class TestRestSensor(unittest.TestCase):
- """Tests for REST sensor platform."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.initial_state = "initial_state"
- self.rest = Mock("rest.RestData")
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '{ "key": "' + self.initial_state + '" }',
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
- ),
- )
- self.name = "foo"
- self.unit_of_measurement = DATA_MEGABYTES
- self.device_class = None
- self.value_template = template("{{ value_json.key }}")
- self.json_attrs_path = None
- self.value_template.hass = self.hass
- self.force_update = False
- self.resource_template = None
-
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- self.value_template,
- [],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.addCleanup(self.hass.stop)
-
- def update_side_effect(self, data, headers):
- """Side effect function for mocking RestData.update()."""
- self.rest.data = data
- self.rest.headers = headers
-
- def test_name(self):
- """Test the name."""
- assert self.name == self.sensor.name
-
- def test_unit_of_measurement(self):
- """Test the unit of measurement."""
- assert self.unit_of_measurement == self.sensor.unit_of_measurement
-
- def test_force_update(self):
- """Test the unit of measurement."""
- assert self.force_update == self.sensor.force_update
-
- def test_state(self):
- """Test the initial state."""
- self.sensor.update()
- assert self.initial_state == self.sensor.state
-
- def test_update_when_value_is_none(self):
- """Test state gets updated to unknown when sensor returns no data."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(None, CaseInsensitiveDict()),
- )
- self.sensor.update()
- assert self.sensor.state is None
- assert not self.sensor.available
-
- def test_update_when_value_changed(self):
- """Test state gets updated when sensor returns a new status."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '{ "key": "updated_state" }',
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
- ),
- )
- self.sensor.update()
- assert "updated_state" == self.sensor.state
- assert self.sensor.available
-
- def test_update_with_no_template(self):
- """Test update when there is no value template."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- "plain_state", CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON})
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- None,
- [],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.sensor.update()
- assert "plain_state" == self.sensor.state
- assert self.sensor.available
-
- def test_update_with_json_attrs(self):
- """Test attributes get extracted from a JSON result."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '{ "key": "some_json_value" }',
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- None,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.sensor.update()
- assert "some_json_value" == self.sensor.device_state_attributes["key"]
-
- def test_update_with_json_attrs_list_dict(self):
- """Test attributes get extracted from a JSON list[0] result."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '[{ "key": "another_value" }]',
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- None,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.sensor.update()
- assert "another_value" == self.sensor.device_state_attributes["key"]
-
- @patch("homeassistant.components.rest.sensor._LOGGER")
- def test_update_with_json_attrs_no_data(self, mock_logger):
- """Test attributes when no JSON result fetched."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- None, CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON})
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- None,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
-
- @patch("homeassistant.components.rest.sensor._LOGGER")
- def test_update_with_json_attrs_not_dict(self, mock_logger):
- """Test attributes get extracted from a JSON result."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '["list", "of", "things"]',
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- None,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
-
- @patch("homeassistant.components.rest.sensor._LOGGER")
- def test_update_with_json_attrs_bad_JSON(self, mock_logger):
- """Test attributes get extracted from a JSON result."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- "This is text rather than JSON data.",
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_TEXT_PLAIN}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- None,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
- assert mock_logger.debug.called
-
- def test_update_with_json_attrs_and_template(self):
- """Test attributes get extracted from a JSON result."""
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '{ "key": "json_state_updated_value" }',
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- self.value_template,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
- self.sensor.update()
-
- assert "json_state_updated_value" == self.sensor.state
- assert (
- "json_state_updated_value" == self.sensor.device_state_attributes["key"]
- ), self.force_update
-
- def test_update_with_json_attrs_with_json_attrs_path(self):
- """Test attributes get extracted from a JSON result with a template for the attributes."""
- json_attrs_path = "$.toplevel.second_level"
- value_template = template("{{ value_json.toplevel.master_value }}")
- value_template.hass = self.hass
-
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }',
- CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- value_template,
- ["some_json_key", "some_json_key2"],
- self.force_update,
- self.resource_template,
- json_attrs_path,
- )
-
- self.sensor.update()
- assert "some_json_value" == self.sensor.device_state_attributes["some_json_key"]
- assert (
- "some_json_value2" == self.sensor.device_state_attributes["some_json_key2"]
- )
- assert "master" == self.sensor.state
-
- def test_update_with_xml_convert_json_attrs_with_json_attrs_path(self):
- """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes."""
- json_attrs_path = "$.toplevel.second_level"
- value_template = template("{{ value_json.toplevel.master_value }}")
- value_template.hass = self.hass
-
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- "mastersome_json_valuesome_json_value2",
- CaseInsensitiveDict({"Content-Type": "text/xml+svg"}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- value_template,
- ["some_json_key", "some_json_key2"],
- self.force_update,
- self.resource_template,
- json_attrs_path,
- )
-
- self.sensor.update()
- assert "some_json_value" == self.sensor.device_state_attributes["some_json_key"]
- assert (
- "some_json_value2" == self.sensor.device_state_attributes["some_json_key2"]
- )
- assert "master" == self.sensor.state
-
- def test_update_with_xml_convert_json_attrs_with_jsonattr_template(self):
- """Test attributes get extracted from a JSON result that was converted from XML."""
- json_attrs_path = "$.response"
- value_template = template("{{ value_json.response.bss.wlan }}")
- value_template.hass = self.hass
-
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- '01255648alexander000bogus000000000upupupup000x0XF0x0XF 0',
- CaseInsensitiveDict({"Content-Type": "text/xml"}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- value_template,
- ["led0", "led1", "temp0", "time0", "ver"],
- self.force_update,
- self.resource_template,
- json_attrs_path,
- )
-
- self.sensor.update()
- assert "0" == self.sensor.device_state_attributes["led0"]
- assert "0" == self.sensor.device_state_attributes["led1"]
- assert "0x0XF0x0XF" == self.sensor.device_state_attributes["temp0"]
- assert "0" == self.sensor.device_state_attributes["time0"]
- assert "12556" == self.sensor.device_state_attributes["ver"]
- assert "bogus" == self.sensor.state
-
- def test_update_with_application_xml_convert_json_attrs_with_jsonattr_template(
- self,
- ):
- """Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type."""
- json_attrs_path = "$.main"
- value_template = template("{{ value_json.main.dog }}")
- value_template.hass = self.hass
-
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- "13",
- CaseInsensitiveDict({"Content-Type": "application/xml"}),
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- value_template,
- ["dog", "cat"],
- self.force_update,
- self.resource_template,
- json_attrs_path,
- )
-
- self.sensor.update()
- assert "3" == self.sensor.device_state_attributes["cat"]
- assert "1" == self.sensor.device_state_attributes["dog"]
- assert "1" == self.sensor.state
-
- @patch("homeassistant.components.rest.sensor._LOGGER")
- def test_update_with_xml_convert_bad_xml(self, mock_logger):
- """Test attributes get extracted from a XML result with bad xml."""
- value_template = template("{{ value_json.toplevel.master_value }}")
- value_template.hass = self.hass
-
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(
- "this is not xml", CaseInsensitiveDict({"Content-Type": "text/xml"})
- ),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- value_template,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
-
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
- assert mock_logger.debug.called
-
- @patch("homeassistant.components.rest.sensor._LOGGER")
- def test_update_with_failed_get(self, mock_logger):
- """Test attributes get extracted from a XML result with bad xml."""
- value_template = template("{{ value_json.toplevel.master_value }}")
- value_template.hass = self.hass
-
- self.rest.update = Mock(
- "rest.RestData.update",
- side_effect=self.update_side_effect(None, None),
- )
- self.sensor = rest.RestSensor(
- self.hass,
- self.rest,
- self.name,
- self.unit_of_measurement,
- self.device_class,
- value_template,
- ["key"],
- self.force_update,
- self.resource_template,
- self.json_attrs_path,
- )
-
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
- assert mock_logger.debug.called
- assert self.sensor.state is None
- assert self.sensor.available is False
+async def test_setup_missing_schema(hass):
+ """Test setup with resource missing schema."""
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {"sensor": {"platform": "rest", "resource": "localhost", "method": "GET"}},
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
-class TestRestData(unittest.TestCase):
- """Tests for RestData."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.method = "GET"
- self.resource = "http://localhost"
- self.verify_ssl = True
- self.timeout = 10
- self.rest = rest.RestData(
- self.method, self.resource, None, None, None, self.verify_ssl, self.timeout
- )
-
- @requests_mock.Mocker()
- def test_update(self, mock_req):
- """Test update."""
- mock_req.get("http://localhost", text="test data")
- self.rest.update()
- assert "test data" == self.rest.data
-
- @patch("requests.Session.request", side_effect=RequestException)
- def test_update_request_exception(self, mock_req):
- """Test update when a request exception occurs."""
- self.rest.update()
- assert self.rest.data is None
+@respx.mock
+async def test_setup_failed_connect(hass):
+ """Test setup when connection error occurs."""
+ respx.get(
+ "http://localhost", content=httpx.RequestError(message="any", request=Mock())
+ )
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
-async def test_reload(hass, requests_mock):
+@respx.mock
+async def test_setup_timeout(hass):
+ """Test setup when connection timeout occurs."""
+ respx.get("http://localhost", content=asyncio.TimeoutError())
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {"sensor": {"platform": "rest", "resource": "localhost", "method": "GET"}},
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+@respx.mock
+async def test_setup_minimum(hass):
+ """Test setup with minimum configuration."""
+ respx.get("http://localhost", status_code=200)
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_minimum_resource_template(hass):
+ """Test setup with minimum configuration (resource_template)."""
+ respx.get("http://localhost", status_code=200)
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource_template": "http://localhost",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_duplicate_resource_template(hass):
+ """Test setup with duplicate resources."""
+ respx.get("http://localhost", status_code=200)
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "resource_template": "http://localhost",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+@respx.mock
+async def test_setup_get(hass):
+ """Test setup with valid configuration."""
+ respx.get("http://localhost", status_code=200, content="{}")
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "authentication": "basic",
+ "username": "my username",
+ "password": "my password",
+ "headers": {"Accept": CONTENT_TYPE_JSON},
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_get_digest_auth(hass):
+ """Test setup with valid configuration."""
+ respx.get("http://localhost", status_code=200, content="{}")
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "authentication": "digest",
+ "username": "my username",
+ "password": "my password",
+ "headers": {"Accept": CONTENT_TYPE_JSON},
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_post(hass):
+ """Test setup with valid configuration."""
+ respx.post("http://localhost", status_code=200, content="{}")
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "POST",
+ "value_template": "{{ value_json.key }}",
+ "payload": '{ "device": "toaster"}',
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "authentication": "basic",
+ "username": "my username",
+ "password": "my password",
+ "headers": {"Accept": CONTENT_TYPE_JSON},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
+@respx.mock
+async def test_setup_get_xml(hass):
+ """Test setup with valid xml configuration."""
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "text/xml"},
+ content="abc",
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.dog }}",
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("sensor.foo")
+ assert state.state == "abc"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_MEGABYTES
+
+
+@respx.mock
+async def test_update_with_json_attrs(hass):
+ """Test attributes get extracted from a JSON result."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": CONTENT_TYPE_JSON},
+ content='{ "key": "some_json_value" }',
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "json_attributes": ["key"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("sensor.foo")
+ assert state.state == "some_json_value"
+ assert state.attributes["key"] == "some_json_value"
+
+
+@respx.mock
+async def test_update_with_no_template(hass):
+ """Test update when there is no value template."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": CONTENT_TYPE_JSON},
+ content='{ "key": "some_json_value" }',
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "json_attributes": ["key"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "headers": {"Accept": "text/xml"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("sensor.foo")
+ assert state.state == '{ "key": "some_json_value" }'
+
+
+@respx.mock
+async def test_update_with_json_attrs_no_data(hass, caplog):
+ """Test attributes when no JSON result fetched."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": CONTENT_TYPE_JSON},
+ content="",
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "json_attributes": ["key"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "headers": {"Accept": "text/xml"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("sensor.foo")
+ assert state.state == STATE_UNKNOWN
+ assert state.attributes == {"unit_of_measurement": "MB", "friendly_name": "foo"}
+ assert "Empty reply" in caplog.text
+
+
+@respx.mock
+async def test_update_with_json_attrs_not_dict(hass, caplog):
+ """Test attributes get extracted from a JSON result."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": CONTENT_TYPE_JSON},
+ content='["list", "of", "things"]',
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "json_attributes": ["key"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "headers": {"Accept": "text/xml"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("sensor.foo")
+ assert state.state == ""
+ assert state.attributes == {"unit_of_measurement": "MB", "friendly_name": "foo"}
+ assert "not a dictionary or list" in caplog.text
+
+
+@respx.mock
+async def test_update_with_json_attrs_bad_JSON(hass, caplog):
+ """Test attributes get extracted from a JSON result."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": CONTENT_TYPE_JSON},
+ content="This is text rather than JSON data.",
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "json_attributes": ["key"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "headers": {"Accept": "text/xml"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("sensor.foo")
+ assert state.state == STATE_UNKNOWN
+ assert state.attributes == {"unit_of_measurement": "MB", "friendly_name": "foo"}
+ assert "Erroneous JSON" in caplog.text
+
+
+@respx.mock
+async def test_update_with_json_attrs_with_json_attrs_path(hass):
+ """Test attributes get extracted from a JSON result with a template for the attributes."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": CONTENT_TYPE_JSON},
+ content='{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }',
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.toplevel.master_value }}",
+ "json_attributes_path": "$.toplevel.second_level",
+ "json_attributes": ["some_json_key", "some_json_key2"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "headers": {"Accept": "text/xml"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("sensor.foo")
+
+ assert state.state == "master"
+ assert state.attributes["some_json_key"] == "some_json_value"
+ assert state.attributes["some_json_key2"] == "some_json_value2"
+
+
+@respx.mock
+async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass):
+ """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "text/xml"},
+ content="mastersome_json_valuesome_json_value2",
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.toplevel.master_value }}",
+ "json_attributes_path": "$.toplevel.second_level",
+ "json_attributes": ["some_json_key", "some_json_key2"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("sensor.foo")
+
+ assert state.state == "master"
+ assert state.attributes["some_json_key"] == "some_json_value"
+ assert state.attributes["some_json_key2"] == "some_json_value2"
+
+
+@respx.mock
+async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass):
+ """Test attributes get extracted from a JSON result that was converted from XML."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "text/xml"},
+ content='01255648alexander000bogus000000000upupupup000x0XF0x0XF 0',
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.response.bss.wlan }}",
+ "json_attributes_path": "$.response",
+ "json_attributes": ["led0", "led1", "temp0", "time0", "ver"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("sensor.foo")
+
+ assert state.state == "bogus"
+ assert state.attributes["led0"] == "0"
+ assert state.attributes["led1"] == "0"
+ assert state.attributes["temp0"] == "0x0XF0x0XF"
+ assert state.attributes["time0"] == "0"
+ assert state.attributes["ver"] == "12556"
+
+
+@respx.mock
+async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_template(
+ hass,
+):
+ """Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "application/xml"},
+ content="13",
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.main.dog }}",
+ "json_attributes_path": "$.main",
+ "json_attributes": ["dog", "cat"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("sensor.foo")
+
+ assert state.state == "1"
+ assert state.attributes["dog"] == "1"
+ assert state.attributes["cat"] == "3"
+
+
+@respx.mock
+async def test_update_with_xml_convert_bad_xml(hass, caplog):
+ """Test attributes get extracted from a XML result with bad xml."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "text/xml"},
+ content="",
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.toplevel.master_value }}",
+ "json_attributes": ["key"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("sensor.foo")
+
+ assert state.state == STATE_UNKNOWN
+ assert "Erroneous XML" in caplog.text
+ assert "Empty reply" in caplog.text
+
+
+@respx.mock
+async def test_update_with_failed_get(hass, caplog):
+ """Test attributes get extracted from a XML result with bad xml."""
+
+ respx.get(
+ "http://localhost",
+ status_code=200,
+ headers={"content-type": "text/xml"},
+ content="",
+ )
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.toplevel.master_value }}",
+ "json_attributes": ["key"],
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("sensor.foo")
+
+ assert state.state == STATE_UNKNOWN
+ assert "Erroneous XML" in caplog.text
+ assert "Empty reply" in caplog.text
+
+
+@respx.mock
+async def test_reload(hass):
"""Verify we can reload reset sensors."""
- requests_mock.get("http://localhost", text="test data")
+ respx.get("http://localhost", status_code=200)
await async_setup_component(
hass,
@@ -726,8 +695,6 @@ async def test_reload(hass, requests_mock):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 1
-
assert hass.states.get("sensor.mockreset") is None
assert hass.states.get("sensor.rollout")
diff --git a/tests/fixtures/rest/configuration.yaml b/tests/fixtures/rest/configuration.yaml
index 69a4e771ebf..a8a4081fcae 100644
--- a/tests/fixtures/rest/configuration.yaml
+++ b/tests/fixtures/rest/configuration.yaml
@@ -4,6 +4,12 @@ sensor:
method: GET
name: rollout
+binary_sensor:
+ - platform: rest
+ resource: "http://localhost"
+ method: GET
+ name: rollout
+
notify:
- name: rest_reloaded
platform: rest