Update Verisure package to 2.6.1 (#89318)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: RobinBolder <33325401+RobinBolder@users.noreply.github.com>
Co-authored-by: Tobias Lindaaker <tobias@thobe.org>
This commit is contained in:
Niels Perfors 2023-03-26 19:32:25 +02:00 committed by GitHub
parent 6e92dac61f
commit 1baadc1d09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 294 additions and 219 deletions

View File

@ -1293,8 +1293,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/velux/ @Julius2342 /homeassistant/components/velux/ @Julius2342
/homeassistant/components/venstar/ @garbled1 /homeassistant/components/venstar/ @garbled1
/tests/components/venstar/ @garbled1 /tests/components/venstar/ @garbled1
/homeassistant/components/verisure/ @frenck /homeassistant/components/verisure/ @frenck @niro1987
/tests/components/verisure/ @frenck /tests/components/verisure/ @frenck @niro1987
/homeassistant/components/versasense/ @flamm3blemuff1n /homeassistant/components/versasense/ @flamm3blemuff1n
/homeassistant/components/version/ @ludeeus /homeassistant/components/version/ @ludeeus
/tests/components/version/ @ludeeus /tests/components/version/ @ludeeus

View File

@ -6,9 +6,9 @@ import os
from pathlib import Path from pathlib import Path
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.const import CONF_EMAIL, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
@ -34,11 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) coordinator = VerisureDataUpdateCoordinator(hass, entry=entry)
if not await coordinator.async_login(): if not await coordinator.async_login():
raise ConfigEntryAuthFailed raise ConfigEntryNotReady("Could not log in to verisure.")
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout)
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()

View File

