Use DataUpdateCoordinator for canary (#40691)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Chris Talkington 2020-10-01 03:26:26 -05:00 committed by GitHub
parent 7285c7806f
commit d93141c1a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 193 additions and 162 deletions

View File

@ -13,16 +13,16 @@ from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
from .const import ( from .const import (
CONF_FFMPEG_ARGUMENTS, CONF_FFMPEG_ARGUMENTS,
DATA_CANARY, DATA_COORDINATOR,
DATA_UNDO_UPDATE_LISTENER, DATA_UNDO_UPDATE_LISTENER,
DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS,
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
DOMAIN, DOMAIN,
) )
from .coordinator import CanaryDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -89,17 +89,21 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass.config_entries.async_update_entry(entry, options=options) hass.config_entries.async_update_entry(entry, options=options)
try: try:
canary_data = await hass.async_add_executor_job( canary_api = await hass.async_add_executor_job(_get_canary_api_instance, entry)
_get_canary_data_instance, entry
)
except (ConnectTimeout, HTTPError) as error: except (ConnectTimeout, HTTPError) as error:
_LOGGER.error("Unable to connect to Canary service: %s", str(error)) _LOGGER.error("Unable to connect to Canary service: %s", str(error))
raise ConfigEntryNotReady from error raise ConfigEntryNotReady from error
coordinator = CanaryDataUpdateCoordinator(hass, api=canary_api)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
undo_listener = entry.add_update_listener(_async_update_listener) undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
DATA_CANARY: canary_data, DATA_COORDINATOR: coordinator,
DATA_UNDO_UPDATE_LISTENER: undo_listener, DATA_UNDO_UPDATE_LISTENER: undo_listener,
} }
@ -134,77 +138,12 @@ async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) ->
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
class CanaryData: def _get_canary_api_instance(entry: ConfigEntry) -> Api:
"""Manages the data retrieved from Canary API.""" """Initialize a new instance of CanaryApi."""
def __init__(self, api: Api):
"""Init the Canary data object."""
self._api = api
self._locations_by_id = {}
self._readings_by_device_id = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Get the latest data from py-canary with a throttle."""
self._update(**kwargs)
def _update(self, **kwargs):
"""Get the latest data from py-canary."""
for location in self._api.get_locations():
location_id = location.location_id
self._locations_by_id[location_id] = location
for device in location.devices:
if device.is_online:
self._readings_by_device_id[
device.device_id
] = self._api.get_latest_readings(device.device_id)
@property
def locations(self):
"""Return a list of locations."""
return self._locations_by_id.values()
def get_location(self, location_id):
"""Return a location based on location_id."""
return self._locations_by_id.get(location_id, [])
def get_readings(self, device_id):
"""Return a list of readings based on device_id."""
return self._readings_by_device_id.get(device_id, [])
def get_reading(self, device_id, sensor_type):
"""Return reading for device_id and sensor type."""
readings = self._readings_by_device_id.get(device_id, [])
return next(
(
reading.value
for reading in readings
if reading.sensor_type == sensor_type
),
None,
)
def set_location_mode(self, location_id, mode_name, is_private=False):
"""Set location mode."""
self._api.set_location_mode(location_id, mode_name, is_private)
self.update(no_throttle=True)
def get_live_stream_session(self, device):
"""Return live stream session."""
return self._api.get_live_stream_session(device)
def _get_canary_data_instance(entry: ConfigEntry) -> CanaryData:
"""Initialize a new instance of CanaryData."""
canary = Api( canary = Api(
entry.data[CONF_USERNAME], entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD], entry.data[CONF_PASSWORD],
entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
) )
canary_data = CanaryData(canary) return canary
canary_data.update()
return canary_data

View File

@ -19,9 +19,10 @@ from homeassistant.const import (
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CanaryData from .const import DATA_COORDINATOR, DOMAIN
from .const import DATA_CANARY, DOMAIN from .coordinator import CanaryDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,25 +33,35 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None], async_add_entities: Callable[[List[Entity], bool], None],
) -> None: ) -> None:
"""Set up Canary alarm control panels based on a config entry.""" """Set up Canary alarm control panels based on a config entry."""
data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
alarms = [CanaryAlarm(data, location.location_id) for location in data.locations] DATA_COORDINATOR
]
alarms = [
CanaryAlarm(coordinator, location)
for location_id, location in coordinator.data["locations"].items()
]
async_add_entities(alarms, True) async_add_entities(alarms, True)
class CanaryAlarm(AlarmControlPanelEntity): class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Representation of a Canary alarm control panel.""" """Representation of a Canary alarm control panel."""
def __init__(self, data, location_id): def __init__(self, coordinator, location):
"""Initialize a Canary security camera.""" """Initialize a Canary security camera."""
self._data = data super().__init__(coordinator)
self._location_id = location_id self._location_id = location.location_id
self._location_name = location.name
@property
def location(self):
"""Return information about the location."""
return self.coordinator.data["locations"][self._location_id]
@property @property
def name(self): def name(self):
"""Return the name of the alarm.""" """Return the name of the alarm."""
location = self._data.get_location(self._location_id) return self._location_name
return location.name
@property @property
def unique_id(self): def unique_id(self):
@ -60,18 +71,17 @@ class CanaryAlarm(AlarmControlPanelEntity):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
location = self._data.get_location(self._location_id) if self.location.is_private:
if location.is_private:
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
mode = location.mode mode = self.location.mode
if mode.name == LOCATION_MODE_AWAY: if mode.name == LOCATION_MODE_AWAY:
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
if mode.name == LOCATION_MODE_HOME: if mode.name == LOCATION_MODE_HOME:
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
if mode.name == LOCATION_MODE_NIGHT: if mode.name == LOCATION_MODE_NIGHT:
return STATE_ALARM_ARMED_NIGHT return STATE_ALARM_ARMED_NIGHT
return None return None
@property @property
@ -82,26 +92,24 @@ class CanaryAlarm(AlarmControlPanelEntity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
location = self._data.get_location(self._location_id) return {"private": self.location.is_private}
return {"private": location.is_private}
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
location = self._data.get_location(self._location_id) self.coordinator.canary.set_location_mode(
self._data.set_location_mode(self._location_id, location.mode.name, True) self._location_id, self.location.mode.name, True
)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME) self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY) self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None): def alarm_arm_night(self, code=None):
"""Send arm night command.""" """Send arm night command."""
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT) self.coordinator.canary.set_location_mode(
self._location_id, LOCATION_MODE_NIGHT
def update(self): )
"""Get the latest state of the sensor."""
self._data.update()

