Make rest sensor and binary sensor more efficient (#14484)

* create binary sensor even if initial update fails

* fixed broken test assertion

* fixed broken test assertion

* avoid fetching resource twice - manually in the setup_platform and then through add_devices

* raising PlatformNotReady instead of creating the sensor if the initial rest call fails; throttling the update to avoid fetching the same resource again immediately after setting up sensor

* rolled back throttling of the rest update call; can still avoid updating the binary sensor's rest resoure twice; fixed tests

* typo
This commit is contained in:
Malte Franken 2018-09-21 23:24:50 +09:30 committed by Paulus Schoutsen
parent 3e59ffb33a
commit d5813cf167
4 changed files with 129 additions and 103 deletions

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS) HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -66,13 +67,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
rest = RestData(method, resource, auth, headers, payload, verify_ssl) rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update() rest.update()
if rest.data is None: if rest.data is None:
_LOGGER.error("Unable to fetch REST data from %s", resource) raise PlatformNotReady
return False
# No need to update the sensor now because it will determine its state
# based in the rest resource that has just been retrieved.
add_entities([RestBinarySensor( add_entities([RestBinarySensor(
hass, rest, name, device_class, value_template)], True) hass, rest, name, device_class, value_template)])
class RestBinarySensor(BinarySensorDevice): class RestBinarySensor(BinarySensorDevice):

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME,
CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN) HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -76,7 +77,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
auth = None auth = None
rest = RestData(method, resource, auth, headers, payload, verify_ssl) rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update() rest.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([RestSensor( add_entities([RestSensor(
hass, rest, name, unit, value_template, json_attrs, force_update hass, rest, name, unit, value_template, json_attrs, force_update
)], True) )], True)
@ -170,6 +175,7 @@ class RestData:
def update(self): def update(self):
"""Get the latest data from REST service with provided method.""" """Get the latest data from REST service with provided method."""
_LOGGER.debug("Updating from %s", self._request.url)
try: try:
with requests.Session() as sess: with requests.Session() as sess:
response = sess.send( response = sess.send(

View File

@ -1,11 +1,13 @@
"""The tests for the REST binary sensor platform.""" """The tests for the REST binary sensor platform."""
import unittest import unittest
from pytest import raises
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
import requests import requests
from requests.exceptions import Timeout, MissingSchema from requests.exceptions import Timeout, MissingSchema
import requests_mock import requests_mock
from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
import homeassistant.components.binary_sensor as binary_sensor import homeassistant.components.binary_sensor as binary_sensor
import homeassistant.components.binary_sensor.rest as rest import homeassistant.components.binary_sensor.rest as rest
@ -18,9 +20,18 @@ from tests.common import get_test_home_assistant, assert_setup_component
class TestRestBinarySensorSetup(unittest.TestCase): class TestRestBinarySensorSetup(unittest.TestCase):
"""Tests for setting up the REST binary sensor platform.""" """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): def setUp(self):
"""Set up things to be run when tests are started.""" """Set up things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
# Reset for this test.
self.DEVICES = []
def tearDown(self): def tearDown(self):
"""Stop everything that was started.""" """Stop everything that was started."""
@ -45,76 +56,80 @@ class TestRestBinarySensorSetup(unittest.TestCase):
side_effect=requests.exceptions.ConnectionError()) side_effect=requests.exceptions.ConnectionError())
def test_setup_failed_connect(self, mock_req): def test_setup_failed_connect(self, mock_req):
"""Test setup when connection error occurs.""" """Test setup when connection error occurs."""
self.assertFalse(rest.setup_platform(self.hass, { with raises(PlatformNotReady):
'platform': 'rest', rest.setup_platform(self.hass, {
'resource': 'http://localhost', 'platform': 'rest',
}, lambda devices, update=True: None)) 'resource': 'http://localhost',
}, self.add_devices, None)
self.assertEqual(len(self.DEVICES), 0)
@patch('requests.Session.send', side_effect=Timeout()) @patch('requests.Session.send', side_effect=Timeout())
def test_setup_timeout(self, mock_req): def test_setup_timeout(self, mock_req):
"""Test setup when connection timeout occurs.""" """Test setup when connection timeout occurs."""
self.assertFalse(rest.setup_platform(self.hass, { with raises(PlatformNotReady):
'platform': 'rest', rest.setup_platform(self.hass, {
'resource': 'http://localhost', 'platform': 'rest',
}, lambda devices, update=True: None)) 'resource': 'http://localhost',
}, self.add_devices, None)
self.assertEqual(len(self.DEVICES), 0)
@requests_mock.Mocker() @requests_mock.Mocker()
def test_setup_minimum(self, mock_req): def test_setup_minimum(self, mock_req):
"""Test setup with minimum configuration.""" """Test setup with minimum configuration."""
mock_req.get('http://localhost', status_code=200) mock_req.get('http://localhost', status_code=200)
self.assertTrue(setup_component(self.hass, 'binary_sensor', { with assert_setup_component(1, 'binary_sensor'):
'binary_sensor': { self.assertTrue(setup_component(self.hass, 'binary_sensor', {
'platform': 'rest', 'binary_sensor': {
'resource': 'http://localhost' 'platform': 'rest',
} 'resource': 'http://localhost'
})) }
self.assertEqual(2, mock_req.call_count) }))
assert_setup_component(1, 'switch') self.assertEqual(1, mock_req.call_count)
@requests_mock.Mocker() @requests_mock.Mocker()
def test_setup_get(self, mock_req): def test_setup_get(self, mock_req):
"""Test setup with valid configuration.""" """Test setup with valid configuration."""
mock_req.get('http://localhost', status_code=200) mock_req.get('http://localhost', status_code=200)
self.assertTrue(setup_component(self.hass, 'binary_sensor', { with assert_setup_component(1, 'binary_sensor'):
'binary_sensor': { self.assertTrue(setup_component(self.hass, 'binary_sensor', {
'platform': 'rest', 'binary_sensor': {
'resource': 'http://localhost', 'platform': 'rest',
'method': 'GET', 'resource': 'http://localhost',
'value_template': '{{ value_json.key }}', 'method': 'GET',
'name': 'foo', 'value_template': '{{ value_json.key }}',
'unit_of_measurement': 'MB', 'name': 'foo',
'verify_ssl': 'true', 'unit_of_measurement': 'MB',
'authentication': 'basic', 'verify_ssl': 'true',
'username': 'my username', 'authentication': 'basic',
'password': 'my password', 'username': 'my username',
'headers': {'Accept': 'application/json'} 'password': 'my password',
} 'headers': {'Accept': 'application/json'}
})) }
self.assertEqual(2, mock_req.call_count) }))
assert_setup_component(1, 'binary_sensor') self.assertEqual(1, mock_req.call_count)
@requests_mock.Mocker() @requests_mock.Mocker()
def test_setup_post(self, mock_req): def test_setup_post(self, mock_req):
"""Test setup with valid configuration.""" """Test setup with valid configuration."""
mock_req.post('http://localhost', status_code=200) mock_req.post('http://localhost', status_code=200)
self.assertTrue(setup_component(self.hass, 'binary_sensor', { with assert_setup_component(1, 'binary_sensor'):
'binary_sensor': { self.assertTrue(setup_component(self.hass, 'binary_sensor', {
'platform': 'rest', 'binary_sensor': {
'resource': 'http://localhost', 'platform': 'rest',
'method': 'POST', 'resource': 'http://localhost',
'value_template': '{{ value_json.key }}', 'method': 'POST',
'payload': '{ "device": "toaster"}', 'value_template': '{{ value_json.key }}',
'name': 'foo', 'payload': '{ "device": "toaster"}',
'unit_of_measurement': 'MB', 'name': 'foo',
'verify_ssl': 'true', 'unit_of_measurement': 'MB',
'authentication': 'basic', 'verify_ssl': 'true',
'username': 'my username', 'authentication': 'basic',
'password': 'my password', 'username': 'my username',
'headers': {'Accept': 'application/json'} 'password': 'my password',
} 'headers': {'Accept': 'application/json'}
})) }
self.assertEqual(2, mock_req.call_count) }))
assert_setup_component(1, 'binary_sensor') self.assertEqual(1, mock_req.call_count)
class TestRestBinarySensor(unittest.TestCase): class TestRestBinarySensor(unittest.TestCase):

