Skip to content
Permalink
Browse files

style changes after review, additional test case

  • Loading branch information...
ties committed May 7, 2019
1 parent 974f4af commit 9229e369d7948ed8ccd180adbc2023cb9c4a5c24
Showing with 99 additions and 58 deletions.
  1. +28 −17 homeassistant/components/buienradar/camera.py
  2. +71 −41 tests/components/buienradar/test_camera.py
@@ -8,7 +8,7 @@
import voluptuous as vol

from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ID, CONF_NAME
from homeassistant.const import CONF_NAME

from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -29,22 +29,22 @@

PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DIMENSION): DIM_RANGE,
vol.Optional(CONF_DELTA): vol.All(vol.Coerce(float),
vol.Range(min=0)),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DIMENSION, default=512): DIM_RANGE,
vol.Optional(CONF_DELTA, default=600.0): vol.All(vol.Coerce(float),
vol.Range(min=0)),
vol.Optional(CONF_NAME, default="Buienradar loop"): cv.string,
}))


async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up buienradar radar-loop camera component."""
c_id = config.get(CONF_ID)
dimension = config.get(CONF_DIMENSION) or 512
delta = config.get(CONF_DELTA) or 600.0
name = config.get(CONF_NAME) or "Buienradar loop"
dimension = config.get(CONF_DIMENSION)
delta = config.get(CONF_DELTA)
name = config.get(CONF_NAME)

async_add_entities([BuienradarCam(hass, name, c_id, dimension, delta)])
async_add_entities([BuienradarCam(name, dimension, delta,
loop=hass.loop)])


class BuienradarCam(Camera):
@@ -60,7 +60,13 @@ class BuienradarCam(Camera):
""" Deadline for image refresh """
_deadline = None # type: Optional[datetime]
_name = "" # type: str
_delta = 0.0 # type: float
_delta = 600.0 # type: float
"""
Ensures that only one reader can cause an http request at the same time,
and that all readers return after this request completes.
invariant: this condition is private to and owned by this instance.
"""
_condition = None # type: Optional[asyncio.Condition]
""" Loading status """
_loading = False # type: bool
@@ -69,17 +75,22 @@ class BuienradarCam(Camera):
""" last modified HTTP response header"""
_last_modified = None # type: Optional[str]

def __init__(self, hass, name: str, c_id: Optional[str], dimension: int,
delta: float):
"""Initialize the component."""
def __init__(self, name: str, dimension: int, delta: float,
loop: Optional[asyncio.AbstractEventLoop] = None):
"""
Initialize the component.
This constructor must be run in the event loop _or_ be provided with
an applicable event loop as argument.
"""
super().__init__()
self._hass = hass

self._name = name

self._dimension = dimension
self._delta = delta

self._condition = asyncio.Condition(loop=hass.loop)
self._condition = asyncio.Condition(loop=loop)

self._last_image = None
self._last_modified = None
@@ -100,7 +111,7 @@ def __needs_refresh(self) -> bool:

async def __retrieve_radar_image(self) -> bool:
"""Retrieve new radar image and return whether this succeeded."""
session = async_get_clientsession(self._hass)
session = async_get_clientsession(self.hass)

url = RADAR_MAP_URL_TEMPLATE.format(w=self._dimension,
h=self._dimension)
@@ -1,5 +1,6 @@
"""The tests for generic camera component."""
import asyncio
from aiohttp.client_exceptions import ClientResponseError

from homeassistant.util import dt as dt_util

@@ -15,124 +16,118 @@ def radar_map_url(dim: int = 512) -> str:
"image/1.0/RadarMapNL?w={dim}&h={dim}").format(dim=dim)


@asyncio.coroutine
def test_fetching_url_and_caching(aioclient_mock, hass, hass_client):
async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client):
"""Test that it fetches the given url."""
aioclient_mock.get(radar_map_url(), text='hello world')

yield from async_setup_component(hass, 'camera', {
await async_setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'buienradar',
}})

client = yield from hass_client()
client = await hass_client()

resp = yield from client.get('/api/camera_proxy/camera.config_test')
resp = await client.get('/api/camera_proxy/camera.config_test')

assert resp.status == 200
assert aioclient_mock.call_count == 1
body = yield from resp.text()
body = await resp.text()
assert body == 'hello world'

# default delta is 600s -> should be the same when calling immediately
# afterwards.

resp = yield from client.get('/api/camera_proxy/camera.config_test')
resp = await client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 1


@asyncio.coroutine
def test_expire_delta(aioclient_mock, hass, hass_client):
async def test_expire_delta(aioclient_mock, hass, hass_client):
"""Test that the cache expires after delta."""
aioclient_mock.get(radar_map_url(), text='hello world')

yield from async_setup_component(hass, 'camera', {
await async_setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'buienradar',
'delta': EPSILON_DELTA,
}})

client = yield from hass_client()
client = await hass_client()

resp = yield from client.get('/api/camera_proxy/camera.config_test')
resp = await client.get('/api/camera_proxy/camera.config_test')

assert resp.status == 200
assert aioclient_mock.call_count == 1
body = yield from resp.text()
body = await resp.text()
assert body == 'hello world'

yield from asyncio.sleep(EPSILON_DELTA)
await asyncio.sleep(EPSILON_DELTA)
# tiny delta has passed -> should immediately call again
resp = yield from client.get('/api/camera_proxy/camera.config_test')
resp = await client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 2


@asyncio.coroutine
def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client):
async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client):
"""Test that it fetches with only one request at the same time."""
aioclient_mock.get(radar_map_url(), text='hello world')

yield from async_setup_component(hass, 'camera', {
await async_setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'buienradar',
}})

client = yield from hass_client()
client = await hass_client()

resp_1 = client.get('/api/camera_proxy/camera.config_test')
resp_2 = client.get('/api/camera_proxy/camera.config_test')

resp = yield from resp_1
resp_2 = yield from resp_2
resp = await resp_1
resp_2 = await resp_2

assert (yield from resp.text()) == (yield from resp_2.text())
assert (await resp.text()) == (await resp_2.text())

assert aioclient_mock.call_count == 1


@asyncio.coroutine
def test_dimension(aioclient_mock, hass, hass_client):
async def test_dimension(aioclient_mock, hass, hass_client):
"""Test that it actually adheres to the dimension."""
aioclient_mock.get(radar_map_url(700), text='hello world')

yield from async_setup_component(hass, 'camera', {
await async_setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'buienradar',
'dimension': 700,
}})

client = yield from hass_client()
client = await hass_client()

yield from client.get('/api/camera_proxy/camera.config_test')
await client.get('/api/camera_proxy/camera.config_test')

assert aioclient_mock.call_count == 1


@asyncio.coroutine
def test_failure_response_not_cached(aioclient_mock, hass, hass_client):
async def test_failure_response_not_cached(aioclient_mock, hass, hass_client):
"""Test that it does not cache a failure response."""
aioclient_mock.get(radar_map_url(), text='hello world', status=401)

yield from async_setup_component(hass, 'camera', {
await async_setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'buienradar',
}})

client = yield from hass_client()
client = await hass_client()

yield from client.get('/api/camera_proxy/camera.config_test')
yield from client.get('/api/camera_proxy/camera.config_test')
await client.get('/api/camera_proxy/camera.config_test')
await client.get('/api/camera_proxy/camera.config_test')

assert aioclient_mock.call_count == 2


@asyncio.coroutine
def test_last_modified_updates(aioclient_mock, hass, hass_client):
async def test_last_modified_updates(aioclient_mock, hass, hass_client):
"""Test that it does respect HTTP not modified."""
# Build Last-Modified header value
now = dt_util.utcnow()
@@ -143,20 +138,20 @@ def test_last_modified_updates(aioclient_mock, hass, hass_client):
'Last-Modified': last_modified,
})

yield from async_setup_component(hass, 'camera', {
await async_setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'buienradar',
'delta': EPSILON_DELTA,
}})

client = yield from hass_client()
client = await hass_client()

resp_1 = yield from client.get('/api/camera_proxy/camera.config_test')
resp_1 = await client.get('/api/camera_proxy/camera.config_test')
# It is not possible to check if header was sent.
assert aioclient_mock.call_count == 1

yield from asyncio.sleep(EPSILON_DELTA)
await asyncio.sleep(EPSILON_DELTA)

# Content has expired, change response to a 304 NOT MODIFIED, which has no
# text, i.e. old value should be kept
@@ -166,7 +161,42 @@ def test_last_modified_updates(aioclient_mock, hass, hass_client):

aioclient_mock.get(radar_map_url(), text=None, status=304)

resp_2 = yield from client.get('/api/camera_proxy/camera.config_test')
resp_2 = await client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 1

assert (yield from resp_1.read()) == (yield from resp_2.read())
assert (await resp_1.read()) == (await resp_2.read())


async def test_retries_after_error(aioclient_mock, hass, hass_client):
"""Test that it does retry after an error instead of caching."""
await async_setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'buienradar',
}})

client = await hass_client()

aioclient_mock.get(radar_map_url(), text=None, status=500)

# A 404 should not return data and throw:
try:
await client.get('/api/camera_proxy/camera.config_test')
except ClientResponseError:
pass

assert aioclient_mock.call_count == 1

# Change the response to a 200
aioclient_mock.clear_requests()
aioclient_mock.get(radar_map_url(), text="DEADBEEF")

assert aioclient_mock.call_count == 0

# http error should not be cached, immediate retry.
resp_2 = await client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 1

# Binary text can not be added as body to `aioclient_mock.get(text=...)`,
# while `resp.read()` returns bytes, encode the value.
assert (await resp_2.read()) == b"DEADBEEF"

0 comments on commit 9229e36

Please sign in to comment.
You can’t perform that action at this time.