@ -55,33 +55,49 @@ class VerisureAlarm(
"""Return the unique ID for this entity.""" """Return the unique ID for this entity."""
return self.coordinator.entry.data[CONF_GIID] return self.coordinator.entry.data[CONF_GIID]
async def _async_set_arm_state(self, state: str, code: str | None = None) -> None: async def _async_set_arm_state(
self, state: str, command_data: dict[str, str | dict[str, str]]
) -> None:
"""Send set arm state command.""" """Send set arm state command."""
arm_state = await self.hass.async_add_executor_job( arm_state = await self.hass.async_add_executor_job(
self.coordinator.verisure.set_arm_state, code, state self.coordinator.verisure.request, command_data
) )
LOGGER.debug("Verisure set arm state %s", state) LOGGER.debug("Verisure set arm state %s", state)
transaction = {} result = None
while "result" not in transaction: while result is None:
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
transaction = await self.hass.async_add_executor_job( transaction = await self.hass.async_add_executor_job(
self.coordinator.verisure.get_arm_state_transaction, self.coordinator.verisure.request,
arm_state["armStateChangeTransactionId"], self.coordinator.verisure.poll_arm_state(
list(arm_state["data"].values())[0], state
),
)
result = (
transaction.get("data", {})
.get("installation", {})
.get("armStateChangePollResult", {})
.get("result")
) )
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
async def async_alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
await self._async_set_arm_state("DISARMED", code) await self._async_set_arm_state(
"DISARMED", self.coordinator.verisure.disarm(code)
)
async def async_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."""
await self._async_set_arm_state("ARMED_HOME", code) await self._async_set_arm_state(
"ARMED_HOME", self.coordinator.verisure.arm_home(code)
)
async def async_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."""
await self._async_set_arm_state("ARMED_AWAY", code) await self._async_set_arm_state(
"ARMED_AWAY", self.coordinator.verisure.arm_away(code)
)
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:

View File

@ -109,9 +109,9 @@ class VerisureEthernetStatus(
@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 self.coordinator.data["ethernet"] return self.coordinator.data["broadband"]["isBroadbandConnected"]
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return super().available and self.coordinator.data["ethernet"] is not None return super().available and self.coordinator.data["broadband"] is not None

View File

@ -63,12 +63,12 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera)
self.serial_number = serial_number self.serial_number = serial_number
self._directory_path = directory_path self._directory_path = directory_path
self._image: str | None = None self._image: str | None = None
self._image_id = None self._image_id: str | None = None
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this entity.""" """Return device information about this entity."""
area = self.coordinator.data["cameras"][self.serial_number]["area"] area = self.coordinator.data["cameras"][self.serial_number]["device"]["area"]
return DeviceInfo( return DeviceInfo(
name=area, name=area,
suggested_area=area, suggested_area=area,
@ -95,16 +95,16 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera)
"""Check the contents of the image list.""" """Check the contents of the image list."""
self.coordinator.update_smartcam_imageseries() self.coordinator.update_smartcam_imageseries()
images = self.coordinator.imageseries.get("imageSeries", []) new_image = None
new_image_id = None for image in self.coordinator.imageseries:
for image in images:
if image["deviceLabel"] == self.serial_number: if image["deviceLabel"] == self.serial_number:
new_image_id = image["image"][0]["imageId"] new_image = image
break break
if not new_image_id: if not new_image:
return return
new_image_id = new_image["mediaId"]
if new_image_id in ("-1", self._image_id): if new_image_id in ("-1", self._image_id):
LOGGER.debug("The image is the same, or loading image_id") LOGGER.debug("The image is the same, or loading image_id")
return return
@ -113,9 +113,8 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], 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")
) )
self.coordinator.verisure.download_image( new_image_url = new_image["contentUrl"]
self.serial_number, new_image_id, new_image_path self.coordinator.verisure.download_image(new_image_url, 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()

View File

@ -56,7 +56,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
self.verisure = Verisure( self.verisure = Verisure(
username=self.email, username=self.email,
password=self.password, password=self.password,
cookieFileName=self.hass.config.path( cookie_file_name=self.hass.config.path(
STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}" STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}"
), ),
) )
@ -66,7 +66,9 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
except VerisureLoginError as ex: except VerisureLoginError as ex:
if "Multifactor authentication enabled" in str(ex): if "Multifactor authentication enabled" in str(ex):
try: try:
await self.hass.async_add_executor_job(self.verisure.login_mfa) await self.hass.async_add_executor_job(
self.verisure.request_mfa
)
except ( except (
VerisureLoginError, VerisureLoginError,
VerisureError, VerisureError,
@ -108,9 +110,8 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
self.verisure.mfa_validate, user_input[CONF_CODE], True self.verisure.validate_mfa, user_input[CONF_CODE]
) )
await self.hass.async_add_executor_job(self.verisure.login)
except VerisureLoginError as ex: except VerisureLoginError as ex:
LOGGER.debug("Could not log in to Verisure, %s", ex) LOGGER.debug("Could not log in to Verisure, %s", ex)
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
@ -136,9 +137,16 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Select Verisure installation to add.""" """Select Verisure installation to add."""
installations_data = await self.hass.async_add_executor_job(
self.verisure.get_installations
)
installations = { installations = {
inst["giid"]: f"{inst['alias']} ({inst['street']})" inst["giid"]: f"{inst['alias']} ({inst['address']['street']})"
for inst in self.verisure.installations or [] for inst in (
installations_data.get("data", {})
.get("account", {})
.get("installations", [])
)
} }
if user_input is None: if user_input is None:
@ -184,8 +192,8 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
self.verisure = Verisure( self.verisure = Verisure(
username=self.email, username=self.email,
password=self.password, password=self.password,
cookieFileName=self.hass.config.path( cookie_file_name=self.hass.config.path(
STORAGE_DIR, f"verisure-{user_input[CONF_EMAIL]}" STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}"
), ),
) )
@ -194,7 +202,9 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
except VerisureLoginError as ex: except VerisureLoginError as ex:
if "Multifactor authentication enabled" in str(ex): if "Multifactor authentication enabled" in str(ex):
try: try:
await self.hass.async_add_executor_job(self.verisure.login_mfa) await self.hass.async_add_executor_job(
self.verisure.request_mfa
)
except ( except (
VerisureLoginError, VerisureLoginError,
VerisureError, VerisureError,
@ -248,7 +258,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
self.verisure.mfa_validate, user_input[CONF_CODE], True self.verisure.validate_mfa, user_input[CONF_CODE]
) )
await self.hass.async_add_executor_job(self.verisure.login) await self.hass.async_add_executor_job(self.verisure.login)
except VerisureLoginError as ex: except VerisureLoginError as ex:

View File

