Add DataUpdateCoordinator to Verisure (#47574)

This commit is contained in:
Franck Nijhof 2021-03-11 19:41:01 +01:00 committed by GitHub
parent 10848b9bdf
commit 1095905f8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 287 additions and 475 deletions

View file

@ -5,7 +5,11 @@ from datetime import timedelta
from typing import Any, Literal from typing import Any, Literal
from jsonpath import jsonpath from jsonpath import jsonpath
import verisure from verisure import (
Error as VerisureError,
ResponseError as VerisureResponseError,
Session as Verisure,
)
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
@ -19,6 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .const import ( from .const import (
@ -52,8 +57,6 @@ PLATFORMS = [
"binary_sensor", "binary_sensor",
] ]
HUB = None
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
@ -83,31 +86,43 @@ CONFIG_SCHEMA = vol.Schema(
DEVICE_SERIAL_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) DEVICE_SERIAL_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string})
def setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Verisure integration.""" """Set up the Verisure integration."""
global HUB # pylint: disable=global-statement verisure = Verisure(config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD])
HUB = VerisureHub(config[DOMAIN]) coordinator = VerisureDataUpdateCoordinator(
HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])( hass, session=verisure, domain_config=config[DOMAIN]
HUB.update_overview
) )
if not HUB.login():
if not await hass.async_add_executor_job(coordinator.login):
LOGGER.error("Login failed")
return False return False
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: HUB.logout())
HUB.update_overview() hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, lambda event: coordinator.logout()
)
await coordinator.async_refresh()
if not coordinator.last_update_success:
LOGGER.error("Update failed")
return False
hass.data[DOMAIN] = coordinator
for platform in PLATFORMS: for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config) hass.async_create_task(
discovery.async_load_platform(hass, platform, DOMAIN, {}, config)
)
async def capture_smartcam(service): async def capture_smartcam(service):
"""Capture a new picture from a smartcam.""" """Capture a new picture from a smartcam."""
device_id = service.data[ATTR_DEVICE_SERIAL] device_id = service.data[ATTR_DEVICE_SERIAL]
try: try:
await hass.async_add_executor_job(HUB.smartcam_capture, device_id) await hass.async_add_executor_job(coordinator.smartcam_capture, device_id)
LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL)
except verisure.Error as ex: except VerisureError as ex:
LOGGER.error("Could not capture image, %s", ex) LOGGER.error("Could not capture image, %s", ex)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA
) )
@ -115,12 +130,12 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Disable autolock on a doorlock.""" """Disable autolock on a doorlock."""
device_id = service.data[ATTR_DEVICE_SERIAL] device_id = service.data[ATTR_DEVICE_SERIAL]
try: try:
await hass.async_add_executor_job(HUB.disable_autolock, device_id) await hass.async_add_executor_job(coordinator.disable_autolock, device_id)
LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL)
except verisure.Error as ex: except VerisureError as ex:
LOGGER.error("Could not disable autolock, %s", ex) LOGGER.error("Could not disable autolock, %s", ex)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA
) )
@ -128,38 +143,39 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Enable autolock on a doorlock.""" """Enable autolock on a doorlock."""
device_id = service.data[ATTR_DEVICE_SERIAL] device_id = service.data[ATTR_DEVICE_SERIAL]
try: try:
await hass.async_add_executor_job(HUB.enable_autolock, device_id) await hass.async_add_executor_job(coordinator.enable_autolock, device_id)
LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL)
except verisure.Error as ex: except VerisureError as ex:
LOGGER.error("Could not enable autolock, %s", ex) LOGGER.error("Could not enable autolock, %s", ex)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA
) )
return True return True
class VerisureHub: class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
"""A Verisure hub wrapper class.""" """A Verisure Data Update Coordinator."""
def __init__(self, domain_config: ConfigType): def __init__(
self, hass: HomeAssistant, domain_config: ConfigType, session: Verisure
) -> None:
"""Initialize the Verisure hub.""" """Initialize the Verisure hub."""
self.overview = {}
self.imageseries = {} self.imageseries = {}
self.config = domain_config self.config = domain_config
self.session = verisure.Session(
domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD]
)
self.giid = domain_config.get(CONF_GIID) self.giid = domain_config.get(CONF_GIID)
self.session = session
super().__init__(
hass, LOGGER, name=DOMAIN, update_interval=domain_config[CONF_SCAN_INTERVAL]
)
def login(self) -> bool: def login(self) -> bool:
"""Login to Verisure.""" """Login to Verisure."""
try: try:
self.session.login() self.session.login()
except verisure.Error as ex: except VerisureError as ex:
LOGGER.error("Could not log in to verisure, %s", ex) LOGGER.error("Could not log in to verisure, %s", ex)
return False return False
if self.giid: if self.giid:
@ -170,7 +186,7 @@ class VerisureHub:
"""Logout from Verisure.""" """Logout from Verisure."""
try: try:
self.session.logout() self.session.logout()
except verisure.Error as ex: except VerisureError as ex:
LOGGER.error("Could not log out from verisure, %s", ex) LOGGER.error("Could not log out from verisure, %s", ex)
return False return False
return True return True
@ -179,22 +195,22 @@ class VerisureHub:
"""Set installation GIID.""" """Set installation GIID."""
try: try:
self.session.set_giid(self.giid) self.session.set_giid(self.giid)
except verisure.Error as ex: except VerisureError as ex:
LOGGER.error("Could not set installation GIID, %s", ex) LOGGER.error("Could not set installation GIID, %s", ex)
return False return False
return True return True
def update_overview(self) -> None: async def _async_update_data(self) -> dict:
"""Update the overview.""" """Fetch data from Verisure."""
try: try:
self.overview = self.session.get_overview() return await self.hass.async_add_executor_job(self.session.get_overview)
except verisure.ResponseError as ex: except VerisureResponseError as ex:
LOGGER.error("Could not read overview, %s", ex) LOGGER.error("Could not read overview, %s", ex)
if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable
LOGGER.info("Trying to log in again") LOGGER.info("Trying to log in again")
self.login() await self.hass.async_add_executor_job(self.login)
else: return {}
raise raise
@Throttle(timedelta(seconds=60)) @Throttle(timedelta(seconds=60))
def update_smartcam_imageseries(self) -> None: def update_smartcam_imageseries(self) -> None:
@ -216,7 +232,7 @@ class VerisureHub:
def get(self, jpath: str, *args) -> list[Any] | Literal[False]: def get(self, jpath: str, *args) -> list[Any] | Literal[False]:
"""Get values from the overview that matches the jsonpath.""" """Get values from the overview that matches the jsonpath."""
res = jsonpath(self.overview, jpath % args) res = jsonpath(self.data, jpath % args)
return res or [] return res or []
def get_first(self, jpath: str, *args) -> Any | None: def get_first(self, jpath: str, *args) -> Any | None:

View file

@ -1,7 +1,7 @@
"""Support for Verisure alarm control panels.""" """Support for Verisure alarm control panels."""
from __future__ import annotations from __future__ import annotations
from time import sleep import asyncio
from typing import Any, Callable from typing import Any, Callable
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
@ -16,12 +16,14 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HUB as hub from . import VerisureDataUpdateCoordinator
from .const import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, LOGGER from .const import CONF_ALARM, CONF_GIID, DOMAIN, LOGGER
def setup_platform( def setup_platform(
@ -31,51 +33,53 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None, discovery_info: dict[str, Any] | None = None,
) -> None: ) -> None:
"""Set up the Verisure platform.""" """Set up the Verisure platform."""
coordinator = hass.data[DOMAIN]
alarms = [] alarms = []
if int(hub.config.get(CONF_ALARM, 1)): if int(coordinator.config.get(CONF_ALARM, 1)):
hub.update_overview() alarms.append(VerisureAlarm(coordinator))
alarms.append(VerisureAlarm())
add_entities(alarms) add_entities(alarms)
def set_arm_state(state: str, code: str | None = None) -> None: class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Send set arm state command."""
transaction_id = hub.session.set_arm_state(code, state)[
"armStateChangeTransactionId"
]
LOGGER.info("verisure set arm state %s", state)
transaction = {}
while "result" not in transaction:
sleep(0.5)
transaction = hub.session.get_arm_state_transaction(transaction_id)
hub.update_overview()
class VerisureAlarm(AlarmControlPanelEntity):
"""Representation of a Verisure alarm status.""" """Representation of a Verisure alarm status."""
def __init__(self): coordinator: VerisureDataUpdateCoordinator
def __init__(self, coordinator: VerisureDataUpdateCoordinator) -> None:
"""Initialize the Verisure alarm panel.""" """Initialize the Verisure alarm panel."""
super().__init__(coordinator)
self._state = None self._state = None
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the device.""" """Return the name of the device."""
giid = hub.config.get(CONF_GIID) giid = self.coordinator.config.get(CONF_GIID)
if giid is not None: if giid is not None:
aliass = {i["giid"]: i["alias"] for i in hub.session.installations} aliass = {
i["giid"]: i["alias"] for i in self.coordinator.session.installations
}
if giid in aliass: if giid in aliass:
return "{} alarm".format(aliass[giid]) return "{} alarm".format(aliass[giid])
LOGGER.error("Verisure installation giid not found: %s", giid) LOGGER.error("Verisure installation giid not found: %s", giid)
return "{} alarm".format(hub.session.installations[0]["alias"]) return "{} alarm".format(self.coordinator.session.installations[0]["alias"])
@property @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
status = self.coordinator.get_first("$.armState.statusType")
if status == "DISARMED":
self._state = STATE_ALARM_DISARMED
elif status == "ARMED_HOME":
self._state = STATE_ALARM_ARMED_HOME
elif status == "ARMED_AWAY":
self._state = STATE_ALARM_ARMED_AWAY
elif status == "PENDING":
self._state = STATE_ALARM_PENDING
else:
LOGGER.error("Unknown alarm state %s", status)
return self._state return self._state
@property @property
@ -91,30 +95,32 @@ class VerisureAlarm(AlarmControlPanelEntity):
@property @property
def changed_by(self) -> str | None: def changed_by(self) -> str | None:
"""Return the last change triggered by.""" """Return the last change triggered by."""
return self._changed_by return self.coordinator.get_first("$.armState.name")
def update(self) -> None: async def _async_set_arm_state(self, state: str, code: str | None = None) -> None:
"""Update alarm status.""" """Send set arm state command."""
hub.update_overview() arm_state = await self.hass.async_add_executor_job(
status = hub.get_first("$.armState.statusType") self.coordinator.session.set_arm_state, code, state
if status == "DISARMED": )
self._state = STATE_ALARM_DISARMED LOGGER.debug("Verisure set arm state %s", state)
elif status == "ARMED_HOME": transaction = {}
self._state = STATE_ALARM_ARMED_HOME while "result" not in transaction:
elif status == "ARMED_AWAY": await asyncio.sleep(0.5)
self._state = STATE_ALARM_ARMED_AWAY transaction = await self.hass.async_add_executor_job(
elif status != "PENDING": self.coordinator.session.get_arm_state_transaction,
LOGGER.error("Unknown alarm state %s", status) arm_state["armStateChangeTransactionId"],
self._changed_by = hub.get_first("$.armState.name") )
def alarm_disarm(self, code: str | None = None) -> None: await self.coordinator.async_refresh()
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
set_arm_state("DISARMED", code) await self._async_set_arm_state("DISARMED", code)
def alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """Send arm home command."""
set_arm_state("ARMED_HOME", code) await self._async_set_arm_state("ARMED_HOME", code)
def alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """Send arm away command."""
set_arm_state("ARMED_AWAY", code) await self._async_set_arm_state("ARMED_AWAY", code)

View file

@ -9,8 +9,9 @@ from homeassistant.components.binary_sensor import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CONF_DOOR_WINDOW, HUB as hub from . import CONF_DOOR_WINDOW, DOMAIN, VerisureDataUpdateCoordinator
def setup_platform( def setup_platform(
@ -20,34 +21,39 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None, discovery_info: dict[str, Any] | None = None,
) -> None: ) -> None:
"""Set up the Verisure binary sensors.""" """Set up the Verisure binary sensors."""
sensors = [] coordinator = hass.data[DOMAIN]
hub.update_overview()
if int(hub.config.get(CONF_DOOR_WINDOW, 1)): sensors = [VerisureEthernetStatus(coordinator)]
if int(coordinator.config.get(CONF_DOOR_WINDOW, 1)):
sensors.extend( sensors.extend(
[ [
VerisureDoorWindowSensor(device_label) VerisureDoorWindowSensor(coordinator, device_label)
for device_label in hub.get( for device_label in coordinator.get(
"$.doorWindow.doorWindowDevice[*].deviceLabel" "$.doorWindow.doorWindowDevice[*].deviceLabel"
) )
] ]
) )
sensors.extend([VerisureEthernetStatus()])
add_entities(sensors) add_entities(sensors)
class VerisureDoorWindowSensor(BinarySensorEntity): class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity):
"""Representation of a Verisure door window sensor.""" """Representation of a Verisure door window sensor."""
def __init__(self, device_label: str): coordinator: VerisureDataUpdateCoordinator
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, device_label: str
) -> None:
"""Initialize the Verisure door window sensor.""" """Initialize the Verisure door window sensor."""
super().__init__(coordinator)
self._device_label = device_label self._device_label = device_label
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return hub.get_first( return self.coordinator.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area", "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
self._device_label, self._device_label,
) )
@ -56,7 +62,7 @@ class VerisureDoorWindowSensor(BinarySensorEntity):
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state", "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
self._device_label, self._device_label,
) )
@ -67,22 +73,19 @@ class VerisureDoorWindowSensor(BinarySensorEntity):
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]", "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
self._device_label, self._device_label,
) )
is not None is not None
) )
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the state of the sensor."""
hub.update_overview()
class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity):
class VerisureEthernetStatus(BinarySensorEntity):
"""Representation of a Verisure VBOX internet status.""" """Representation of a Verisure VBOX internet status."""
coordinator: VerisureDataUpdateCoordinator
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
@ -91,17 +94,12 @@ class VerisureEthernetStatus(BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return hub.get_first("$.ethernetConnectedNow") return self.coordinator.get_first("$.ethernetConnectedNow")
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return hub.get_first("$.ethernetConnectedNow") is not None return self.coordinator.get_first("$.ethernetConnectedNow") is not None
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the state of the sensor."""
hub.update_overview()
@property @property
def device_class(self) -> str: def device_class(self) -> str:

View file

@ -3,15 +3,16 @@ from __future__ import annotations
import errno import errno
import os import os
from typing import Any, Callable, Literal from typing import Any, Callable
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HUB as hub from . import VerisureDataUpdateCoordinator
from .const import CONF_SMARTCAM, LOGGER from .const import CONF_SMARTCAM, DOMAIN, LOGGER
def setup_platform( def setup_platform(
@ -19,31 +20,39 @@ def setup_platform(
config: dict[str, Any], config: dict[str, Any],
add_entities: Callable[[list[Entity], bool], None], add_entities: Callable[[list[Entity], bool], None],
discovery_info: dict[str, Any] | None = None, discovery_info: dict[str, Any] | None = None,
) -> None | Literal[False]: ) -> None:
"""Set up the Verisure Camera.""" """Set up the Verisure Camera."""
if not int(hub.config.get(CONF_SMARTCAM, 1)): coordinator = hass.data[DOMAIN]
return False if not int(coordinator.config.get(CONF_SMARTCAM, 1)):
return
directory_path = hass.config.config_dir directory_path = hass.config.config_dir
if not os.access(directory_path, os.R_OK): if not os.access(directory_path, os.R_OK):
LOGGER.error("file path %s is not readable", directory_path) LOGGER.error("file path %s is not readable", directory_path)
return False return
hub.update_overview() add_entities(
smartcams = [ [
VerisureSmartcam(hass, device_label, directory_path) VerisureSmartcam(hass, coordinator, device_label, directory_path)
for device_label in hub.get("$.customerImageCameras[*].deviceLabel") for device_label in coordinator.get("$.customerImageCameras[*].deviceLabel")
] ]
)
add_entities(smartcams)
class VerisureSmartcam(Camera): class VerisureSmartcam(CoordinatorEntity, Camera):
"""Representation of a Verisure camera.""" """Representation of a Verisure camera."""
def __init__(self, hass: HomeAssistant, device_label: str, directory_path: str): coordinator = VerisureDataUpdateCoordinator
def __init__(
self,
hass: HomeAssistant,
coordinator: VerisureDataUpdateCoordinator,
device_label: str,
directory_path: str,
):
"""Initialize Verisure File Camera component.""" """Initialize Verisure File Camera component."""
super().__init__() super().__init__(coordinator)
self._device_label = device_label self._device_label = device_label
self._directory_path = directory_path self._directory_path = directory_path
@ -63,8 +72,8 @@ class VerisureSmartcam(Camera):
def check_imagelist(self) -> None: def check_imagelist(self) -> None:
"""Check the contents of the image list.""" """Check the contents of the image list."""
hub.update_smartcam_imageseries() self.coordinator.update_smartcam_imageseries()
image_ids = hub.get_image_info( image_ids = self.coordinator.get_image_info(
"$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId", self._device_label "$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId", self._device_label
) )
if not image_ids: if not image_ids:
@ -77,7 +86,9 @@ class VerisureSmartcam(Camera):
new_image_path = os.path.join( new_image_path = os.path.join(
self._directory_path, "{}{}".format(new_image_id, ".jpg") self._directory_path, "{}{}".format(new_image_id, ".jpg")
) )
hub.session.download_image(self._device_label, new_image_id, new_image_path) self.coordinator.session.download_image(
self._device_label, new_image_id, new_image_path
)
LOGGER.debug("Old image_id=%s", self._image_id) LOGGER.debug("Old image_id=%s", self._image_id)
self.delete_image() self.delete_image()
@ -99,6 +110,6 @@ class VerisureSmartcam(Camera):
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of this camera.""" """Return the name of this camera."""
return hub.get_first( return self.coordinator.get_first(
"$.customerImageCameras[?(@.deviceLabel=='%s')].area", self._device_label "$.customerImageCameras[?(@.deviceLabel=='%s')].area", self._device_label
) )

View file

@ -1,16 +1,17 @@
"""Support for Verisure locks.""" """Support for Verisure locks."""
from __future__ import annotations from __future__ import annotations
from time import monotonic, sleep import asyncio
from typing import Any, Callable from typing import Any, Callable
from homeassistant.components.lock import LockEntity from homeassistant.components.lock import LockEntity
from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HUB as hub from . import VerisureDataUpdateCoordinator
from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, LOGGER from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, DOMAIN, LOGGER
def setup_platform( def setup_platform(
@ -20,48 +21,48 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None, discovery_info: dict[str, Any] | None = None,
) -> None: ) -> None:
"""Set up the Verisure lock platform.""" """Set up the Verisure lock platform."""
coordinator = hass.data[DOMAIN]
locks = [] locks = []
if int(hub.config.get(CONF_LOCKS, 1)): if int(coordinator.config.get(CONF_LOCKS, 1)):
hub.update_overview()
locks.extend( locks.extend(
[ [
VerisureDoorlock(device_label) VerisureDoorlock(coordinator, device_label)
for device_label in hub.get("$.doorLockStatusList[*].deviceLabel") for device_label in coordinator.get(
"$.doorLockStatusList[*].deviceLabel"
)
] ]
) )
add_entities(locks) add_entities(locks)
class VerisureDoorlock(LockEntity): class VerisureDoorlock(CoordinatorEntity, LockEntity):
"""Representation of a Verisure doorlock.""" """Representation of a Verisure doorlock."""
def __init__(self, device_label: str): coordinator: VerisureDataUpdateCoordinator
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, device_label: str
) -> None:
"""Initialize the Verisure lock.""" """Initialize the Verisure lock."""
super().__init__(coordinator)
self._device_label = device_label self._device_label = device_label
self._state = None self._state = None
self._digits = hub.config.get(CONF_CODE_DIGITS) self._digits = coordinator.config.get(CONF_CODE_DIGITS)
self._changed_by = None self._default_lock_code = coordinator.config.get(CONF_DEFAULT_LOCK_CODE)
self._change_timestamp = 0
self._default_lock_code = hub.config.get(CONF_DEFAULT_LOCK_CODE)
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the lock.""" """Return the name of the lock."""
return hub.get_first( return self.coordinator.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].area", self._device_label "$.doorLockStatusList[?(@.deviceLabel=='%s')].area", self._device_label
) )
@property
def state(self) -> str | None:
"""Return the state of the lock."""
return self._state
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')]", self._device_label "$.doorLockStatusList[?(@.deviceLabel=='%s')]", self._device_label
) )
is not None is not None
@ -70,78 +71,65 @@ class VerisureDoorlock(LockEntity):
@property @property
def changed_by(self) -> str | None: def changed_by(self) -> str | None:
"""Last change triggered by.""" """Last change triggered by."""
return self._changed_by return self.coordinator.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
self._device_label,
)
@property @property
def code_format(self) -> str: def code_format(self) -> str:
"""Return the required six digit code.""" """Return the required six digit code."""
return "^\\d{%s}$" % self._digits return "^\\d{%s}$" % self._digits
def update(self) -> None:
"""Update lock status."""
if monotonic() - self._change_timestamp < 10:
return
hub.update_overview()
status = hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
self._device_label,
)
if status == "UNLOCKED":
self._state = STATE_UNLOCKED
elif status == "LOCKED":
self._state = STATE_LOCKED
elif status != "PENDING":
LOGGER.error("Unknown lock state %s", status)
self._changed_by = hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
self._device_label,
)
@property @property
def is_locked(self) -> bool: def is_locked(self) -> bool:
"""Return true if lock is locked.""" """Return true if lock is locked."""
return self._state == STATE_LOCKED status = self.coordinator.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
self._device_label,
)
return status == "LOCKED"
def unlock(self, **kwargs) -> None: async def async_unlock(self, **kwargs) -> None:
"""Send unlock command.""" """Send unlock command."""
if self._state is None:
return
code = kwargs.get(ATTR_CODE, self._default_lock_code) code = kwargs.get(ATTR_CODE, self._default_lock_code)
if code is None: if code is None:
LOGGER.error("Code required but none provided") LOGGER.error("Code required but none provided")
return return
self.set_lock_state(code, STATE_UNLOCKED) await self.async_set_lock_state(code, STATE_UNLOCKED)
def lock(self, **kwargs) -> None: async def async_lock(self, **kwargs) -> None:
"""Send lock command.""" """Send lock command."""
if self._state == STATE_LOCKED:
return
code = kwargs.get(ATTR_CODE, self._default_lock_code) code = kwargs.get(ATTR_CODE, self._default_lock_code)
if code is None: if code is None:
LOGGER.error("Code required but none provided") LOGGER.error("Code required but none provided")
return return
self.set_lock_state(code, STATE_LOCKED) await self.async_set_lock_state(code, STATE_LOCKED)
def set_lock_state(self, code: str, state: str) -> None: async def async_set_lock_state(self, code: str, state: str) -> None:
"""Send set lock state command.""" """Send set lock state command."""
lock_state = "lock" if state == STATE_LOCKED else "unlock" target_state = "lock" if state == STATE_LOCKED else "unlock"
transaction_id = hub.session.set_lock_state( lock_state = await self.hass.async_add_executor_job(
code, self._device_label, lock_state self.coordinator.session.set_lock_state,
)["doorLockStateChangeTransactionId"] code,
self._device_label,
target_state,
)
LOGGER.debug("Verisure doorlock %s", state) LOGGER.debug("Verisure doorlock %s", state)
transaction = {} transaction = {}
attempts = 0 attempts = 0
while "result" not in transaction: while "result" not in transaction:
transaction = hub.session.get_lock_state_transaction(transaction_id) transaction = await self.hass.async_add_executor_job(
self.coordinator.session.get_lock_state_transaction,
lock_state["doorLockStateChangeTransactionId"],
)
attempts += 1 attempts += 1
if attempts == 30: if attempts == 30:
break break
if attempts > 1: if attempts > 1:
sleep(0.5) await asyncio.sleep(0.5)
if transaction["result"] == "OK": if transaction["result"] == "OK":
self._state = state self._state = state
self._change_timestamp = monotonic()

View file

@ -6,9 +6,10 @@ from typing import Any, Callable
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HUB as hub from . import VerisureDataUpdateCoordinator
from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS, DOMAIN
def setup_platform( def setup_platform(
@ -18,34 +19,34 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None, discovery_info: dict[str, Any] | None = None,
) -> None: ) -> None:
"""Set up the Verisure platform.""" """Set up the Verisure platform."""
sensors = [] coordinator = hass.data[DOMAIN]
hub.update_overview()
if int(hub.config.get(CONF_THERMOMETERS, 1)): sensors = []
if int(coordinator.config.get(CONF_THERMOMETERS, 1)):
sensors.extend( sensors.extend(
[ [
VerisureThermometer(device_label) VerisureThermometer(coordinator, device_label)
for device_label in hub.get( for device_label in coordinator.get(
"$.climateValues[?(@.temperature)].deviceLabel" "$.climateValues[?(@.temperature)].deviceLabel"
) )
] ]
) )
if int(hub.config.get(CONF_HYDROMETERS, 1)): if int(coordinator.config.get(CONF_HYDROMETERS, 1)):
sensors.extend( sensors.extend(
[ [
VerisureHygrometer(device_label) VerisureHygrometer(coordinator, device_label)
for device_label in hub.get( for device_label in coordinator.get(
"$.climateValues[?(@.humidity)].deviceLabel" "$.climateValues[?(@.humidity)].deviceLabel"
) )
] ]
) )
if int(hub.config.get(CONF_MOUSE, 1)): if int(coordinator.config.get(CONF_MOUSE, 1)):
sensors.extend( sensors.extend(
[ [
VerisureMouseDetection(device_label) VerisureMouseDetection(coordinator, device_label)
for device_label in hub.get( for device_label in coordinator.get(
"$.eventCounts[?(@.deviceType=='MOUSE1')].deviceLabel" "$.eventCounts[?(@.deviceType=='MOUSE1')].deviceLabel"
) )
] ]
@ -54,18 +55,23 @@ def setup_platform(
add_entities(sensors) add_entities(sensors)
class VerisureThermometer(Entity): class VerisureThermometer(CoordinatorEntity, Entity):
"""Representation of a Verisure thermometer.""" """Representation of a Verisure thermometer."""
def __init__(self, device_label: str): coordinator: VerisureDataUpdateCoordinator
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, device_label: str
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator)
self._device_label = device_label self._device_label = device_label
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the device.""" """Return the name of the device."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label "$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label
) )
+ " temperature" + " temperature"
@ -74,7 +80,7 @@ class VerisureThermometer(Entity):
@property @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
return hub.get_first( return self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].temperature", self._device_label "$.climateValues[?(@.deviceLabel=='%s')].temperature", self._device_label
) )
@ -82,7 +88,7 @@ class VerisureThermometer(Entity):
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].temperature", "$.climateValues[?(@.deviceLabel=='%s')].temperature",
self._device_label, self._device_label,
) )
@ -94,24 +100,24 @@ class VerisureThermometer(Entity):
"""Return the unit of measurement of this entity.""" """Return the unit of measurement of this entity."""
return TEMP_CELSIUS return TEMP_CELSIUS
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the sensor."""
hub.update_overview()
class VerisureHygrometer(CoordinatorEntity, Entity):
class VerisureHygrometer(Entity):
"""Representation of a Verisure hygrometer.""" """Representation of a Verisure hygrometer."""
def __init__(self, device_label: str): coordinator: VerisureDataUpdateCoordinator
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, device_label: str
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator)
self._device_label = device_label self._device_label = device_label
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the device.""" """Return the name of the device."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label "$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label
) )
+ " humidity" + " humidity"
@ -120,7 +126,7 @@ class VerisureHygrometer(Entity):
@property @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
return hub.get_first( return self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label "$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label
) )
@ -128,7 +134,7 @@ class VerisureHygrometer(Entity):
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label "$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label
) )
is not None is not None
@ -139,24 +145,24 @@ class VerisureHygrometer(Entity):
"""Return the unit of measurement of this entity.""" """Return the unit of measurement of this entity."""
return PERCENTAGE return PERCENTAGE
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the sensor."""
hub.update_overview()
class VerisureMouseDetection(CoordinatorEntity, Entity):
class VerisureMouseDetection(Entity):
"""Representation of a Verisure mouse detector.""" """Representation of a Verisure mouse detector."""
def __init__(self, device_label): coordinator: VerisureDataUpdateCoordinator
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, device_label: str
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator)
self._device_label = device_label self._device_label = device_label
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the device.""" """Return the name of the device."""
return ( return (
hub.get_first( self.coordinator.get_first(
"$.eventCounts[?(@.deviceLabel=='%s')].area", self._device_label "$.eventCounts[?(@.deviceLabel=='%s')].area", self._device_label
) )
+ " mouse" + " mouse"
@ -165,7 +171,7 @@ class VerisureMouseDetection(Entity):
@property @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
return hub.get_first( return self.coordinator.get_first(
"$.eventCounts[?(@.deviceLabel=='%s')].detections", self._device_label "$.eventCounts[?(@.deviceLabel=='%s')].detections", self._device_label
) )
@ -173,7 +179,9 @@ class VerisureMouseDetection(Entity):
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
hub.get_first("$.eventCounts[?(@.deviceLabel=='%s')]", self._device_label) self.coordinator.get_first(
"$.eventCounts[?(@.deviceLabel=='%s')]", self._device_label
)
is not None is not None
) )
@ -181,8 +189,3 @@ class VerisureMouseDetection(Entity):
def unit_of_measurement(self) -> str: def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity.""" """Return the unit of measurement of this entity."""
return "Mice" return "Mice"
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the sensor."""
hub.update_overview()

