From 4f2b75fe37ff7892d09573010dc07d742d5dd029 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 8 Oct 2024 01:50:07 +0000 Subject: [PATCH] Handle race condition and alarm panel unique_id change --- custom_components/vivint/__init__.py | 4 +-- .../vivint/alarm_control_panel.py | 25 ++++++++++++++++++- custom_components/vivint/hub.py | 19 ++++++++------ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/custom_components/vivint/__init__.py b/custom_components/vivint/__init__.py index 18a4eb3..543cc34 100644 --- a/custom_components/vivint/__init__.py +++ b/custom_components/vivint/__init__.py @@ -155,9 +155,9 @@ def async_on_device_event(event_type: str, viv_device: VivintDevice) -> None: dev_reg.async_remove_device(device.id) @callback - def _async_save_tokens(ev: Event) -> None: + async def _async_save_tokens(ev: Event) -> None: """Save tokens to the config entry data.""" - undo_listener() + await entry.runtime_data.disconnect() hass.config_entries.async_update_entry( entry, data=entry.data | {CONF_REFRESH_TOKEN: hub.account.refresh_token} ) diff --git a/custom_components/vivint/alarm_control_panel.py b/custom_components/vivint/alarm_control_panel.py index a0716a2..3bbf987 100644 --- a/custom_components/vivint/alarm_control_panel.py +++ b/custom_components/vivint/alarm_control_panel.py @@ -2,10 +2,13 @@ from __future__ import annotations +from typing import Iterable + from vivintpy.devices.alarm_panel import AlarmPanel from vivintpy.enums import ArmedState from homeassistant.components.alarm_control_panel import ( + DOMAIN as PLATFORM, AlarmControlPanelEntity, AlarmControlPanelEntityFeature as Feature, CodeFormat, @@ -19,11 +22,12 @@ STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import VivintConfigEntry -from .const import CONF_DISARM_CODE +from .const import CONF_DISARM_CODE, DOMAIN from .hub import VivintEntity, VivintHub ARMED_STATE_MAP = { @@ -62,6 +66,9 @@ async def async_setup_entry( if not entities: return + # Migrate unique ids + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) @@ -105,3 +112,19 @@ async def async_alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" await self.device.trigger_alarm() + + +# to be removed 2025-01 +def async_update_unique_id( + hass: HomeAssistant, domain: str, entities: Iterable[VivintAlarmControlPanelEntity] +) -> None: + """Update unique ID to be based on VIN and entity description key instead of name.""" + ent_reg = er.async_get(hass) + for entity in entities: + old_unique_id = int(entity.unique_id) + if entity_id := ent_reg.async_get_entity_id(domain, DOMAIN, old_unique_id): + if existing_entity_id := ent_reg.async_get_entity_id( + domain, DOMAIN, entity.unique_id + ): + ent_reg.async_remove(existing_entity_id) + ent_reg.async_update_entity(entity_id, new_unique_id=entity.unique_id) diff --git a/custom_components/vivint/hub.py b/custom_components/vivint/hub.py index 35aff00..433d8b3 100644 --- a/custom_components/vivint/hub.py +++ b/custom_components/vivint/hub.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Callable from datetime import timedelta import logging @@ -72,6 +73,7 @@ def __init__( self.account: Account = None self.logged_in = False self.session = ClientSession() + self._lock = asyncio.Lock() async def _async_update_data() -> None: """Update all device states from the Vivint API.""" @@ -113,14 +115,15 @@ async def login( raise ex async def disconnect(self) -> None: - """Disconnect from Vivint, close the session and optionally remove cache.""" - if self.account.connected: - await self.account.disconnect() - if not self.session.closed: - await self.session.close() - if self.__undo_listener: - self.__undo_listener() - self.__undo_listener = None + """Disconnect from Vivint, close the session and stop listener.""" + async with self._lock: + if self.account.connected: + await self.account.disconnect() + if not self.session.closed: + await self.session.close() + if self.__undo_listener: + self.__undo_listener() + self.__undo_listener = None async def verify_mfa(self, code: str) -> bool: """Verify MFA."""