View File

@ -1,11 +1,13 @@
"""The tests for the REST sensor platform.""" """The tests for the REST sensor platform."""
import unittest import unittest
from pytest import raises
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
import requests import requests
from requests.exceptions import Timeout, MissingSchema, RequestException from requests.exceptions import Timeout, MissingSchema, RequestException
import requests_mock import requests_mock
from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
import homeassistant.components.sensor as sensor import homeassistant.components.sensor as sensor
import homeassistant.components.sensor.rest as rest import homeassistant.components.sensor.rest as rest
@ -45,76 +47,78 @@ class TestRestSensorSetup(unittest.TestCase):
side_effect=requests.exceptions.ConnectionError()) side_effect=requests.exceptions.ConnectionError())
def test_setup_failed_connect(self, mock_req): def test_setup_failed_connect(self, mock_req):
"""Test setup when connection error occurs.""" """Test setup when connection error occurs."""
self.assertTrue(rest.setup_platform(self.hass, { with raises(PlatformNotReady):
'platform': 'rest', rest.setup_platform(self.hass, {
'resource': 'http://localhost', 'platform': 'rest',
}, lambda devices, update=True: None) is None) 'resource': 'http://localhost',
}, lambda devices, update=True: None)
@patch('requests.Session.send', side_effect=Timeout()) @patch('requests.Session.send', side_effect=Timeout())
def test_setup_timeout(self, mock_req): def test_setup_timeout(self, mock_req):
"""Test setup when connection timeout occurs.""" """Test setup when connection timeout occurs."""
self.assertTrue(rest.setup_platform(self.hass, { with raises(PlatformNotReady):
'platform': 'rest', rest.setup_platform(self.hass, {
'resource': 'http://localhost', 'platform': 'rest',
}, lambda devices, update=True: None) is None) 'resource': 'http://localhost',
}, lambda devices, update=True: None)
@requests_mock.Mocker() @requests_mock.Mocker()
def test_setup_minimum(self, mock_req): def test_setup_minimum(self, mock_req):
"""Test setup with minimum configuration.""" """Test setup with minimum configuration."""
mock_req.get('http://localhost', status_code=200) mock_req.get('http://localhost', status_code=200)
self.assertTrue(setup_component(self.hass, 'sensor', { with assert_setup_component(1, 'sensor'):
'sensor': { self.assertTrue(setup_component(self.hass, 'sensor', {
'platform': 'rest', 'sensor': {
'resource': 'http://localhost' 'platform': 'rest',
} 'resource': 'http://localhost'
})) }
}))
self.assertEqual(2, mock_req.call_count) self.assertEqual(2, mock_req.call_count)
assert_setup_component(1, 'switch')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_setup_get(self, mock_req): def test_setup_get(self, mock_req):
"""Test setup with valid configuration.""" """Test setup with valid configuration."""
mock_req.get('http://localhost', status_code=200) mock_req.get('http://localhost', status_code=200)
self.assertTrue(setup_component(self.hass, 'sensor', { with assert_setup_component(1, 'sensor'):
'sensor': { self.assertTrue(setup_component(self.hass, 'sensor', {
'platform': 'rest', 'sensor': {
'resource': 'http://localhost', 'platform': 'rest',
'method': 'GET', 'resource': 'http://localhost',
'value_template': '{{ value_json.key }}', 'method': 'GET',
'name': 'foo', 'value_template': '{{ value_json.key }}',
'unit_of_measurement': 'MB', 'name': 'foo',
'verify_ssl': 'true', 'unit_of_measurement': 'MB',
'authentication': 'basic', 'verify_ssl': 'true',
'username': 'my username', 'authentication': 'basic',
'password': 'my password', 'username': 'my username',
'headers': {'Accept': 'application/json'} 'password': 'my password',
} 'headers': {'Accept': 'application/json'}
})) }
}))
self.assertEqual(2, mock_req.call_count) self.assertEqual(2, mock_req.call_count)
assert_setup_component(1, 'sensor')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_setup_post(self, mock_req): def test_setup_post(self, mock_req):
"""Test setup with valid configuration.""" """Test setup with valid configuration."""
mock_req.post('http://localhost', status_code=200) mock_req.post('http://localhost', status_code=200)
self.assertTrue(setup_component(self.hass, 'sensor', { with assert_setup_component(1, 'sensor'):
'sensor': { self.assertTrue(setup_component(self.hass, 'sensor', {
'platform': 'rest', 'sensor': {
'resource': 'http://localhost', 'platform': 'rest',
'method': 'POST', 'resource': 'http://localhost',
'value_template': '{{ value_json.key }}', 'method': 'POST',
'payload': '{ "device": "toaster"}', 'value_template': '{{ value_json.key }}',
'name': 'foo', 'payload': '{ "device": "toaster"}',
'unit_of_measurement': 'MB', 'name': 'foo',
'verify_ssl': 'true', 'unit_of_measurement': 'MB',
'authentication': 'basic', 'verify_ssl': 'true',
'username': 'my username', 'authentication': 'basic',
'password': 'my password', 'username': 'my username',
'headers': {'Accept': 'application/json'} 'password': 'my password',
} 'headers': {'Accept': 'application/json'}
})) }
}))
self.assertEqual(2, mock_req.call_count) self.assertEqual(2, mock_req.call_count)
assert_setup_component(1, 'sensor')
class TestRestSensor(unittest.TestCase): class TestRestSensor(unittest.TestCase):