View file

@ -2,13 +2,15 @@
from __future__ import annotations from __future__ import annotations
from time import monotonic from time import monotonic
from typing import Any, Callable, Literal from typing import Any, Callable
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CONF_SMARTPLUGS, HUB as hub from . import VerisureDataUpdateCoordinator
from .const import CONF_SMARTPLUGS, DOMAIN
def setup_platform( def setup_platform(
@ -16,25 +18,31 @@ def setup_platform(
config: dict[str, Any], config: dict[str, Any],
add_entities: Callable[[list[Entity], bool], None], add_entities: Callable[[list[Entity], bool], None],
discovery_info: dict[str, Any] | None = None, discovery_info: dict[str, Any] | None = None,
) -> None | Literal[False]: ) -> None:
"""Set up the Verisure switch platform.""" """Set up the Verisure switch platform."""
if not int(hub.config.get(CONF_SMARTPLUGS, 1)): coordinator = hass.data[DOMAIN]
return False
hub.update_overview() if not int(coordinator.config.get(CONF_SMARTPLUGS, 1)):
switches = [ return
VerisureSmartplug(device_label)
for device_label in hub.get("$.smartPlugs[*].deviceLabel")
]
add_entities(switches) add_entities(
[
VerisureSmartplug(coordinator, device_label)
for device_label in coordinator.get("$.smartPlugs[*].deviceLabel")
]
)
class VerisureSmartplug(SwitchEntity): class VerisureSmartplug(CoordinatorEntity, SwitchEntity):
"""Representation of a Verisure smartplug.""" """Representation of a Verisure smartplug."""
def __init__(self, device_id: str): coordinator: VerisureDataUpdateCoordinator
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, device_id: str
) -> None:
"""Initialize the Verisure device.""" """Initialize the Verisure device."""
super().__init__(coordinator)
self._device_label = device_id self._device_label = device_id
self._change_timestamp = 0 self._change_timestamp = 0
self._state = False self._state = False
@ -42,7 +50,7 @@ class VerisureSmartplug(SwitchEntity):
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name or location of the smartplug.""" """Return the name or location of the smartplug."""
return hub.get_first( return self.coordinator.get_first(
"$.smartPlugs[?(@.deviceLabel == '%s')].area", self._device_label "$.smartPlugs[?(@.deviceLabel == '%s')].area", self._device_label
) )
@ -52,7 +60,7 @@ class VerisureSmartplug(SwitchEntity):
if monotonic() - self._change_timestamp < 10: if monotonic() - self._change_timestamp < 10:
return self._state return self._state
self._state = ( self._state = (
hub.get_first( self.coordinator.get_first(
"$.smartPlugs[?(@.deviceLabel == '%s')].currentState", "$.smartPlugs[?(@.deviceLabel == '%s')].currentState",
self._device_label, self._device_label,
) )
@ -64,23 +72,20 @@ class VerisureSmartplug(SwitchEntity):
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
hub.get_first("$.smartPlugs[?(@.deviceLabel == '%s')]", self._device_label) self.coordinator.get_first(
"$.smartPlugs[?(@.deviceLabel == '%s')]", self._device_label
)
is not None is not None
) )
def turn_on(self, **kwargs) -> None: def turn_on(self, **kwargs) -> None:
"""Set smartplug status on.""" """Set smartplug status on."""
hub.session.set_smartplug_state(self._device_label, True) self.coordinator.session.set_smartplug_state(self._device_label, True)
self._state = True self._state = True
self._change_timestamp = monotonic() self._change_timestamp = monotonic()
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Set smartplug status off.""" """Set smartplug status off."""
hub.session.set_smartplug_state(self._device_label, False) self.coordinator.session.set_smartplug_state(self._device_label, False)
self._state = False self._state = False
self._change_timestamp = monotonic() self._change_timestamp = monotonic()
# pylint: disable=no-self-use
def update(self) -> None:
"""Get the latest date of the smartplug."""
hub.update_overview()

