1
mirror of https://github.com/home-assistant/core synced 2024-09-06 10:29:55 +02:00

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 jsonpath import jsonpath
import verisure
from verisure import (
Error as VerisureError,
ResponseError as VerisureResponseError,
Session as Verisure,
)
import voluptuous as vol
from homeassistant.const import (
@ -19,6 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import Throttle
from .const import (
@ -52,8 +57,6 @@ PLATFORMS = [
"binary_sensor",
]
HUB = None
CONFIG_SCHEMA = 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})
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Verisure integration."""
global HUB # pylint: disable=global-statement
HUB = VerisureHub(config[DOMAIN])
HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])(
HUB.update_overview
verisure = Verisure(config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD])
coordinator = VerisureDataUpdateCoordinator(
hass, session=verisure, domain_config=config[DOMAIN]
)
if not HUB.login():
if not await hass.async_add_executor_job(coordinator.login):
LOGGER.error("Login failed")
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:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
hass.async_create_task(
discovery.async_load_platform(hass, platform, DOMAIN, {}, config)
)
async def capture_smartcam(service):
"""Capture a new picture from a smartcam."""
device_id = service.data[ATTR_DEVICE_SERIAL]
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)
except verisure.Error as ex:
except VerisureError as 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
)
@ -115,12 +130,12 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Disable autolock on a doorlock."""
device_id = service.data[ATTR_DEVICE_SERIAL]
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)
except verisure.Error as ex:
except VerisureError as 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
)
@ -128,38 +143,39 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Enable autolock on a doorlock."""
device_id = service.data[ATTR_DEVICE_SERIAL]
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)
except verisure.Error as ex:
except VerisureError as 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
)
return True
class VerisureHub:
"""A Verisure hub wrapper class."""
class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
"""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."""
self.overview = {}
self.imageseries = {}
self.config = domain_config
self.session = verisure.Session(
domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD]
)
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:
"""Login to Verisure."""
try:
self.session.login()
except verisure.Error as ex:
except VerisureError as ex:
LOGGER.error("Could not log in to verisure, %s", ex)
return False
if self.giid:
@ -170,7 +186,7 @@ class VerisureHub:
"""Logout from Verisure."""
try:
self.session.logout()
except verisure.Error as ex:
except VerisureError as ex:
LOGGER.error("Could not log out from verisure, %s", ex)
return False
return True
@ -179,22 +195,22 @@ class VerisureHub:
"""Set installation GIID."""
try:
self.session.set_giid(self.giid)
except verisure.Error as ex:
except VerisureError as ex:
LOGGER.error("Could not set installation GIID, %s", ex)
return False
return True
def update_overview(self) -> None:
"""Update the overview."""
async def _async_update_data(self) -> dict:
"""Fetch data from Verisure."""
try:
self.overview = self.session.get_overview()
except verisure.ResponseError as ex:
return await self.hass.async_add_executor_job(self.session.get_overview)
except VerisureResponseError as ex:
LOGGER.error("Could not read overview, %s", ex)
if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable
LOGGER.info("Trying to log in again")
self.login()
else:
raise
await self.hass.async_add_executor_job(self.login)
return {}
raise
@Throttle(timedelta(seconds=60))
def update_smartcam_imageseries(self) -> None:
@ -216,7 +232,7 @@ class VerisureHub:
def get(self, jpath: str, *args) -> list[Any] | Literal[False]:
"""Get values from the overview that matches the jsonpath."""
res = jsonpath(self.overview, jpath % args)
res = jsonpath(self.data, jpath % args)
return res or []
def get_first(self, jpath: str, *args) -> Any | None:

View File

@ -1,7 +1,7 @@
"""Support for Verisure alarm control panels."""
from __future__ import annotations
from time import sleep
import asyncio
from typing import Any, Callable
from homeassistant.components.alarm_control_panel import (
@ -16,12 +16,14 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HUB as hub
from .const import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, LOGGER
from . import VerisureDataUpdateCoordinator
from .const import CONF_ALARM, CONF_GIID, DOMAIN, LOGGER
def setup_platform(
@ -31,51 +33,53 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None,
) -> None:
"""Set up the Verisure platform."""
coordinator = hass.data[DOMAIN]
alarms = []
if int(hub.config.get(CONF_ALARM, 1)):
hub.update_overview()
alarms.append(VerisureAlarm())
if int(coordinator.config.get(CONF_ALARM, 1)):
alarms.append(VerisureAlarm(coordinator))
add_entities(alarms)
def set_arm_state(state: str, code: str | None = None) -> None:
"""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):
class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Representation of a Verisure alarm status."""
def __init__(self):
coordinator: VerisureDataUpdateCoordinator
def __init__(self, coordinator: VerisureDataUpdateCoordinator) -> None:
"""Initialize the Verisure alarm panel."""
super().__init__(coordinator)
self._state = None
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None
@property
def name(self) -> str:
"""Return the name of the device."""
giid = hub.config.get(CONF_GIID)
giid = self.coordinator.config.get(CONF_GIID)
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:
return "{} alarm".format(aliass[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
def state(self) -> str | None:
"""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
@property
@ -91,30 +95,32 @@ class VerisureAlarm(AlarmControlPanelEntity):
@property
def changed_by(self) -> str | None:
"""Return the last change triggered by."""
return self._changed_by
return self.coordinator.get_first("$.armState.name")
def update(self) -> None:
"""Update alarm status."""
hub.update_overview()
status = hub.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":
LOGGER.error("Unknown alarm state %s", status)
self._changed_by = hub.get_first("$.armState.name")
async def _async_set_arm_state(self, state: str, code: str | None = None) -> None:
"""Send set arm state command."""
arm_state = await self.hass.async_add_executor_job(
self.coordinator.session.set_arm_state, code, state
)
LOGGER.debug("Verisure set arm state %s", state)
transaction = {}
while "result" not in transaction:
await asyncio.sleep(0.5)
transaction = await self.hass.async_add_executor_job(
self.coordinator.session.get_arm_state_transaction,
arm_state["armStateChangeTransactionId"],
)
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."""
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."""
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."""
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.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(
@ -20,34 +21,39 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None,
) -> None:
"""Set up the Verisure binary sensors."""
sensors = []
hub.update_overview()
coordinator = hass.data[DOMAIN]
if int(hub.config.get(CONF_DOOR_WINDOW, 1)):
sensors = [VerisureEthernetStatus(coordinator)]
if int(coordinator.config.get(CONF_DOOR_WINDOW, 1)):
sensors.extend(
[
VerisureDoorWindowSensor(device_label)
for device_label in hub.get(
VerisureDoorWindowSensor(coordinator, device_label)
for device_label in coordinator.get(
"$.doorWindow.doorWindowDevice[*].deviceLabel"
)
]
)
sensors.extend([VerisureEthernetStatus()])
add_entities(sensors)
class VerisureDoorWindowSensor(BinarySensorEntity):
class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity):
"""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."""
super().__init__(coordinator)
self._device_label = device_label
@property
def name(self) -> str:
"""Return the name of the binary sensor."""
return hub.get_first(
return self.coordinator.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
self._device_label,
)
@ -56,7 +62,7 @@ class VerisureDoorWindowSensor(BinarySensorEntity):
def is_on(self) -> bool:
"""Return the state of the sensor."""
return (
hub.get_first(
self.coordinator.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
self._device_label,
)
@ -67,22 +73,19 @@ class VerisureDoorWindowSensor(BinarySensorEntity):
def available(self) -> bool:
"""Return True if entity is available."""
return (
hub.get_first(
self.coordinator.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
self._device_label,
)
is not None
)
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the state of the sensor."""
hub.update_overview()
class VerisureEthernetStatus(BinarySensorEntity):
class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity):
"""Representation of a Verisure VBOX internet status."""
coordinator: VerisureDataUpdateCoordinator
@property
def name(self) -> str:
"""Return the name of the binary sensor."""
@ -91,17 +94,12 @@ class VerisureEthernetStatus(BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return the state of the sensor."""
return hub.get_first("$.ethernetConnectedNow")
return self.coordinator.get_first("$.ethernetConnectedNow")
@property
def available(self) -> bool:
"""Return True if entity is available."""
return hub.get_first("$.ethernetConnectedNow") is not None
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the state of the sensor."""
hub.update_overview()
return self.coordinator.get_first("$.ethernetConnectedNow") is not None
@property
def device_class(self) -> str:

View File

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

View File

@ -1,16 +1,17 @@
"""Support for Verisure locks."""
from __future__ import annotations
from time import monotonic, sleep
import asyncio
from typing import Any, Callable
from homeassistant.components.lock import LockEntity
from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HUB as hub
from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, LOGGER
from . import VerisureDataUpdateCoordinator
from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, DOMAIN, LOGGER
def setup_platform(
@ -20,48 +21,48 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None,
) -> None:
"""Set up the Verisure lock platform."""
coordinator = hass.data[DOMAIN]
locks = []
if int(hub.config.get(CONF_LOCKS, 1)):
hub.update_overview()
if int(coordinator.config.get(CONF_LOCKS, 1)):
locks.extend(
[
VerisureDoorlock(device_label)
for device_label in hub.get("$.doorLockStatusList[*].deviceLabel")
VerisureDoorlock(coordinator, device_label)
for device_label in coordinator.get(
"$.doorLockStatusList[*].deviceLabel"
)
]
)
add_entities(locks)
class VerisureDoorlock(LockEntity):
class VerisureDoorlock(CoordinatorEntity, LockEntity):
"""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."""
super().__init__(coordinator)
self._device_label = device_label
self._state = None
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None
self._change_timestamp = 0
self._default_lock_code = hub.config.get(CONF_DEFAULT_LOCK_CODE)
self._digits = coordinator.config.get(CONF_CODE_DIGITS)
self._default_lock_code = coordinator.config.get(CONF_DEFAULT_LOCK_CODE)
@property
def name(self) -> str:
"""Return the name of the lock."""
return hub.get_first(
return self.coordinator.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].area", self._device_label
)
@property
def state(self) -> str | None:
"""Return the state of the lock."""
return self._state
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
hub.get_first(
self.coordinator.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')]", self._device_label
)
is not None
@ -70,78 +71,65 @@ class VerisureDoorlock(LockEntity):
@property
def changed_by(self) -> str | None:
"""Last change triggered by."""
return self._changed_by
return self.coordinator.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
self._device_label,
)
@property
def code_format(self) -> str:
"""Return the required six digit code."""
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
def is_locked(self) -> bool:
"""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."""
if self._state is None:
return
code = kwargs.get(ATTR_CODE, self._default_lock_code)
if code is None:
LOGGER.error("Code required but none provided")
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."""
if self._state == STATE_LOCKED:
return
code = kwargs.get(ATTR_CODE, self._default_lock_code)
if code is None:
LOGGER.error("Code required but none provided")
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."""
lock_state = "lock" if state == STATE_LOCKED else "unlock"
transaction_id = hub.session.set_lock_state(
code, self._device_label, lock_state
)["doorLockStateChangeTransactionId"]
target_state = "lock" if state == STATE_LOCKED else "unlock"
lock_state = await self.hass.async_add_executor_job(
self.coordinator.session.set_lock_state,
code,
self._device_label,
target_state,
)
LOGGER.debug("Verisure doorlock %s", state)
transaction = {}
attempts = 0
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
if attempts == 30:
break
if attempts > 1:
sleep(0.5)
await asyncio.sleep(0.5)
if transaction["result"] == "OK":
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.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HUB as hub
from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS
from . import VerisureDataUpdateCoordinator
from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS, DOMAIN
def setup_platform(
@ -18,34 +19,34 @@ def setup_platform(
discovery_info: dict[str, Any] | None = None,
) -> None:
"""Set up the Verisure platform."""
sensors = []
hub.update_overview()
coordinator = hass.data[DOMAIN]
if int(hub.config.get(CONF_THERMOMETERS, 1)):
sensors = []
if int(coordinator.config.get(CONF_THERMOMETERS, 1)):
sensors.extend(
[
VerisureThermometer(device_label)
for device_label in hub.get(
VerisureThermometer(coordinator, device_label)
for device_label in coordinator.get(
"$.climateValues[?(@.temperature)].deviceLabel"
)
]
)
if int(hub.config.get(CONF_HYDROMETERS, 1)):
if int(coordinator.config.get(CONF_HYDROMETERS, 1)):
sensors.extend(
[
VerisureHygrometer(device_label)
for device_label in hub.get(
VerisureHygrometer(coordinator, device_label)
for device_label in coordinator.get(
"$.climateValues[?(@.humidity)].deviceLabel"
)
]
)
if int(hub.config.get(CONF_MOUSE, 1)):
if int(coordinator.config.get(CONF_MOUSE, 1)):
sensors.extend(
[
VerisureMouseDetection(device_label)
for device_label in hub.get(
VerisureMouseDetection(coordinator, device_label)
for device_label in coordinator.get(
"$.eventCounts[?(@.deviceType=='MOUSE1')].deviceLabel"
)
]
@ -54,18 +55,23 @@ def setup_platform(
add_entities(sensors)
class VerisureThermometer(Entity):
class VerisureThermometer(CoordinatorEntity, Entity):
"""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."""
super().__init__(coordinator)
self._device_label = device_label
@property
def name(self) -> str:
"""Return the name of the device."""
return (
hub.get_first(
self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label
)
+ " temperature"
@ -74,7 +80,7 @@ class VerisureThermometer(Entity):
@property
def state(self) -> str | None:
"""Return the state of the device."""
return hub.get_first(
return self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].temperature", self._device_label
)
@ -82,7 +88,7 @@ class VerisureThermometer(Entity):
def available(self) -> bool:
"""Return True if entity is available."""
return (
hub.get_first(
self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].temperature",
self._device_label,
)
@ -94,24 +100,24 @@ class VerisureThermometer(Entity):
"""Return the unit of measurement of this entity."""
return TEMP_CELSIUS
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the sensor."""
hub.update_overview()
class VerisureHygrometer(Entity):
class VerisureHygrometer(CoordinatorEntity, Entity):
"""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."""
super().__init__(coordinator)
self._device_label = device_label
@property
def name(self) -> str:
"""Return the name of the device."""
return (
hub.get_first(
self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label
)
+ " humidity"
@ -120,7 +126,7 @@ class VerisureHygrometer(Entity):
@property
def state(self) -> str | None:
"""Return the state of the device."""
return hub.get_first(
return self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label
)
@ -128,7 +134,7 @@ class VerisureHygrometer(Entity):
def available(self) -> bool:
"""Return True if entity is available."""
return (
hub.get_first(
self.coordinator.get_first(
"$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label
)
is not None
@ -139,24 +145,24 @@ class VerisureHygrometer(Entity):
"""Return the unit of measurement of this entity."""
return PERCENTAGE
# pylint: disable=no-self-use
def update(self) -> None:
"""Update the sensor."""
hub.update_overview()
class VerisureMouseDetection(Entity):
class VerisureMouseDetection(CoordinatorEntity, Entity):
"""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."""
super().__init__(coordinator)
self._device_label = device_label
@property
def name(self) -> str:
"""Return the name of the device."""
return (
hub.get_first(
self.coordinator.get_first(
"$.eventCounts[?(@.deviceLabel=='%s')].area", self._device_label
)
+ " mouse"
@ -165,7 +171,7 @@ class VerisureMouseDetection(Entity):
@property
def state(self) -> str | None:
"""Return the state of the device."""
return hub.get_first(
return self.coordinator.get_first(
"$.eventCounts[?(@.deviceLabel=='%s')].detections", self._device_label
)
@ -173,7 +179,9 @@ class VerisureMouseDetection(Entity):
def available(self) -> bool:
"""Return True if entity is available."""
return (
hub.get_first("$.eventCounts[?(@.deviceLabel=='%s')]", self._device_label)
self.coordinator.get_first(
"$.eventCounts[?(@.deviceLabel=='%s')]", self._device_label
)
is not None
)
@ -181,8 +189,3 @@ class VerisureMouseDetection(Entity):
def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity."""
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 time import monotonic
from typing import Any, Callable, Literal
from typing import Any, Callable
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
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(
@ -16,25 +18,31 @@ def setup_platform(
config: dict[str, Any],
add_entities: Callable[[list[Entity], bool], None],
discovery_info: dict[str, Any] | None = None,
) -> None | Literal[False]:
) -> None:
"""Set up the Verisure switch platform."""
if not int(hub.config.get(CONF_SMARTPLUGS, 1)):
return False
coordinator = hass.data[DOMAIN]
hub.update_overview()
switches = [
VerisureSmartplug(device_label)
for device_label in hub.get("$.smartPlugs[*].deviceLabel")
]
if not int(coordinator.config.get(CONF_SMARTPLUGS, 1)):
return
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."""
def __init__(self, device_id: str):
coordinator: VerisureDataUpdateCoordinator
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, device_id: str
) -> None:
"""Initialize the Verisure device."""
super().__init__(coordinator)
self._device_label = device_id
self._change_timestamp = 0
self._state = False
@ -42,7 +50,7 @@ class VerisureSmartplug(SwitchEntity):
@property
def name(self) -> str:
"""Return the name or location of the smartplug."""
return hub.get_first(
return self.coordinator.get_first(
"$.smartPlugs[?(@.deviceLabel == '%s')].area", self._device_label
)
@ -52,7 +60,7 @@ class VerisureSmartplug(SwitchEntity):
if monotonic() - self._change_timestamp < 10:
return self._state
self._state = (
hub.get_first(
self.coordinator.get_first(
"$.smartPlugs[?(@.deviceLabel == '%s')].currentState",
self._device_label,
)
@ -64,23 +72,20 @@ class VerisureSmartplug(SwitchEntity):
def available(self) -> bool:
"""Return True if entity is available."""
return (
hub.get_first("$.smartPlugs[?(@.deviceLabel == '%s')]", self._device_label)
self.coordinator.get_first(
"$.smartPlugs[?(@.deviceLabel == '%s')]", self._device_label
)
is not None
)
def turn_on(self, **kwargs) -> None:
"""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._change_timestamp = monotonic()
def turn_off(self, **kwargs) -> None:
"""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._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
vilfo-api-client==0.3.2
# homeassistant.components.verisure
vsure==1.7.2
# homeassistant.components.vultr
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")