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
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/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

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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 = {

View File

@ -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")
)

View File

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

View File

@ -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:

View File

@ -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"]
}

View File

@ -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]
)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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