View file

@ -1166,9 +1166,6 @@ uvcclient==0.11.0
# homeassistant.components.vilfo # homeassistant.components.vilfo
vilfo-api-client==0.3.2 vilfo-api-client==0.3.2
# homeassistant.components.verisure
vsure==1.7.2
# homeassistant.components.vultr # homeassistant.components.vultr
vultr==0.1.2 vultr==0.1.2

View file

@ -1 +0,0 @@
"""Tests for Verisure integration."""

View file

@ -1,67 +0,0 @@
"""Test Verisure ethernet status."""
from contextlib import contextmanager
from unittest.mock import patch
from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.setup import async_setup_component
CONFIG = {
"verisure": {
"username": "test",
"password": "test",
"alarm": False,
"door_window": False,
"hygrometers": False,
"mouse": False,
"smartplugs": False,
"thermometers": False,
"smartcam": False,
}
}
@contextmanager
def mock_hub(config, response):
"""Extensively mock out a verisure hub."""
hub_prefix = "homeassistant.components.verisure.binary_sensor.hub"
verisure_prefix = "verisure.Session"
with patch(verisure_prefix) as session, patch(hub_prefix) as hub:
session.login.return_value = True
hub.config = config["verisure"]
hub.get.return_value = response
hub.get_first.return_value = response.get("ethernetConnectedNow", None)
yield hub
async def setup_verisure(hass, config, response):
"""Set up mock verisure."""
with mock_hub(config, response):
await async_setup_component(hass, VERISURE_DOMAIN, config)
await hass.async_block_till_done()
async def test_verisure_no_ethernet_status(hass):
"""Test no data from API."""
await setup_verisure(hass, CONFIG, {})
assert len(hass.states.async_all()) == 1
entity_id = hass.states.async_entity_ids()[0]
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
async def test_verisure_ethernet_status_disconnected(hass):
"""Test disconnected."""
await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": False})
assert len(hass.states.async_all()) == 1
entity_id = hass.states.async_entity_ids()[0]
assert hass.states.get(entity_id).state == "off"
async def test_verisure_ethernet_status_connected(hass):
"""Test connected."""
await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": True})
assert len(hass.states.async_all()) == 1
entity_id = hass.states.async_entity_ids()[0]
assert hass.states.get(entity_id).state == "on"