View File

@ -15,17 +15,18 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import CanaryData
from .const import ( from .const import (
CONF_FFMPEG_ARGUMENTS, CONF_FFMPEG_ARGUMENTS,
DATA_CANARY, DATA_COORDINATOR,
DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS,
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
DOMAIN, DOMAIN,
MANUFACTURER, MANUFACTURER,
) )
from .coordinator import CanaryDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -49,21 +50,22 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None], async_add_entities: Callable[[List[Entity], bool], None],
) -> None: ) -> None:
"""Set up Canary sensors based on a config entry.""" """Set up Canary sensors based on a config entry."""
data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
DATA_COORDINATOR
]
ffmpeg_arguments = entry.options.get( ffmpeg_arguments = entry.options.get(
CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
) )
cameras = [] cameras = []
for location in data.locations: for location_id, location in coordinator.data["locations"].items():
for device in location.devices: for device in location.devices:
if device.is_online: if device.is_online:
cameras.append( cameras.append(
CanaryCamera( CanaryCamera(
hass, hass,
data, coordinator,
location, location_id,
device, device,
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
ffmpeg_arguments, ffmpeg_arguments,
@ -73,17 +75,15 @@ async def async_setup_entry(
async_add_entities(cameras, True) async_add_entities(cameras, True)
class CanaryCamera(Camera): class CanaryCamera(CoordinatorEntity, Camera):
"""An implementation of a Canary security camera.""" """An implementation of a Canary security camera."""
def __init__(self, hass, data, location, device, timeout, ffmpeg_args): def __init__(self, hass, coordinator, location_id, device, timeout, ffmpeg_args):
"""Initialize a Canary security camera.""" """Initialize a Canary security camera."""
super().__init__() super().__init__(coordinator)
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = ffmpeg_args self._ffmpeg_arguments = ffmpeg_args
self._data = data self._location_id = location_id
self._location = location
self._device = device self._device = device
self._device_id = device.device_id self._device_id = device.device_id
self._device_name = device.name self._device_name = device.name
@ -91,6 +91,11 @@ class CanaryCamera(Camera):
self._timeout = timeout self._timeout = timeout
self._live_stream_session = None self._live_stream_session = None
@property
def location(self):
"""Return information about the location."""
return self.coordinator.data["locations"][self._location_id]
@property @property
def name(self): def name(self):
"""Return the name of this device.""" """Return the name of this device."""
@ -114,12 +119,12 @@ class CanaryCamera(Camera):
@property @property
def is_recording(self): def is_recording(self):
"""Return true if the device is recording.""" """Return true if the device is recording."""
return self._location.is_recording return self.location.is_recording
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return not self._location.is_recording return not self.location.is_recording
async def async_camera_image(self): async def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
@ -159,4 +164,6 @@ class CanaryCamera(Camera):
@Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW)
def renew_live_stream_session(self): def renew_live_stream_session(self):
"""Renew live stream session.""" """Renew live stream session."""
self._live_stream_session = self._data.get_live_stream_session(self._device) self._live_stream_session = self.coordinator.canary.get_live_stream_session(
self._device
)

View File

@ -8,7 +8,7 @@ MANUFACTURER = "Canary Connect, Inc"
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
# Data # Data
DATA_CANARY = "canary" DATA_COORDINATOR = "coordinator"
DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" DATA_UNDO_UPDATE_LISTENER = "undo_update_listener"
# Defaults # Defaults

View File

@ -0,0 +1,59 @@
"""Provides the Canary DataUpdateCoordinator."""
from datetime import timedelta
import logging
from async_timeout import timeout
from canary.api import Api
from requests import ConnectTimeout, HTTPError
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class CanaryDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Canary data."""
def __init__(self, hass: HomeAssistantType, *, api: Api):
"""Initialize global Canary data updater."""
self.canary = api
update_interval = timedelta(seconds=30)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=update_interval,
)
def _update_data(self) -> dict:
"""Fetch data from Canary via sync functions."""
locations_by_id = {}
readings_by_device_id = {}
for location in self.canary.get_locations():
location_id = location.location_id
locations_by_id[location_id] = location
for device in location.devices:
if device.is_online:
readings_by_device_id[
device.device_id
] = self.canary.get_latest_readings(device.device_id)
return {
"locations": locations_by_id,
"readings": readings_by_device_id,
}
async def _async_update_data(self) -> dict:
"""Fetch data from Canary."""
try:
async with timeout(15):
return await self.hass.async_add_executor_job(self._update_data)
except (ConnectTimeout, HTTPError) as error:
raise UpdateFailed(f"Invalid response from API: {error}") from error

View File

@ -14,9 +14,10 @@ from homeassistant.const import (
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CanaryData from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER
from .const import DATA_CANARY, DOMAIN, MANUFACTURER from .coordinator import CanaryDataUpdateCoordinator
SENSOR_VALUE_PRECISION = 2 SENSOR_VALUE_PRECISION = 2
ATTR_AIR_QUALITY = "air_quality" ATTR_AIR_QUALITY = "air_quality"
@ -49,37 +50,71 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None], async_add_entities: Callable[[List[Entity], bool], None],
) -> None: ) -> None:
"""Set up Canary sensors based on a config entry.""" """Set up Canary sensors based on a config entry."""
data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
DATA_COORDINATOR
]
sensors = [] sensors = []
for location in data.locations: for location in coordinator.data["locations"].values():
for device in location.devices: for device in location.devices:
if device.is_online: if device.is_online:
device_type = device.device_type device_type = device.device_type
for sensor_type in SENSOR_TYPES: for sensor_type in SENSOR_TYPES:
if device_type.get("name") in sensor_type[4]: if device_type.get("name") in sensor_type[4]:
sensors.append( sensors.append(
CanarySensor(data, sensor_type, location, device) CanarySensor(coordinator, sensor_type, location, device)
) )
async_add_entities(sensors, True) async_add_entities(sensors, True)
class CanarySensor(Entity): class CanarySensor(CoordinatorEntity, Entity):
"""Representation of a Canary sensor.""" """Representation of a Canary sensor."""
def __init__(self, data, sensor_type, location, device): def __init__(self, coordinator, sensor_type, location, device):
"""Initialize the sensor.""" """Initialize the sensor."""
self._data = data super().__init__(coordinator)
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._device_id = device.device_id self._device_id = device.device_id
self._device_name = device.name self._device_name = device.name
self._device_type_name = device.device_type["name"] self._device_type_name = device.device_type["name"]
self._sensor_value = None
sensor_type_name = sensor_type[0].replace("_", " ").title() sensor_type_name = sensor_type[0].replace("_", " ").title()
self._name = f"{location.name} {device.name} {sensor_type_name}" self._name = f"{location.name} {device.name} {sensor_type_name}"
canary_sensor_type = None
if self._sensor_type[0] == "air_quality":
canary_sensor_type = SensorType.AIR_QUALITY
elif self._sensor_type[0] == "temperature":
canary_sensor_type = SensorType.TEMPERATURE
elif self._sensor_type[0] == "humidity":
canary_sensor_type = SensorType.HUMIDITY
elif self._sensor_type[0] == "wifi":
canary_sensor_type = SensorType.WIFI
elif self._sensor_type[0] == "battery":
canary_sensor_type = SensorType.BATTERY
self._canary_type = canary_sensor_type
@property
def reading(self):
"""Return the device sensor reading."""
readings = self.coordinator.data["readings"][self._device_id]
value = next(
(
reading.value
for reading in readings
if reading.sensor_type == self._canary_type
),
None,
)
if value is not None:
return round(float(value), SENSOR_VALUE_PRECISION)
return None
@property @property
def name(self): def name(self):
"""Return the name of the Canary sensor.""" """Return the name of the Canary sensor."""
@ -88,7 +123,7 @@ class CanarySensor(Entity):
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._sensor_value return self.reading
@property @property
def unique_id(self): def unique_id(self):
@ -123,36 +158,17 @@ class CanarySensor(Entity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
if self._sensor_type[0] == "air_quality" and self._sensor_value is not None: reading = self.reading
if self._sensor_type[0] == "air_quality" and reading is not None:
air_quality = None air_quality = None
if self._sensor_value <= 0.4: if reading <= 0.4:
air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL
elif self._sensor_value <= 0.59: elif reading <= 0.59:
air_quality = STATE_AIR_QUALITY_ABNORMAL air_quality = STATE_AIR_QUALITY_ABNORMAL
elif self._sensor_value <= 1.0: elif reading <= 1.0:
air_quality = STATE_AIR_QUALITY_NORMAL air_quality = STATE_AIR_QUALITY_NORMAL
return {ATTR_AIR_QUALITY: air_quality} return {ATTR_AIR_QUALITY: air_quality}
return None return None
def update(self):
"""Get the latest state of the sensor."""
self._data.update()
canary_sensor_type = None
if self._sensor_type[0] == "air_quality":
canary_sensor_type = SensorType.AIR_QUALITY
elif self._sensor_type[0] == "temperature":
canary_sensor_type = SensorType.TEMPERATURE
elif self._sensor_type[0] == "humidity":
canary_sensor_type = SensorType.HUMIDITY
elif self._sensor_type[0] == "wifi":
canary_sensor_type = SensorType.WIFI
elif self._sensor_type[0] == "battery":
canary_sensor_type = SensorType.BATTERY
value = self._data.get_reading(self._device_id, canary_sensor_type)
if value is not None:
self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION)

View File

@ -5,17 +5,12 @@ from pytest import fixture
from tests.async_mock import MagicMock, patch from tests.async_mock import MagicMock, patch
def mock_canary_update(self, **kwargs):
"""Get the latest data from py-canary."""
self._update(**kwargs)
@fixture @fixture
def canary(hass): def canary(hass):
"""Mock the CanaryApi for easier testing.""" """Mock the CanaryApi for easier testing."""
with patch.object(Api, "login", return_value=True), patch( with patch.object(Api, "login", return_value=True), patch(
"homeassistant.components.canary.CanaryData.update", mock_canary_update "homeassistant.components.canary.Api"
), patch("homeassistant.components.canary.Api") as mock_canary: ) as mock_canary:
instance = mock_canary.return_value = Api( instance = mock_canary.return_value = Api(
"test-username", "test-username",
"test-password", "test-password",

View File

@ -137,7 +137,7 @@ async def test_alarm_control_panel_services(hass, canary) -> None:
service_data={"entity_id": entity_id}, service_data={"entity_id": entity_id},
blocking=True, blocking=True,
) )
instance.set_location_mode.assert_called_with(100, LOCATION_MODE_AWAY, False) instance.set_location_mode.assert_called_with(100, LOCATION_MODE_AWAY)
# test arm home # test arm home
await hass.services.async_call( await hass.services.async_call(
@ -146,7 +146,7 @@ async def test_alarm_control_panel_services(hass, canary) -> None:
service_data={"entity_id": entity_id}, service_data={"entity_id": entity_id},
blocking=True, blocking=True,
) )
instance.set_location_mode.assert_called_with(100, LOCATION_MODE_HOME, False) instance.set_location_mode.assert_called_with(100, LOCATION_MODE_HOME)
# test arm night # test arm night
await hass.services.async_call( await hass.services.async_call(
@ -155,7 +155,7 @@ async def test_alarm_control_panel_services(hass, canary) -> None:
service_data={"entity_id": entity_id}, service_data={"entity_id": entity_id},
blocking=True, blocking=True,
) )
instance.set_location_mode.assert_called_with(100, LOCATION_MODE_NIGHT, False) instance.set_location_mode.assert_called_with(100, LOCATION_MODE_NIGHT)
# test disarm # test disarm
await hass.services.async_call( await hass.services.async_call(

View File

@ -1,4 +1,6 @@
"""The tests for the Canary sensor platform.""" """The tests for the Canary sensor platform."""
from datetime import timedelta
from homeassistant.components.canary.const import DOMAIN, MANUFACTURER from homeassistant.components.canary.const import DOMAIN, MANUFACTURER
from homeassistant.components.canary.sensor import ( from homeassistant.components.canary.sensor import (
ATTR_AIR_QUALITY, ATTR_AIR_QUALITY,
@ -16,11 +18,12 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from . import mock_device, mock_location, mock_reading from . import mock_device, mock_location, mock_reading
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import mock_device_registry, mock_registry from tests.common import async_fire_time_changed, mock_device_registry, mock_registry
async def test_sensors_pro(hass, canary) -> None: async def test_sensors_pro(hass, canary) -> None:
@ -124,6 +127,8 @@ async def test_sensors_attributes_pro(hass, canary) -> None:
mock_reading("air_quality", "0.4"), mock_reading("air_quality", "0.4"),
] ]
future = utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, future)
await hass.helpers.entity_component.async_update_entity(entity_id) await hass.helpers.entity_component.async_update_entity(entity_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -137,6 +142,8 @@ async def test_sensors_attributes_pro(hass, canary) -> None:
mock_reading("air_quality", "1.0"), mock_reading("air_quality", "1.0"),
] ]
future += timedelta(seconds=30)
async_fire_time_changed(hass, future)
await hass.helpers.entity_component.async_update_entity(entity_id) await hass.helpers.entity_component.async_update_entity(entity_id)
await hass.async_block_till_done() await hass.async_block_till_done()