@ -36,6 +36,9 @@ DEVICE_TYPE_NAME = {
"SMOKE3": "Smoke detector", "SMOKE3": "Smoke detector",
"VOICEBOX1": "VoiceBox", "VOICEBOX1": "VoiceBox",
"WATER1": "Water detector", "WATER1": "Water detector",
"SMOKE": "Smoke detector",
"SIREN": "Siren",
"VOICEBOX": "VoiceBox",
} }
ALARM_STATE_TO_HA = { ALARM_STATE_TO_HA = {

View File

@ -2,19 +2,21 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from http import HTTPStatus from time import sleep
from verisure import ( from verisure import (
Error as VerisureError, Error as VerisureError,
LoginError as VerisureLoginError,
ResponseError as VerisureResponseError, ResponseError as VerisureResponseError,
Session as Verisure, Session as Verisure,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import Event, HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .const import CONF_GIID, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER from .const import CONF_GIID, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
@ -25,13 +27,14 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the Verisure hub.""" """Initialize the Verisure hub."""
self.imageseries: dict[str, list] = {} self.imageseries: list[dict[str, str]] = []
self.entry = entry self.entry = entry
self._overview: list[dict] = []
self.verisure = Verisure( self.verisure = Verisure(
username=entry.data[CONF_EMAIL], username=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD], password=entry.data[CONF_PASSWORD],
cookieFileName=hass.config.path( cookie_file_name=hass.config.path(
STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}" STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}"
), ),
) )
@ -43,8 +46,11 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
async def async_login(self) -> bool: async def async_login(self) -> bool:
"""Login to Verisure.""" """Login to Verisure."""
try: try:
await self.hass.async_add_executor_job(self.verisure.login) await self.hass.async_add_executor_job(self.verisure.login_cookie)
except VerisureError as ex: except VerisureLoginError as ex:
LOGGER.error("Could not log in to verisure, %s", ex)
raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex
except VerisureResponseError 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
@ -54,62 +60,116 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
return True return True
async def async_logout(self, _event: Event) -> None:
"""Logout from Verisure."""
try:
await self.hass.async_add_executor_job(self.verisure.logout)
except VerisureError as ex:
LOGGER.error("Could not log out from verisure, %s", ex)
async def _async_update_data(self) -> dict: async def _async_update_data(self) -> dict:
"""Fetch data from Verisure.""" """Fetch data from Verisure."""
try: try:
overview = await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(self.verisure.update_cookie)
self.verisure.get_overview except VerisureLoginError as ex:
) LOGGER.error("Credentials expired for Verisure, %s", ex)
raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex
except VerisureResponseError as ex: except VerisureResponseError as ex:
LOGGER.error("Could not read overview, %s", ex) LOGGER.error("Could not log in to verisure, %s", ex)
if ex.status_code == HTTPStatus.SERVICE_UNAVAILABLE: raise ConfigEntryAuthFailed("Could not log in to verisure") from ex
LOGGER.info("Trying to log in again") try:
await self.async_login() overview = await self.hass.async_add_executor_job(
return {} self.verisure.request,
raise self.verisure.arm_state(),
self.verisure.broadband(),
self.verisure.cameras(),
self.verisure.climate(),
self.verisure.door_window(),
self.verisure.smart_lock(),
self.verisure.smartplugs(),
)
except VerisureResponseError as err:
LOGGER.debug("Cookie expired or service unavailable, %s", err)
overview = self._overview
try:
await self.hass.async_add_executor_job(self.verisure.update_cookie)
except VerisureResponseError as ex:
raise ConfigEntryAuthFailed("Credentials for Verisure expired.") from ex
except VerisureError as err:
LOGGER.error("Could not read overview, %s", err)
raise UpdateFailed("Could not read overview") from err
def unpack(overview: list, value: str) -> dict | list:
return next(
(
item["data"]["installation"][value]
for item in overview
if value in item.get("data", {}).get("installation", {})
),
[],
)
# Store data in a way Home Assistant can easily consume it # Store data in a way Home Assistant can easily consume it
self._overview = overview
return { return {
"alarm": overview["armState"], "alarm": unpack(overview, "armState"),
"ethernet": overview.get("ethernetConnectedNow"), "broadband": unpack(overview, "broadband"),
"cameras": { "cameras": {
device["deviceLabel"]: device device["device"]["deviceLabel"]: device
for device in overview["customerImageCameras"] for device in unpack(overview, "cameras")
}, },
"climate": { "climate": {
device["deviceLabel"]: device for device in overview["climateValues"] device["device"]["deviceLabel"]: device
for device in unpack(overview, "climates")
}, },
"door_window": { "door_window": {
device["deviceLabel"]: device device["device"]["deviceLabel"]: device
for device in overview["doorWindow"]["doorWindowDevice"] for device in unpack(overview, "doorWindows")
}, },
"locks": { "locks": {
device["deviceLabel"]: device device["device"]["deviceLabel"]: device
for device in overview["doorLockStatusList"] for device in unpack(overview, "smartLocks")
},
"mice": {
device["deviceLabel"]: device
for device in overview["eventCounts"]
if device["deviceType"] == "MOUSE1"
}, },
"smart_plugs": { "smart_plugs": {
device["deviceLabel"]: device for device in overview["smartPlugs"] device["device"]["deviceLabel"]: device
for device in unpack(overview, "smartplugs")
}, },
} }
@Throttle(timedelta(seconds=60)) @Throttle(timedelta(seconds=60))
def update_smartcam_imageseries(self) -> None: def update_smartcam_imageseries(self) -> None:
"""Update the image series.""" """Update the image series."""
self.imageseries = self.verisure.get_camera_imageseries() image_data = self.verisure.request(self.verisure.cameras_image_series())
self.imageseries = [
content
for series in (
image_data.get("data", {})
.get("ContentProviderMediaSearch", {})
.get("mediaSeriesList", [])
)
for content in series.get("deviceMediaList", [])
if content.get("contentType") == "IMAGE_JPEG"
]
@Throttle(timedelta(seconds=30)) @Throttle(timedelta(seconds=30))
def smartcam_capture(self, device_id: str) -> None: def smartcam_capture(self, device_id: str) -> None:
"""Capture a new image from a smartcam.""" """Capture a new image from a smartcam."""
self.verisure.capture_image(device_id) capture_request = self.verisure.request(
self.verisure.camera_get_request_id(device_id)
)
request_id = (
capture_request.get("data", {})
.get("ContentProviderCaptureImageRequest", {})
.get("requestId")
)
capture_status = None
attempts = 0
while capture_status != "AVAILABLE":
if attempts == 30:
break
if attempts > 1:
sleep(0.5)
attempts += 1
capture_data = self.verisure.request(
self.verisure.camera_capture(device_id, request_id)
)
capture_status = (
capture_data.get("data", {})
.get("installation", {})
.get("cameraContentProvider", {})
.get("captureImageRequestStatus", {})
.get("mediaRequestStatus")
)

View File

@ -16,6 +16,7 @@ TO_REDACT = {
"deviceArea", "deviceArea",
"name", "name",
"time", "time",
"reportTime",
"userString", "userString",
} }

View File

@ -77,7 +77,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this entity.""" """Return device information about this entity."""
area = self.coordinator.data["locks"][self.serial_number]["area"] area = self.coordinator.data["locks"][self.serial_number]["device"]["area"]
return DeviceInfo( return DeviceInfo(
name=area, name=area,
suggested_area=area, suggested_area=area,
@ -98,12 +98,16 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
@property @property
def changed_by(self) -> str | None: def changed_by(self) -> str | None:
"""Last change triggered by.""" """Last change triggered by."""
return self.coordinator.data["locks"][self.serial_number].get("userString") return (
self.coordinator.data["locks"][self.serial_number]
.get("user", {})
.get("name")
)
@property @property
def changed_method(self) -> str: def changed_method(self) -> str:
"""Last change method.""" """Last change method."""
return self.coordinator.data["locks"][self.serial_number]["method"] return self.coordinator.data["locks"][self.serial_number]["lockMethod"]
@property @property
def code_format(self) -> str: def code_format(self) -> str:
@ -114,8 +118,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
def is_locked(self) -> bool: def is_locked(self) -> bool:
"""Return true if lock is locked.""" """Return true if lock is locked."""
return ( return (
self.coordinator.data["locks"][self.serial_number]["lockedState"] self.coordinator.data["locks"][self.serial_number]["lockStatus"] == "LOCKED"
== "LOCKED"
) )
@property @property
@ -147,28 +150,39 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
async def async_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."""
target_state = "lock" if state == STATE_LOCKED else "unlock" command = (
lock_state = await self.hass.async_add_executor_job( self.coordinator.verisure.door_lock(self.serial_number, code)
self.coordinator.verisure.set_lock_state, if state == STATE_LOCKED
code, else self.coordinator.verisure.door_unlock(self.serial_number, code)
self.serial_number, )
target_state, lock_request = await self.hass.async_add_executor_job(
self.coordinator.verisure.request,
command,
) )
LOGGER.debug("Verisure doorlock %s", state) LOGGER.debug("Verisure doorlock %s", state)
transaction = {} transaction_id = lock_request.get("data", {}).get(command["operationName"])
target_state = "LOCKED" if state == STATE_LOCKED else "UNLOCKED"
lock_status = None
attempts = 0 attempts = 0
while "result" not in transaction: while lock_status != "OK":
transaction = await self.hass.async_add_executor_job(
self.coordinator.verisure.get_lock_state_transaction,
lock_state["doorLockStateChangeTransactionId"],
)
attempts += 1
if attempts == 30: if attempts == 30:
break break
if attempts > 1: if attempts > 1:
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
if transaction["result"] == "OK": attempts += 1
poll_data = await self.hass.async_add_executor_job(
self.coordinator.verisure.request,
self.coordinator.verisure.poll_lock_state(
transaction_id, self.serial_number, target_state
),
)
lock_status = (
poll_data.get("data", {})
.get("installation", {})
.get("doorLockStateChangePollResult", {})
.get("result")
)
if lock_status == "OK":
self._state = state self._state = state
def disable_autolock(self) -> None: def disable_autolock(self) -> None:

View File

@ -1,7 +1,7 @@
{ {
"domain": "verisure", "domain": "verisure",
"name": "Verisure", "name": "Verisure",
"codeowners": ["@frenck"], "codeowners": ["@frenck", "@niro1987"],
"config_flow": true, "config_flow": true,
"dhcp": [ "dhcp": [
{ {
@ -12,5 +12,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["verisure"], "loggers": ["verisure"],
"requirements": ["vsure==1.8.1"] "requirements": ["vsure==2.6.1"]
} }

View File

@ -28,18 +28,13 @@ async def async_setup_entry(
sensors: list[Entity] = [ sensors: list[Entity] = [
VerisureThermometer(coordinator, serial_number) VerisureThermometer(coordinator, serial_number)
for serial_number, values in coordinator.data["climate"].items() for serial_number, values in coordinator.data["climate"].items()
if "temperature" in values if "temperatureValue" in values
] ]
sensors.extend( sensors.extend(
VerisureHygrometer(coordinator, serial_number) VerisureHygrometer(coordinator, serial_number)
for serial_number, values in coordinator.data["climate"].items() for serial_number, values in coordinator.data["climate"].items()
if "humidity" in values if values.get("humidityEnabled")
)
sensors.extend(
VerisureMouseDetection(coordinator, serial_number)
for serial_number in coordinator.data["mice"]
) )
async_add_entities(sensors) async_add_entities(sensors)
@ -67,10 +62,10 @@ class VerisureThermometer(
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this entity.""" """Return device information about this entity."""
device_type = self.coordinator.data["climate"][self.serial_number].get( device_type = self.coordinator.data["climate"][self.serial_number]["device"][
"deviceType" "gui"
) ]["label"]
area = self.coordinator.data["climate"][self.serial_number]["deviceArea"] area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
return DeviceInfo( return DeviceInfo(
name=area, name=area,
suggested_area=area, suggested_area=area,
@ -84,7 +79,7 @@ class VerisureThermometer(
@property @property
def native_value(self) -> str | None: def native_value(self) -> str | None:
"""Return the state of the entity.""" """Return the state of the entity."""
return self.coordinator.data["climate"][self.serial_number]["temperature"] return self.coordinator.data["climate"][self.serial_number]["temperatureValue"]
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -92,7 +87,8 @@ class VerisureThermometer(
return ( return (
super().available super().available
and self.serial_number in self.coordinator.data["climate"] and self.serial_number in self.coordinator.data["climate"]
and "temperature" in self.coordinator.data["climate"][self.serial_number] and "temperatureValue"
in self.coordinator.data["climate"][self.serial_number]
) )
@ -118,10 +114,10 @@ class VerisureHygrometer(
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this entity.""" """Return device information about this entity."""
device_type = self.coordinator.data["climate"][self.serial_number].get( device_type = self.coordinator.data["climate"][self.serial_number]["device"][
"deviceType" "gui"
) ]["label"]
area = self.coordinator.data["climate"][self.serial_number]["deviceArea"] area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
return DeviceInfo( return DeviceInfo(
name=area, name=area,
suggested_area=area, suggested_area=area,
@ -135,7 +131,7 @@ class VerisureHygrometer(
@property @property
def native_value(self) -> str | None: def native_value(self) -> str | None:
"""Return the state of the entity.""" """Return the state of the entity."""
return self.coordinator.data["climate"][self.serial_number]["humidity"] return self.coordinator.data["climate"][self.serial_number]["humidityValue"]
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -143,51 +139,5 @@ class VerisureHygrometer(
return ( return (
super().available super().available
and self.serial_number in self.coordinator.data["climate"] and self.serial_number in self.coordinator.data["climate"]
and "humidity" in self.coordinator.data["climate"][self.serial_number] and "humidityValue" in self.coordinator.data["climate"][self.serial_number]
)
class VerisureMouseDetection(
CoordinatorEntity[VerisureDataUpdateCoordinator], SensorEntity
):
"""Representation of a Verisure mouse detector."""
_attr_name = "Mouse"
_attr_has_entity_name = True
_attr_native_unit_of_measurement = "Mice"
def __init__(
self, coordinator: VerisureDataUpdateCoordinator, serial_number: str
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{serial_number}_mice"
self.serial_number = serial_number
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this entity."""
area = self.coordinator.data["mice"][self.serial_number]["area"]
return DeviceInfo(
name=area,
suggested_area=area,
manufacturer="Verisure",
model="Mouse detector",
identifiers={(DOMAIN, self.serial_number)},
via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]),
configuration_url="https://mypages.verisure.com",
)
@property
def native_value(self) -> str | None:
"""Return the state of the entity."""
return self.coordinator.data["mice"][self.serial_number]["detections"]
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self.serial_number in self.coordinator.data["mice"]
and "detections" in self.coordinator.data["mice"][self.serial_number]
) )

View File

@ -47,7 +47,9 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this entity.""" """Return device information about this entity."""
area = self.coordinator.data["smart_plugs"][self.serial_number]["area"] area = self.coordinator.data["smart_plugs"][self.serial_number]["device"][
"area"
]
return DeviceInfo( return DeviceInfo(
name=area, name=area,
suggested_area=area, suggested_area=area,
@ -77,16 +79,23 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch
and self.serial_number in self.coordinator.data["smart_plugs"] and self.serial_number in self.coordinator.data["smart_plugs"]
) )
def turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Set smartplug status on.""" """Turn the smartplug on."""
self.coordinator.verisure.set_smartplug_state(self.serial_number, True) await self.async_set_plug_state(True)
self._state = True
self._change_timestamp = monotonic()
self.schedule_update_ha_state()
def turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Set smartplug status off.""" """Turn the smartplug off."""
self.coordinator.verisure.set_smartplug_state(self.serial_number, False) await self.async_set_plug_state(False)
self._state = False
async def async_set_plug_state(self, state: bool) -> None:
"""Set smartplug state."""
command: dict[
str, str | dict[str, str]
] = self.coordinator.verisure.set_smartplug(self.serial_number, state)
await self.hass.async_add_executor_job(
self.coordinator.verisure.request,
command,
)
self._state = state
self._change_timestamp = monotonic() self._change_timestamp = monotonic()
self.schedule_update_ha_state() await self.coordinator.async_request_refresh()

View File

@ -2589,7 +2589,7 @@ volkszaehler==0.4.0
volvooncall==0.10.2 volvooncall==0.10.2
# homeassistant.components.verisure # homeassistant.components.verisure
vsure==1.8.1 vsure==2.6.1
# homeassistant.components.vasttrafik # homeassistant.components.vasttrafik
vtjp==0.1.14 vtjp==0.1.14

View File

@ -1847,7 +1847,7 @@ vilfo-api-client==0.3.2
volvooncall==0.10.2 volvooncall==0.10.2
# homeassistant.components.verisure # homeassistant.components.verisure
vsure==1.8.1 vsure==2.6.1
# homeassistant.components.vulcan # homeassistant.components.vulcan
vulcan-api==2.3.0 vulcan-api==2.3.0

View File

@ -43,8 +43,22 @@ def mock_verisure_config_flow() -> Generator[None, MagicMock, None]:
) as verisure_mock: ) as verisure_mock:
verisure = verisure_mock.return_value verisure = verisure_mock.return_value
verisure.login.return_value = True verisure.login.return_value = True
verisure.installations = [ verisure.get_installations.return_value = {
{"giid": "12345", "alias": "ascending", "street": "12345th street"}, "data": {
{"giid": "54321", "alias": "descending", "street": "54321th street"}, "account": {
] "installations": [
{
"giid": "12345",
"alias": "ascending",
"address": {"street": "12345th street"},
},
{
"giid": "54321",
"alias": "descending",
"address": {"street": "54321th street"},
},
]
}
}
}
yield verisure yield verisure

View File

@ -35,9 +35,10 @@ async def test_full_user_flow_single_installation(
assert result.get("type") == FlowResultType.FORM assert result.get("type") == FlowResultType.FORM
assert result.get("errors") == {} assert result.get("errors") == {}
mock_verisure_config_flow.installations = [ mock_verisure_config_flow.get_installations.return_value = {
mock_verisure_config_flow.installations[0] k1: {k2: {k3: [v3[0]] for k3, v3 in v2.items()} for k2, v2 in v1.items()}
] for k1, v1 in mock_verisure_config_flow.get_installations.return_value.items()
}
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@ -133,9 +134,10 @@ async def test_full_user_flow_single_installation_with_mfa(
assert result2.get("step_id") == "mfa" assert result2.get("step_id") == "mfa"
mock_verisure_config_flow.login.side_effect = None mock_verisure_config_flow.login.side_effect = None
mock_verisure_config_flow.installations = [ mock_verisure_config_flow.get_installations.return_value = {
mock_verisure_config_flow.installations[0] k1: {k2: {k3: [v3[0]] for k3, v3 in v2.items()} for k2, v2 in v1.items()}
] for k1, v1 in mock_verisure_config_flow.get_installations.return_value.items()
}
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@ -153,9 +155,9 @@ async def test_full_user_flow_single_installation_with_mfa(
CONF_PASSWORD: "SuperS3cr3t!", CONF_PASSWORD: "SuperS3cr3t!",
} }
assert len(mock_verisure_config_flow.login.mock_calls) == 2 assert len(mock_verisure_config_flow.login.mock_calls) == 1
assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 1
assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 assert len(mock_verisure_config_flow.validate_mfa.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -215,9 +217,9 @@ async def test_full_user_flow_multiple_installations_with_mfa(
CONF_PASSWORD: "SuperS3cr3t!", CONF_PASSWORD: "SuperS3cr3t!",
} }
assert len(mock_verisure_config_flow.login.mock_calls) == 2 assert len(mock_verisure_config_flow.login.mock_calls) == 1
assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 1
assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 assert len(mock_verisure_config_flow.validate_mfa.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -257,7 +259,7 @@ async def test_verisure_errors(
mock_verisure_config_flow.login.side_effect = VerisureLoginError( mock_verisure_config_flow.login.side_effect = VerisureLoginError(
"Multifactor authentication enabled, disable or create MFA cookie" "Multifactor authentication enabled, disable or create MFA cookie"
) )
mock_verisure_config_flow.login_mfa.side_effect = side_effect mock_verisure_config_flow.request_mfa.side_effect = side_effect
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
@ -268,7 +270,7 @@ async def test_verisure_errors(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_verisure_config_flow.login_mfa.side_effect = None mock_verisure_config_flow.request_mfa.side_effect = None
assert result3.get("type") == FlowResultType.FORM assert result3.get("type") == FlowResultType.FORM
assert result3.get("step_id") == "user" assert result3.get("step_id") == "user"
@ -286,7 +288,7 @@ async def test_verisure_errors(
assert result4.get("type") == FlowResultType.FORM assert result4.get("type") == FlowResultType.FORM
assert result4.get("step_id") == "mfa" assert result4.get("step_id") == "mfa"
mock_verisure_config_flow.mfa_validate.side_effect = side_effect mock_verisure_config_flow.validate_mfa.side_effect = side_effect
result5 = await hass.config_entries.flow.async_configure( result5 = await hass.config_entries.flow.async_configure(
result4["flow_id"], result4["flow_id"],
@ -298,11 +300,11 @@ async def test_verisure_errors(
assert result5.get("step_id") == "mfa" assert result5.get("step_id") == "mfa"
assert result5.get("errors") == {"base": error} assert result5.get("errors") == {"base": error}
mock_verisure_config_flow.installations = [ mock_verisure_config_flow.get_installations.return_value = {
mock_verisure_config_flow.installations[0] k1: {k2: {k3: [v3[0]] for k3, v3 in v2.items()} for k2, v2 in v1.items()}
] for k1, v1 in mock_verisure_config_flow.get_installations.return_value.items()
}
mock_verisure_config_flow.mfa_validate.side_effect = None mock_verisure_config_flow.validate_mfa.side_effect = None
mock_verisure_config_flow.login.side_effect = None mock_verisure_config_flow.login.side_effect = None
result6 = await hass.config_entries.flow.async_configure( result6 = await hass.config_entries.flow.async_configure(
@ -321,9 +323,9 @@ async def test_verisure_errors(
CONF_PASSWORD: "SuperS3cr3t!", CONF_PASSWORD: "SuperS3cr3t!",
} }
assert len(mock_verisure_config_flow.login.mock_calls) == 4 assert len(mock_verisure_config_flow.login.mock_calls) == 3
assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 2
assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_verisure_config_flow.validate_mfa.mock_calls) == 2
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -441,8 +443,8 @@ async def test_reauth_flow_with_mfa(
} }
assert len(mock_verisure_config_flow.login.mock_calls) == 2 assert len(mock_verisure_config_flow.login.mock_calls) == 2
assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 1
assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 assert len(mock_verisure_config_flow.validate_mfa.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -491,7 +493,7 @@ async def test_reauth_flow_errors(
mock_verisure_config_flow.login.side_effect = VerisureLoginError( mock_verisure_config_flow.login.side_effect = VerisureLoginError(
"Multifactor authentication enabled, disable or create MFA cookie" "Multifactor authentication enabled, disable or create MFA cookie"
) )
mock_verisure_config_flow.login_mfa.side_effect = side_effect mock_verisure_config_flow.request_mfa.side_effect = side_effect
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
@ -506,7 +508,7 @@ async def test_reauth_flow_errors(
assert result3.get("step_id") == "reauth_confirm" assert result3.get("step_id") == "reauth_confirm"
assert result3.get("errors") == {"base": "unknown_mfa"} assert result3.get("errors") == {"base": "unknown_mfa"}
mock_verisure_config_flow.login_mfa.side_effect = None mock_verisure_config_flow.request_mfa.side_effect = None
result4 = await hass.config_entries.flow.async_configure( result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"], result3["flow_id"],
@ -520,7 +522,7 @@ async def test_reauth_flow_errors(
assert result4.get("type") == FlowResultType.FORM assert result4.get("type") == FlowResultType.FORM
assert result4.get("step_id") == "reauth_mfa" assert result4.get("step_id") == "reauth_mfa"
mock_verisure_config_flow.mfa_validate.side_effect = side_effect mock_verisure_config_flow.validate_mfa.side_effect = side_effect
result5 = await hass.config_entries.flow.async_configure( result5 = await hass.config_entries.flow.async_configure(
result4["flow_id"], result4["flow_id"],
@ -532,11 +534,12 @@ async def test_reauth_flow_errors(
assert result5.get("step_id") == "reauth_mfa" assert result5.get("step_id") == "reauth_mfa"
assert result5.get("errors") == {"base": error} assert result5.get("errors") == {"base": error}
mock_verisure_config_flow.mfa_validate.side_effect = None mock_verisure_config_flow.validate_mfa.side_effect = None
mock_verisure_config_flow.login.side_effect = None mock_verisure_config_flow.login.side_effect = None
mock_verisure_config_flow.installations = [ mock_verisure_config_flow.get_installations.return_value = {
mock_verisure_config_flow.installations[0] k1: {k2: {k3: [v3[0]] for k3, v3 in v2.items()} for k2, v2 in v1.items()}
] for k1, v1 in mock_verisure_config_flow.get_installations.return_value.items()
}
await hass.config_entries.flow.async_configure( await hass.config_entries.flow.async_configure(
result5["flow_id"], result5["flow_id"],
@ -553,8 +556,8 @@ async def test_reauth_flow_errors(
} }
assert len(mock_verisure_config_flow.login.mock_calls) == 4 assert len(mock_verisure_config_flow.login.mock_calls) == 4
assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 2
assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_verisure_config_flow.validate_mfa.mock_calls) == 2
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1