View file

@ -1,144 +0,0 @@
"""Tests for the Verisure platform."""
from contextlib import contextmanager
from unittest.mock import call, patch
from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN,
SERVICE_LOCK,
SERVICE_UNLOCK,
)
from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN
from homeassistant.const import STATE_UNLOCKED
from homeassistant.setup import async_setup_component
NO_DEFAULT_LOCK_CODE_CONFIG = {
"verisure": {
"username": "test",
"password": "test",
"locks": True,
"alarm": False,
"door_window": False,
"hygrometers": False,
"mouse": False,
"smartplugs": False,
"thermometers": False,
"smartcam": False,
}
}
DEFAULT_LOCK_CODE_CONFIG = {
"verisure": {
"username": "test",
"password": "test",
"locks": True,
"default_lock_code": "9999",
"alarm": False,
"door_window": False,
"hygrometers": False,
"mouse": False,
"smartplugs": False,
"thermometers": False,
"smartcam": False,
}
}
LOCKS = ["door_lock"]
@contextmanager
def mock_hub(config, get_response=LOCKS[0]):
"""Extensively mock out a verisure hub."""
hub_prefix = "homeassistant.components.verisure.lock.hub"
# Since there is no conf to disable ethernet status, mock hub for
# binary sensor too
hub_binary_sensor = "homeassistant.components.verisure.binary_sensor.hub"
verisure_prefix = "verisure.Session"
with patch(verisure_prefix) as session, patch(hub_prefix) as hub:
session.login.return_value = True
hub.config = config["verisure"]
hub.get.return_value = LOCKS
hub.get_first.return_value = get_response.upper()
hub.session.set_lock_state.return_value = {
"doorLockStateChangeTransactionId": "test"
}
hub.session.get_lock_state_transaction.return_value = {"result": "OK"}
with patch(hub_binary_sensor, hub):
yield hub
async def setup_verisure_locks(hass, config):
"""Set up mock verisure locks."""
with mock_hub(config):
await async_setup_component(hass, VERISURE_DOMAIN, config)
await hass.async_block_till_done()
# lock.door_lock, ethernet_status
assert len(hass.states.async_all()) == 2
async def test_verisure_no_default_code(hass):
"""Test configs without a default lock code."""
await setup_verisure_locks(hass, NO_DEFAULT_LOCK_CODE_CONFIG)
with mock_hub(NO_DEFAULT_LOCK_CODE_CONFIG, STATE_UNLOCKED) as hub:
mock = hub.session.set_lock_state
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock"}
)
await hass.async_block_till_done()
assert mock.call_count == 0
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock", "code": "12345"}
)
await hass.async_block_till_done()
assert mock.call_args == call("12345", LOCKS[0], "lock")
mock.reset_mock()
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_UNLOCK, {"entity_id": "lock.door_lock"}
)
await hass.async_block_till_done()
assert mock.call_count == 0
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_UNLOCK,
{"entity_id": "lock.door_lock", "code": "12345"},
)
await hass.async_block_till_done()
assert mock.call_args == call("12345", LOCKS[0], "unlock")
async def test_verisure_default_code(hass):
"""Test configs with a default lock code."""
await setup_verisure_locks(hass, DEFAULT_LOCK_CODE_CONFIG)
with mock_hub(DEFAULT_LOCK_CODE_CONFIG, STATE_UNLOCKED) as hub:
mock = hub.session.set_lock_state
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock"}
)
await hass.async_block_till_done()
assert mock.call_args == call("9999", LOCKS[0], "lock")
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_UNLOCK, {"entity_id": "lock.door_lock"}
)
await hass.async_block_till_done()
assert mock.call_args == call("9999", LOCKS[0], "unlock")
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock", "code": "12345"}
)
await hass.async_block_till_done()
assert mock.call_args == call("12345", LOCKS[0], "lock")
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_UNLOCK,
{"entity_id": "lock.door_lock", "code": "12345"},
)
await hass.async_block_till_done()
assert mock.call_args == call("12345", LOCKS[0], "unlock")