Move SimpliSafe REST API to DataUpdateCoordinator (#41919)
* Mirgrate SimpliSafe REST API to DataUpdateCoordinator * Docstring * More work * Good to go * Linting * Restore previous initial event check * Linting * Comment * Simplify listener * Code review * Cleanuppull/41995/head^2
parent
183f94364a
commit
8b6336a91a
|
@ -35,15 +35,17 @@ from homeassistant.helpers.dispatcher import (
|
|||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.service import (
|
||||
async_register_admin_service,
|
||||
verify_domain_control,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ATTR_ALARM_DURATION,
|
||||
ATTR_ALARM_VOLUME,
|
||||
ATTR_CHIME_VOLUME,
|
||||
|
@ -56,11 +58,11 @@ from .const import (
|
|||
DATA_CLIENT,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
VOLUMES,
|
||||
)
|
||||
|
||||
DATA_LISTENER = "listener"
|
||||
TOPIC_UPDATE_REST_API = "simplisafe_update_rest_api_{0}"
|
||||
TOPIC_UPDATE_WEBSOCKET = "simplisafe_update_websocket_{0}"
|
||||
|
||||
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
|
||||
|
@ -147,9 +149,11 @@ def _async_save_refresh_token(hass, config_entry, token):
|
|||
|
||||
|
||||
async def async_get_client_id(hass):
|
||||
"""Get a client ID (based on the HASS unique ID) for the SimpliSafe API."""
|
||||
"""Get a client ID (based on the HASS unique ID) for the SimpliSafe API.
|
||||
|
||||
Note that SimpliSafe requires full, "dashed" versions of UUIDs.
|
||||
"""
|
||||
hass_id = await hass.helpers.instance_id.async_get()
|
||||
# SimpliSafe requires full, "dashed" versions of UUIDs:
|
||||
return str(UUID(hass_id))
|
||||
|
||||
|
||||
|
@ -173,8 +177,6 @@ async def async_setup(hass, config):
|
|||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up SimpliSafe as config entry."""
|
||||
hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = []
|
||||
|
||||
entry_updates = {}
|
||||
if not config_entry.unique_id:
|
||||
# If the config entry doesn't already have a unique ID, set one:
|
||||
|
@ -201,17 +203,18 @@ async def async_setup_entry(hass, config_entry):
|
|||
config_entry.data[CONF_TOKEN], client_id=client_id, session=websession
|
||||
)
|
||||
except InvalidCredentialsError:
|
||||
_LOGGER.error("Invalid credentials provided")
|
||||
LOGGER.error("Invalid credentials provided")
|
||||
return False
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Config entry failed: %s", err)
|
||||
LOGGER.error("Config entry failed: %s", err)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
_async_save_refresh_token(hass, config_entry, api.refresh_token)
|
||||
|
||||
simplisafe = SimpliSafe(hass, api, config_entry)
|
||||
simplisafe = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = SimpliSafe(
|
||||
hass, api, config_entry
|
||||
)
|
||||
await simplisafe.async_init()
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe
|
||||
|
||||
for platform in SUPPORTED_PLATFORMS:
|
||||
hass.async_create_task(
|
||||
|
@ -226,7 +229,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
"""Decorate."""
|
||||
system_id = int(call.data[ATTR_SYSTEM_ID])
|
||||
if system_id not in simplisafe.systems:
|
||||
_LOGGER.error("Unknown system ID in service call: %s", system_id)
|
||||
LOGGER.error("Unknown system ID in service call: %s", system_id)
|
||||
return
|
||||
await coro(call)
|
||||
|
||||
|
@ -240,7 +243,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
"""Decorate."""
|
||||
system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])]
|
||||
if system.version != 3:
|
||||
_LOGGER.error("Service only available on V3 systems")
|
||||
LOGGER.error("Service only available on V3 systems")
|
||||
return
|
||||
await coro(call)
|
||||
|
||||
|
@ -254,7 +257,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
try:
|
||||
await system.clear_notifications()
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Error during service call: %s", err)
|
||||
LOGGER.error("Error during service call: %s", err)
|
||||
return
|
||||
|
||||
@verify_system_exists
|
||||
|
@ -265,7 +268,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
try:
|
||||
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE])
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Error during service call: %s", err)
|
||||
LOGGER.error("Error during service call: %s", err)
|
||||
return
|
||||
|
||||
@verify_system_exists
|
||||
|
@ -276,7 +279,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
try:
|
||||
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE])
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Error during service call: %s", err)
|
||||
LOGGER.error("Error during service call: %s", err)
|
||||
return
|
||||
|
||||
@verify_system_exists
|
||||
|
@ -294,7 +297,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
}
|
||||
)
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Error during service call: %s", err)
|
||||
LOGGER.error("Error during service call: %s", err)
|
||||
return
|
||||
|
||||
for service, method, schema in [
|
||||
|
@ -326,9 +329,8 @@ async def async_unload_entry(hass, entry):
|
|||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id)
|
||||
for remove_listener in hass.data[DOMAIN][DATA_LISTENER][entry.entry_id]:
|
||||
remove_listener()
|
||||
hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id)
|
||||
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id)
|
||||
remove_listener()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
@ -349,16 +351,16 @@ class SimpliSafeWebsocket:
|
|||
@staticmethod
|
||||
def _on_connect():
|
||||
"""Define a handler to fire when the websocket is connected."""
|
||||
_LOGGER.info("Connected to websocket")
|
||||
LOGGER.info("Connected to websocket")
|
||||
|
||||
@staticmethod
|
||||
def _on_disconnect():
|
||||
"""Define a handler to fire when the websocket is disconnected."""
|
||||
_LOGGER.info("Disconnected from websocket")
|
||||
LOGGER.info("Disconnected from websocket")
|
||||
|
||||
def _on_event(self, event):
|
||||
"""Define a handler to fire when a new SimpliSafe event arrives."""
|
||||
_LOGGER.debug("New websocket event: %s", event)
|
||||
LOGGER.debug("New websocket event: %s", event)
|
||||
async_dispatcher_send(
|
||||
self._hass, TOPIC_UPDATE_WEBSOCKET.format(event.system_id), event
|
||||
)
|
||||
|
@ -408,6 +410,7 @@ class SimpliSafe:
|
|||
self._hass = hass
|
||||
self._system_notifications = {}
|
||||
self.config_entry = config_entry
|
||||
self.coordinator = None
|
||||
self.initial_event_to_use = {}
|
||||
self.systems = {}
|
||||
self.websocket = SimpliSafeWebsocket(hass, api.websocket)
|
||||
|
@ -430,7 +433,7 @@ class SimpliSafe:
|
|||
if not to_add:
|
||||
return
|
||||
|
||||
_LOGGER.debug("New system notifications: %s", to_add)
|
||||
LOGGER.debug("New system notifications: %s", to_add)
|
||||
|
||||
self._system_notifications[system.system_id].update(to_add)
|
||||
|
||||
|
@ -457,10 +460,10 @@ class SimpliSafe:
|
|||
"""Define an event handler to disconnect from the websocket."""
|
||||
await self.websocket.async_disconnect()
|
||||
|
||||
self._hass.data[DOMAIN][DATA_LISTENER][self.config_entry.entry_id].append(
|
||||
self._hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_websocket_disconnect
|
||||
)
|
||||
self._hass.data[DOMAIN][DATA_LISTENER][
|
||||
self.config_entry.entry_id
|
||||
] = self._hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_websocket_disconnect
|
||||
)
|
||||
|
||||
self.systems = await self._api.get_systems()
|
||||
|
@ -481,34 +484,28 @@ class SimpliSafe:
|
|||
system.system_id
|
||||
] = await system.get_latest_event()
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Error while fetching initial event: %s", err)
|
||||
LOGGER.error("Error while fetching initial event: %s", err)
|
||||
self.initial_event_to_use[system.system_id] = {}
|
||||
|
||||
async def refresh(event_time):
|
||||
"""Refresh data from the SimpliSafe account."""
|
||||
await self.async_update()
|
||||
|
||||
self._hass.data[DOMAIN][DATA_LISTENER][self.config_entry.entry_id].append(
|
||||
async_track_time_interval(self._hass, refresh, DEFAULT_SCAN_INTERVAL)
|
||||
self.coordinator = DataUpdateCoordinator(
|
||||
self._hass,
|
||||
LOGGER,
|
||||
name=self.config_entry.data[CONF_USERNAME],
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
update_method=self.async_update,
|
||||
)
|
||||
|
||||
await self.async_update()
|
||||
|
||||
async def async_update(self):
|
||||
"""Get updated data from SimpliSafe."""
|
||||
|
||||
async def update_system(system):
|
||||
async def async_update_system(system):
|
||||
"""Update a system."""
|
||||
await system.update(cached=False)
|
||||
self._async_process_new_notifications(system)
|
||||
_LOGGER.debug('Updated REST API data for "%s"', system.address)
|
||||
async_dispatcher_send(
|
||||
self._hass, TOPIC_UPDATE_REST_API.format(system.system_id)
|
||||
)
|
||||
|
||||
tasks = [update_system(system) for system in self.systems.values()]
|
||||
|
||||
tasks = [async_update_system(system) for system in self.systems.values()]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
for result in results:
|
||||
if isinstance(result, InvalidCredentialsError):
|
||||
if self._emergency_refresh_token_used:
|
||||
|
@ -532,9 +529,10 @@ class SimpliSafe:
|
|||
)
|
||||
)
|
||||
|
||||
return
|
||||
LOGGER.error("Update failed with stored refresh token")
|
||||
raise UpdateFailed from result
|
||||
|
||||
_LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
|
||||
LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
|
||||
self._emergency_refresh_token_used = True
|
||||
|
||||
try:
|
||||
|
@ -543,23 +541,23 @@ class SimpliSafe:
|
|||
)
|
||||
return
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Error while using stored refresh token: %s", err)
|
||||
return
|
||||
LOGGER.error("Error while using stored refresh token: %s", err)
|
||||
raise UpdateFailed from err
|
||||
|
||||
if isinstance(result, EndpointUnavailable):
|
||||
# In case the user attempt an action not allowed in their current plan,
|
||||
# we merely log that message at INFO level (so the user is aware,
|
||||
# but not spammed with ERROR messages that they cannot change):
|
||||
_LOGGER.info(result)
|
||||
return
|
||||
LOGGER.info(result)
|
||||
raise UpdateFailed from result
|
||||
|
||||
if isinstance(result, SimplipyError):
|
||||
_LOGGER.error("SimpliSafe error while updating: %s", result)
|
||||
return
|
||||
LOGGER.error("SimpliSafe error while updating: %s", result)
|
||||
raise UpdateFailed from result
|
||||
|
||||
if isinstance(result, Exception):
|
||||
_LOGGER.error("Unknown error while updating: %s", result)
|
||||
return
|
||||
LOGGER.error("Unknown error while updating: %s", result)
|
||||
raise UpdateFailed from result
|
||||
|
||||
if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]:
|
||||
_async_save_refresh_token(
|
||||
|
@ -572,11 +570,12 @@ class SimpliSafe:
|
|||
self._emergency_refresh_token_used = False
|
||||
|
||||
|
||||
class SimpliSafeEntity(Entity):
|
||||
class SimpliSafeEntity(CoordinatorEntity):
|
||||
"""Define a base SimpliSafe entity."""
|
||||
|
||||
def __init__(self, simplisafe, system, name, *, serial=None):
|
||||
"""Initialize."""
|
||||
super().__init__(simplisafe.coordinator)
|
||||
self._name = name
|
||||
self._online = True
|
||||
self._simplisafe = simplisafe
|
||||
|
@ -619,11 +618,15 @@ class SimpliSafeEntity(Entity):
|
|||
def available(self):
|
||||
"""Return whether the entity is available."""
|
||||
# We can easily detect if the V3 system is offline, but no simple check exists
|
||||
# for the V2 system. Therefore, we mark the entity as available if:
|
||||
# for the V2 system. Therefore, assuming the coordinator hasn't failed, we mark
|
||||
# the entity as available if:
|
||||
# 1. We can verify that the system is online (assuming True if we can't)
|
||||
# 2. We can verify that the entity is online
|
||||
system_offline = self._system.version == 3 and self._system.offline
|
||||
return not system_offline and self._online
|
||||
return (
|
||||
self.coordinator.last_update_success
|
||||
and not (self._system.version == 3 and self._system.offline)
|
||||
and self._online
|
||||
)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
|
@ -675,50 +678,43 @@ class SimpliSafeEntity(Entity):
|
|||
|
||||
self.async_update_from_websocket_event(event)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
"""Update the entity with new REST API data."""
|
||||
self.async_update_from_rest_api()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _handle_websocket_update(self, event):
|
||||
"""Update the entity with new websocket data."""
|
||||
# Ignore this event if it belongs to a system other than this one:
|
||||
if event.system_id != self._system.system_id:
|
||||
return
|
||||
|
||||
# Ignore this event if this entity hasn't expressed interest in its type:
|
||||
if event.event_type not in self.websocket_events_to_listen_for:
|
||||
return
|
||||
|
||||
# Ignore this event if it belongs to a entity with a different serial
|
||||
# number from this one's:
|
||||
if (
|
||||
event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL
|
||||
and event.sensor_serial != self._serial
|
||||
):
|
||||
return
|
||||
|
||||
self._async_internal_update_from_websocket_event(event)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def rest_api_update():
|
||||
"""Update the entity with new REST API data."""
|
||||
self.async_update_from_rest_api()
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
TOPIC_UPDATE_REST_API.format(self._system.system_id),
|
||||
rest_api_update,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def websocket_update(event):
|
||||
"""Update the entity with new websocket data."""
|
||||
# Ignore this event if it belongs to a system other than this one:
|
||||
if event.system_id != self._system.system_id:
|
||||
return
|
||||
|
||||
# Ignore this event if this entity hasn't expressed interest in its type:
|
||||
if event.event_type not in self.websocket_events_to_listen_for:
|
||||
return
|
||||
|
||||
# Ignore this event if it belongs to a entity with a different serial
|
||||
# number from this one's:
|
||||
if (
|
||||
event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL
|
||||
and event.sensor_serial != self._serial
|
||||
):
|
||||
return
|
||||
|
||||
self._async_internal_update_from_websocket_event(event)
|
||||
self.async_write_ha_state()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
TOPIC_UPDATE_WEBSOCKET.format(self._system.system_id),
|
||||
websocket_update,
|
||||
self._handle_websocket_update,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ from homeassistant.core import callback
|
|||
|
||||
from . import SimpliSafeEntity
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ATTR_ALARM_DURATION,
|
||||
ATTR_ALARM_VOLUME,
|
||||
ATTR_CHIME_VOLUME,
|
||||
|
@ -50,6 +49,7 @@ from .const import (
|
|||
ATTR_VOICE_PROMPT_VOLUME,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
VOLUME_STRING_MAP,
|
||||
)
|
||||
|
||||
|
@ -144,7 +144,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
|
|||
return True
|
||||
|
||||
if not code or code != self._simplisafe.config_entry.options[CONF_CODE]:
|
||||
_LOGGER.warning(
|
||||
LOGGER.warning(
|
||||
"Incorrect alarm code entered (target state: %s): %s", state, code
|
||||
)
|
||||
return False
|
||||
|
@ -159,7 +159,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
|
|||
try:
|
||||
await self._system.set_off()
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error('Error while disarming "%s": %s', self._system.name, err)
|
||||
LOGGER.error('Error while disarming "%s": %s', self._system.name, err)
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
|
@ -172,7 +172,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
|
|||
try:
|
||||
await self._system.set_home()
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err)
|
||||
LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err)
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
|
@ -185,7 +185,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
|
|||
try:
|
||||
await self._system.set_away()
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err)
|
||||
LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err)
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMING
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from . import async_get_client_id
|
||||
from .const import _LOGGER, DOMAIN # pylint: disable=unused-import
|
||||
from .const import DOMAIN, LOGGER # pylint: disable=unused-import
|
||||
|
||||
|
||||
class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
@ -62,12 +62,12 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
try:
|
||||
simplisafe = await self._async_get_simplisafe_api()
|
||||
except PendingAuthorizationError:
|
||||
_LOGGER.info("Awaiting confirmation of MFA email click")
|
||||
LOGGER.info("Awaiting confirmation of MFA email click")
|
||||
return await self.async_step_mfa()
|
||||
except InvalidCredentialsError:
|
||||
errors = {"base": "invalid_credentials"}
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("Unknown error while logging into SimpliSafe: %s", err)
|
||||
LOGGER.error("Unknown error while logging into SimpliSafe: %s", err)
|
||||
errors = {"base": "unknown"}
|
||||
|
||||
if errors:
|
||||
|
@ -101,7 +101,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
try:
|
||||
simplisafe = await self._async_get_simplisafe_api()
|
||||
except PendingAuthorizationError:
|
||||
_LOGGER.error("Still awaiting confirmation of MFA email click")
|
||||
LOGGER.error("Still awaiting confirmation of MFA email click")
|
||||
return self.async_show_form(
|
||||
step_id="mfa", errors={"base": "still_awaiting_mfa"}
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ import logging
|
|||
|
||||
from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "simplisafe"
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from homeassistant.components.lock import LockEntity
|
|||
from homeassistant.core import callback
|
||||
|
||||
from . import SimpliSafeEntity
|
||||
from .const import _LOGGER, DATA_CLIENT, DOMAIN
|
||||
from .const import DATA_CLIENT, DOMAIN, LOGGER
|
||||
|
||||
ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
|
||||
ATTR_JAMMED = "jammed"
|
||||
|
@ -48,7 +48,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity):
|
|||
try:
|
||||
await self._lock.lock()
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error('Error while locking "%s": %s', self._lock.name, err)
|
||||
LOGGER.error('Error while locking "%s": %s', self._lock.name, err)
|
||||
return
|
||||
|
||||
self._is_locked = True
|
||||
|
@ -58,7 +58,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity):
|
|||
try:
|
||||
await self._lock.unlock()
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err)
|
||||
LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err)
|
||||
return
|
||||
|
||||
self._is_locked = False
|
||||
|
|
Loading…
Reference in New Issue