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>pull/88759/head
parent
6e92dac61f
commit
1baadc1d09
|
@ -1293,8 +1293,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/velux/ @Julius2342
|
||||
/homeassistant/components/venstar/ @garbled1
|
||||
/tests/components/venstar/ @garbled1
|
||||
/homeassistant/components/verisure/ @frenck
|
||||
/tests/components/verisure/ @frenck
|
||||
/homeassistant/components/verisure/ @frenck @niro1987
|
||||
/tests/components/verisure/ @frenck @niro1987
|
||||
/homeassistant/components/versasense/ @flamm3blemuff1n
|
||||
/homeassistant/components/version/ @ludeeus
|
||||
/tests/components/version/ @ludeeus
|
||||
|
|
|
@ -6,9 +6,9 @@ import os
|
|||
from pathlib import Path
|
||||
|
||||
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.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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)
|
||||
|
||||
if not await coordinator.async_login():
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout)
|
||||
)
|
||||
raise ConfigEntryNotReady("Could not log in to verisure.")
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
|
|
@ -55,33 +55,49 @@ class VerisureAlarm(
|
|||
"""Return the unique ID for this entity."""
|
||||
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."""
|
||||
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)
|
||||
transaction = {}
|
||||
while "result" not in transaction:
|
||||
result = None
|
||||
while result is None:
|
||||
await asyncio.sleep(0.5)
|
||||
transaction = await self.hass.async_add_executor_job(
|
||||
self.coordinator.verisure.get_arm_state_transaction,
|
||||
arm_state["armStateChangeTransactionId"],
|
||||
self.coordinator.verisure.request,
|
||||
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()
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""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:
|
||||
"""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:
|
||||
"""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
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
|
|
|
@ -109,9 +109,9 @@ class VerisureEthernetStatus(
|
|||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.data["ethernet"]
|
||||
return self.coordinator.data["broadband"]["isBroadbandConnected"]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""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
|
||||
|
|
|
@ -63,12 +63,12 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera)
|
|||
self.serial_number = serial_number
|
||||
self._directory_path = directory_path
|
||||
self._image: str | None = None
|
||||
self._image_id = None
|
||||
self._image_id: str | None = None
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""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(
|
||||
name=area,
|
||||
suggested_area=area,
|
||||
|
@ -95,16 +95,16 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera)
|
|||
"""Check the contents of the image list."""
|
||||
self.coordinator.update_smartcam_imageseries()
|
||||
|
||||
images = self.coordinator.imageseries.get("imageSeries", [])
|
||||
new_image_id = None
|
||||
for image in images:
|
||||
new_image = None
|
||||
for image in self.coordinator.imageseries:
|
||||
if image["deviceLabel"] == self.serial_number:
|
||||
new_image_id = image["image"][0]["imageId"]
|
||||
new_image = image
|
||||
break
|
||||
|
||||
if not new_image_id:
|
||||
if not new_image:
|
||||
return
|
||||
|
||||
new_image_id = new_image["mediaId"]
|
||||
if new_image_id in ("-1", self._image_id):
|
||||
LOGGER.debug("The image is the same, or loading image_id")
|
||||
return
|
||||
|
@ -113,9 +113,8 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera)
|
|||
new_image_path = os.path.join(
|
||||
self._directory_path, "{}{}".format(new_image_id, ".jpg")
|
||||
)
|
||||
self.coordinator.verisure.download_image(
|
||||
self.serial_number, new_image_id, new_image_path
|
||||
)
|
||||
new_image_url = new_image["contentUrl"]
|
||||
self.coordinator.verisure.download_image(new_image_url, new_image_path)
|
||||
LOGGER.debug("Old image_id=%s", self._image_id)
|
||||
self.delete_image()
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
self.verisure = Verisure(
|
||||
username=self.email,
|
||||
password=self.password,
|
||||
cookieFileName=self.hass.config.path(
|
||||
cookie_file_name=self.hass.config.path(
|
||||
STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}"
|
||||
),
|
||||
)
|
||||
|
@ -66,7 +66,9 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
except VerisureLoginError as ex:
|
||||
if "Multifactor authentication enabled" in str(ex):
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.verisure.login_mfa)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.verisure.request_mfa
|
||||
)
|
||||
except (
|
||||
VerisureLoginError,
|
||||
VerisureError,
|
||||
|
@ -108,9 +110,8 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
if user_input is not None:
|
||||
try:
|
||||
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:
|
||||
LOGGER.debug("Could not log in to Verisure, %s", ex)
|
||||
errors["base"] = "invalid_auth"
|
||||
|
@ -136,9 +137,16 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Select Verisure installation to add."""
|
||||
installations_data = await self.hass.async_add_executor_job(
|
||||
self.verisure.get_installations
|
||||
)
|
||||
installations = {
|
||||
inst["giid"]: f"{inst['alias']} ({inst['street']})"
|
||||
for inst in self.verisure.installations or []
|
||||
inst["giid"]: f"{inst['alias']} ({inst['address']['street']})"
|
||||
for inst in (
|
||||
installations_data.get("data", {})
|
||||
.get("account", {})
|
||||
.get("installations", [])
|
||||
)
|
||||
}
|
||||
|
||||
if user_input is None:
|
||||
|
@ -184,8 +192,8 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
self.verisure = Verisure(
|
||||
username=self.email,
|
||||
password=self.password,
|
||||
cookieFileName=self.hass.config.path(
|
||||
STORAGE_DIR, f"verisure-{user_input[CONF_EMAIL]}"
|
||||
cookie_file_name=self.hass.config.path(
|
||||
STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}"
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -194,7 +202,9 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
except VerisureLoginError as ex:
|
||||
if "Multifactor authentication enabled" in str(ex):
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.verisure.login_mfa)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.verisure.request_mfa
|
||||
)
|
||||
except (
|
||||
VerisureLoginError,
|
||||
VerisureError,
|
||||
|
@ -248,7 +258,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
if user_input is not None:
|
||||
try:
|
||||
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:
|
||||
|
|
|
@ -36,6 +36,9 @@ DEVICE_TYPE_NAME = {
|
|||
"SMOKE3": "Smoke detector",
|
||||
"VOICEBOX1": "VoiceBox",
|
||||
"WATER1": "Water detector",
|
||||
"SMOKE": "Smoke detector",
|
||||
"SIREN": "Siren",
|
||||
"VOICEBOX": "VoiceBox",
|
||||
}
|
||||
|
||||
ALARM_STATE_TO_HA = {
|
||||
|
|
|
@ -2,19 +2,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from time import sleep
|
||||
|
||||
from verisure import (
|
||||
Error as VerisureError,
|
||||
LoginError as VerisureLoginError,
|
||||
ResponseError as VerisureResponseError,
|
||||
Session as Verisure,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
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:
|
||||
"""Initialize the Verisure hub."""
|
||||
self.imageseries: dict[str, list] = {}
|
||||
self.imageseries: list[dict[str, str]] = []
|
||||
self.entry = entry
|
||||
self._overview: list[dict] = []
|
||||
|
||||
self.verisure = Verisure(
|
||||
username=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
cookieFileName=hass.config.path(
|
||||
cookie_file_name=hass.config.path(
|
||||
STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}"
|
||||
),
|
||||
)
|
||||
|
@ -43,8 +46,11 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
async def async_login(self) -> bool:
|
||||
"""Login to Verisure."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.verisure.login)
|
||||
except VerisureError as ex:
|
||||
await self.hass.async_add_executor_job(self.verisure.login_cookie)
|
||||
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)
|
||||
return False
|
||||
|
||||
|
@ -54,62 +60,116 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
|
||||
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:
|
||||
"""Fetch data from Verisure."""
|
||||
try:
|
||||
overview = await self.hass.async_add_executor_job(
|
||||
self.verisure.get_overview
|
||||
)
|
||||
await self.hass.async_add_executor_job(self.verisure.update_cookie)
|
||||
except VerisureLoginError as ex:
|
||||
LOGGER.error("Credentials expired for Verisure, %s", ex)
|
||||
raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex
|
||||
except VerisureResponseError as ex:
|
||||
LOGGER.error("Could not read overview, %s", ex)
|
||||
if ex.status_code == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||
LOGGER.info("Trying to log in again")
|
||||
await self.async_login()
|
||||
return {}
|
||||
raise
|
||||
LOGGER.error("Could not log in to verisure, %s", ex)
|
||||
raise ConfigEntryAuthFailed("Could not log in to verisure") from ex
|
||||
try:
|
||||
overview = await self.hass.async_add_executor_job(
|
||||
self.verisure.request,
|
||||
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
|
||||
self._overview = overview
|
||||
return {
|
||||
"alarm": overview["armState"],
|
||||
"ethernet": overview.get("ethernetConnectedNow"),
|
||||
"alarm": unpack(overview, "armState"),
|
||||
"broadband": unpack(overview, "broadband"),
|
||||
"cameras": {
|
||||
device["deviceLabel"]: device
|
||||
for device in overview["customerImageCameras"]
|
||||
device["device"]["deviceLabel"]: device
|
||||
for device in unpack(overview, "cameras")
|
||||
},
|
||||
"climate": {
|
||||
device["deviceLabel"]: device for device in overview["climateValues"]
|
||||
device["device"]["deviceLabel"]: device
|
||||
for device in unpack(overview, "climates")
|
||||
},
|
||||
"door_window": {
|
||||
device["deviceLabel"]: device
|
||||
for device in overview["doorWindow"]["doorWindowDevice"]
|
||||
device["device"]["deviceLabel"]: device
|
||||
for device in unpack(overview, "doorWindows")
|
||||
},
|
||||
"locks": {
|
||||
device["deviceLabel"]: device
|
||||
for device in overview["doorLockStatusList"]
|
||||
},
|
||||
"mice": {
|
||||
device["deviceLabel"]: device
|
||||
for device in overview["eventCounts"]
|
||||
if device["deviceType"] == "MOUSE1"
|
||||
device["device"]["deviceLabel"]: device
|
||||
for device in unpack(overview, "smartLocks")
|
||||
},
|
||||
"smart_plugs": {
|
||||
device["deviceLabel"]: device for device in overview["smartPlugs"]
|
||||
device["device"]["deviceLabel"]: device
|
||||
for device in unpack(overview, "smartplugs")
|
||||
},
|
||||
}
|
||||
|
||||
@Throttle(timedelta(seconds=60))
|
||||
def update_smartcam_imageseries(self) -> None:
|
||||
"""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))
|
||||
def smartcam_capture(self, device_id: str) -> None:
|
||||
"""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")
|
||||
)
|
||||
|
|
|
@ -16,6 +16,7 @@ TO_REDACT = {
|
|||
"deviceArea",
|
||||
"name",
|
||||
"time",
|
||||
"reportTime",
|
||||
"userString",
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
|
|||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""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(
|
||||
name=area,
|
||||
suggested_area=area,
|
||||
|
@ -98,12 +98,16 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
|
|||
@property
|
||||
def changed_by(self) -> str | None:
|
||||
"""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
|
||||
def changed_method(self) -> str:
|
||||
"""Last change method."""
|
||||
return self.coordinator.data["locks"][self.serial_number]["method"]
|
||||
return self.coordinator.data["locks"][self.serial_number]["lockMethod"]
|
||||
|
||||
@property
|
||||
def code_format(self) -> str:
|
||||
|
@ -114,8 +118,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
|
|||
def is_locked(self) -> bool:
|
||||
"""Return true if lock is locked."""
|
||||
return (
|
||||
self.coordinator.data["locks"][self.serial_number]["lockedState"]
|
||||
== "LOCKED"
|
||||
self.coordinator.data["locks"][self.serial_number]["lockStatus"] == "LOCKED"
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -147,28 +150,39 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
|
|||
|
||||
async def async_set_lock_state(self, code: str, state: str) -> None:
|
||||
"""Send set lock state command."""
|
||||
target_state = "lock" if state == STATE_LOCKED else "unlock"
|
||||
lock_state = await self.hass.async_add_executor_job(
|
||||
self.coordinator.verisure.set_lock_state,
|
||||
code,
|
||||
self.serial_number,
|
||||
target_state,
|
||||
command = (
|
||||
self.coordinator.verisure.door_lock(self.serial_number, code)
|
||||
if state == STATE_LOCKED
|
||||
else self.coordinator.verisure.door_unlock(self.serial_number, code)
|
||||
)
|
||||
lock_request = await self.hass.async_add_executor_job(
|
||||
self.coordinator.verisure.request,
|
||||
command,
|
||||
)
|
||||
|
||||
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
|
||||
while "result" not in transaction:
|
||||
transaction = await self.hass.async_add_executor_job(
|
||||
self.coordinator.verisure.get_lock_state_transaction,
|
||||
lock_state["doorLockStateChangeTransactionId"],
|
||||
)
|
||||
attempts += 1
|
||||
while lock_status != "OK":
|
||||
if attempts == 30:
|
||||
break
|
||||
if attempts > 1:
|
||||
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
|
||||
|
||||
def disable_autolock(self) -> None:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"domain": "verisure",
|
||||
"name": "Verisure",
|
||||
"codeowners": ["@frenck"],
|
||||
"codeowners": ["@frenck", "@niro1987"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
|
@ -12,5 +12,5 @@
|
|||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["verisure"],
|
||||
"requirements": ["vsure==1.8.1"]
|
||||
"requirements": ["vsure==2.6.1"]
|
||||
}
|
||||
|
|
|
@ -28,18 +28,13 @@ async def async_setup_entry(
|
|||
sensors: list[Entity] = [
|
||||
VerisureThermometer(coordinator, serial_number)
|
||||
for serial_number, values in coordinator.data["climate"].items()
|
||||
if "temperature" in values
|
||||
if "temperatureValue" in values
|
||||
]
|
||||
|
||||
sensors.extend(
|
||||
VerisureHygrometer(coordinator, serial_number)
|
||||
for serial_number, values in coordinator.data["climate"].items()
|
||||
if "humidity" in values
|
||||
)
|
||||
|
||||
sensors.extend(
|
||||
VerisureMouseDetection(coordinator, serial_number)
|
||||
for serial_number in coordinator.data["mice"]
|
||||
if values.get("humidityEnabled")
|
||||
)
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
@ -67,10 +62,10 @@ class VerisureThermometer(
|
|||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
device_type = self.coordinator.data["climate"][self.serial_number].get(
|
||||
"deviceType"
|
||||
)
|
||||
area = self.coordinator.data["climate"][self.serial_number]["deviceArea"]
|
||||
device_type = self.coordinator.data["climate"][self.serial_number]["device"][
|
||||
"gui"
|
||||
]["label"]
|
||||
area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
|
||||
return DeviceInfo(
|
||||
name=area,
|
||||
suggested_area=area,
|
||||
|
@ -84,7 +79,7 @@ class VerisureThermometer(
|
|||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""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
|
||||
def available(self) -> bool:
|
||||
|
@ -92,7 +87,8 @@ class VerisureThermometer(
|
|||
return (
|
||||
super().available
|
||||
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
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
device_type = self.coordinator.data["climate"][self.serial_number].get(
|
||||
"deviceType"
|
||||
)
|
||||
area = self.coordinator.data["climate"][self.serial_number]["deviceArea"]
|
||||
device_type = self.coordinator.data["climate"][self.serial_number]["device"][
|
||||
"gui"
|
||||
]["label"]
|
||||
area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
|
||||
return DeviceInfo(
|
||||
name=area,
|
||||
suggested_area=area,
|
||||
|
@ -135,7 +131,7 @@ class VerisureHygrometer(
|
|||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""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
|
||||
def available(self) -> bool:
|
||||
|
@ -143,51 +139,5 @@ class VerisureHygrometer(
|
|||
return (
|
||||
super().available
|
||||
and self.serial_number in self.coordinator.data["climate"]
|
||||
and "humidity" 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]
|
||||
and "humidityValue" in self.coordinator.data["climate"][self.serial_number]
|
||||
)
|
||||
|
|
|
@ -47,7 +47,9 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch
|
|||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""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(
|
||||
name=area,
|
||||
suggested_area=area,
|
||||
|
@ -77,16 +79,23 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch
|
|||
and self.serial_number in self.coordinator.data["smart_plugs"]
|
||||
)
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Set smartplug status on."""
|
||||
self.coordinator.verisure.set_smartplug_state(self.serial_number, True)
|
||||
self._state = True
|
||||
self._change_timestamp = monotonic()
|
||||
self.schedule_update_ha_state()
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the smartplug on."""
|
||||
await self.async_set_plug_state(True)
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Set smartplug status off."""
|
||||
self.coordinator.verisure.set_smartplug_state(self.serial_number, False)
|
||||
self._state = False
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the smartplug off."""
|
||||
await self.async_set_plug_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.schedule_update_ha_state()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
|
|
@ -2589,7 +2589,7 @@ volkszaehler==0.4.0
|
|||
volvooncall==0.10.2
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==1.8.1
|
||||
vsure==2.6.1
|
||||
|
||||
# homeassistant.components.vasttrafik
|
||||
vtjp==0.1.14
|
||||
|
|
|
@ -1847,7 +1847,7 @@ vilfo-api-client==0.3.2
|
|||
volvooncall==0.10.2
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==1.8.1
|
||||
vsure==2.6.1
|
||||
|
||||
# homeassistant.components.vulcan
|
||||
vulcan-api==2.3.0
|
||||
|
|
|
@ -43,8 +43,22 @@ def mock_verisure_config_flow() -> Generator[None, MagicMock, None]:
|
|||
) as verisure_mock:
|
||||
verisure = verisure_mock.return_value
|
||||
verisure.login.return_value = True
|
||||
verisure.installations = [
|
||||
{"giid": "12345", "alias": "ascending", "street": "12345th street"},
|
||||
{"giid": "54321", "alias": "descending", "street": "54321th street"},
|
||||
]
|
||||
verisure.get_installations.return_value = {
|
||||
"data": {
|
||||
"account": {
|
||||
"installations": [
|
||||
{
|
||||
"giid": "12345",
|
||||
"alias": "ascending",
|
||||
"address": {"street": "12345th street"},
|
||||
},
|
||||
{
|
||||
"giid": "54321",
|
||||
"alias": "descending",
|
||||
"address": {"street": "54321th street"},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
yield verisure
|
||||
|
|
|
@ -35,9 +35,10 @@ async def test_full_user_flow_single_installation(
|
|||
assert result.get("type") == FlowResultType.FORM
|
||||
assert result.get("errors") == {}
|
||||
|
||||
mock_verisure_config_flow.installations = [
|
||||
mock_verisure_config_flow.installations[0]
|
||||
]
|
||||
mock_verisure_config_flow.get_installations.return_value = {
|
||||
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(
|
||||
result["flow_id"],
|
||||
|
@ -133,9 +134,10 @@ async def test_full_user_flow_single_installation_with_mfa(
|
|||
assert result2.get("step_id") == "mfa"
|
||||
|
||||
mock_verisure_config_flow.login.side_effect = None
|
||||
mock_verisure_config_flow.installations = [
|
||||
mock_verisure_config_flow.installations[0]
|
||||
]
|
||||
mock_verisure_config_flow.get_installations.return_value = {
|
||||
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(
|
||||
result["flow_id"],
|
||||
|
@ -153,9 +155,9 @@ async def test_full_user_flow_single_installation_with_mfa(
|
|||
CONF_PASSWORD: "SuperS3cr3t!",
|
||||
}
|
||||
|
||||
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.mfa_validate.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.login.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.validate_mfa.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!",
|
||||
}
|
||||
|
||||
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.mfa_validate.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.login.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.validate_mfa.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(
|
||||
"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(
|
||||
result2["flow_id"],
|
||||
|
@ -268,7 +270,7 @@ async def test_verisure_errors(
|
|||
)
|
||||
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("step_id") == "user"
|
||||
|
@ -286,7 +288,7 @@ async def test_verisure_errors(
|
|||
assert result4.get("type") == FlowResultType.FORM
|
||||
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(
|
||||
result4["flow_id"],
|
||||
|
@ -298,11 +300,11 @@ async def test_verisure_errors(
|
|||
assert result5.get("step_id") == "mfa"
|
||||
assert result5.get("errors") == {"base": error}
|
||||
|
||||
mock_verisure_config_flow.installations = [
|
||||
mock_verisure_config_flow.installations[0]
|
||||
]
|
||||
|
||||
mock_verisure_config_flow.mfa_validate.side_effect = None
|
||||
mock_verisure_config_flow.get_installations.return_value = {
|
||||
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.validate_mfa.side_effect = None
|
||||
mock_verisure_config_flow.login.side_effect = None
|
||||
|
||||
result6 = await hass.config_entries.flow.async_configure(
|
||||
|
@ -321,9 +323,9 @@ async def test_verisure_errors(
|
|||
CONF_PASSWORD: "SuperS3cr3t!",
|
||||
}
|
||||
|
||||
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.mfa_validate.mock_calls) == 2
|
||||
assert len(mock_verisure_config_flow.login.mock_calls) == 3
|
||||
assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 2
|
||||
assert len(mock_verisure_config_flow.validate_mfa.mock_calls) == 2
|
||||
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_mfa.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 1
|
||||
assert len(mock_verisure_config_flow.validate_mfa.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(
|
||||
"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(
|
||||
result2["flow_id"],
|
||||
|
@ -506,7 +508,7 @@ async def test_reauth_flow_errors(
|
|||
assert result3.get("step_id") == "reauth_confirm"
|
||||
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(
|
||||
result3["flow_id"],
|
||||
|
@ -520,7 +522,7 @@ async def test_reauth_flow_errors(
|
|||
assert result4.get("type") == FlowResultType.FORM
|
||||
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(
|
||||
result4["flow_id"],
|
||||
|
@ -532,11 +534,12 @@ async def test_reauth_flow_errors(
|
|||
assert result5.get("step_id") == "reauth_mfa"
|
||||
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.installations = [
|
||||
mock_verisure_config_flow.installations[0]
|
||||
]
|
||||
mock_verisure_config_flow.get_installations.return_value = {
|
||||
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(
|
||||
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_mfa.mock_calls) == 2
|
||||
assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2
|
||||
assert len(mock_verisure_config_flow.request_mfa.mock_calls) == 2
|
||||
assert len(mock_verisure_config_flow.validate_mfa.mock_calls) == 2
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue