diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index ccb479814ab..00d970f133f 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -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: diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index c791bfc38dc..d0a93fb45f9 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -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) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index e30f008dba9..bdefa2af858 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -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: diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index ad6840c0614..4e15b7a88b2 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -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 ) diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index b2e1cfb3db0..8fc067308fb 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -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() diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 6582dfc409a..483d03a1bb5 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -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() diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index e5e19bd6d13..9329d94331a 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -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() diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94f1bbba464..2b77de8022b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/verisure/__init__.py b/tests/components/verisure/__init__.py deleted file mode 100644 index 0382661dbe3..00000000000 --- a/tests/components/verisure/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for Verisure integration.""" diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py deleted file mode 100644 index 611adde19d9..00000000000 --- a/tests/components/verisure/test_ethernet_status.py +++ /dev/null @@ -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" diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py deleted file mode 100644 index d41bbab2037..00000000000 --- a/tests/components/verisure/test_lock.py +++ /dev/null @@ -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")