Skip to content

Commit d8d0d98

Browse files
committed
Add asyncio lock to prevent parallel calls
1 parent a3c3561 commit d8d0d98

File tree

6 files changed

+66
-17
lines changed

6 files changed

+66
-17
lines changed

custom_components/lock_code_manager/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ async def _hard_refresh_usercodes(service: ServiceCall) -> None:
123123
_LOGGER.debug("Hard refresh usercodes service called: %s", service.data)
124124
locks = get_locks_from_targets(hass, service.data)
125125
results = await asyncio.gather(
126-
*(lock.async_hard_refresh_codes() for lock in locks), return_exceptions=True
126+
*(lock.async_internal_hard_refresh_codes() for lock in locks),
127+
return_exceptions=True,
127128
)
128129
errors = [err for err in results if isinstance(err, Exception)]
129130
if errors:
@@ -404,7 +405,7 @@ async def async_update_listener(hass: HomeAssistant, config_entry: ConfigEntry)
404405
# Make sure lock is up before we proceed
405406
timeout = 1
406407

407-
while not await lock.async_is_connection_up():
408+
while not await lock.async_internal_is_connection_up():
408409
_LOGGER.debug(
409410
(
410411
"%s (%s): Lock %s is not connected to Home Assistant yet, waiting %s "

custom_components/lock_code_manager/binary_sensor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ async def _async_update_state(
346346
) is not None and pin_state != self._get_entity_state(ATTR_CODE):
347347
self._attr_is_on = False
348348
self.async_write_ha_state()
349-
await self.lock.async_set_usercode(
349+
await self.lock.async_internal_set_usercode(
350350
int(self.slot_num), pin_state, self._get_entity_state(CONF_NAME)
351351
)
352352
_LOGGER.info(
@@ -364,7 +364,7 @@ async def _async_update_state(
364364
if self._get_entity_state(ATTR_CODE) != "":
365365
self._attr_is_on = False
366366
self.async_write_ha_state()
367-
await self.lock.async_clear_usercode(int(self.slot_num))
367+
await self.lock.async_internal_clear_usercode(int(self.slot_num))
368368
_LOGGER.info(
369369
"%s (%s): Cleared usercode for lock %s slot %s",
370370
self.config_entry.entry_id,

custom_components/lock_code_manager/coordinator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, hass: HomeAssistant, lock: BaseLock) -> None:
3232
async def async_get_usercodes(self) -> dict[int, int | str]:
3333
"""Update usercodes."""
3434
try:
35-
return await self._lock.async_get_usercodes()
35+
return await self._lock.async_internal_get_usercodes()
3636
except LockDisconnected as err:
3737
# We can silently fail if we've never been able to retrieve data
3838
if not self.data:

custom_components/lock_code_manager/providers/_base.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
from dataclasses import dataclass, field
67
from datetime import timedelta
78
import functools
@@ -50,6 +51,7 @@ class BaseLock:
5051
lock_config_entry: ConfigEntry | None = field(repr=False)
5152
lock: er.RegistryEntry
5253
device_entry: dr.DeviceEntry | None = field(default=None, init=False)
54+
_aio_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False)
5355

5456
@final
5557
@callback
@@ -110,6 +112,11 @@ async def async_is_connection_up(self) -> bool:
110112
"""Return whether connection to lock is up."""
111113
return await self.hass.async_add_executor_job(self.is_connection_up)
112114

115+
@final
116+
async def async_internal_is_connection_up(self) -> bool:
117+
"""Return whether connection to lock is up."""
118+
return await self.async_is_connection_up()
119+
113120
def hard_refresh_codes(self) -> None:
114121
"""
115122
Perform hard refresh all codes.
@@ -128,6 +135,17 @@ async def async_hard_refresh_codes(self) -> None:
128135
"""
129136
await self.hass.async_add_executor_job(self.hard_refresh_codes)
130137

138+
@final
139+
async def async_internal_hard_refresh_codes(self) -> None:
140+
"""
141+
Perform hard refresh of all codes.
142+
143+
Needed for integrations where usercodes are cached and may get out of sync with
144+
the lock.
145+
"""
146+
async with self._aio_lock:
147+
await self.async_hard_refresh_codes()
148+
131149
def set_usercode(
132150
self, code_slot: int, usercode: int | str, name: str | None = None
133151
) -> None:
@@ -142,6 +160,14 @@ async def async_set_usercode(
142160
functools.partial(self.set_usercode, code_slot, usercode, name=name)
143161
)
144162

163+
@final
164+
async def async_internal_set_usercode(
165+
self, code_slot: int, usercode: int | str, name: str | None = None
166+
) -> None:
167+
"""Set a usercode on a code slot."""
168+
async with self._aio_lock:
169+
await self.async_set_usercode(code_slot, usercode, name=name)
170+
145171
def clear_usercode(self, code_slot: int) -> None:
146172
"""Clear a usercode on a code slot."""
147173
raise HomeAssistantError from NotImplementedError()
@@ -150,6 +176,12 @@ async def async_clear_usercode(self, code_slot: int) -> None:
150176
"""Clear a usercode on a code slot."""
151177
await self.hass.async_add_executor_job(self.clear_usercode, code_slot)
152178

179+
@final
180+
async def async_internal_clear_usercode(self, code_slot: int) -> None:
181+
"""Clear a usercode on a code slot."""
182+
async with self._aio_lock:
183+
await self.async_clear_usercode(code_slot)
184+
153185
def get_usercodes(self) -> dict[int, int | str]:
154186
"""
155187
Get dictionary of code slots and usercodes.
@@ -178,6 +210,22 @@ async def async_get_usercodes(self) -> dict[int, int | str]:
178210
"""
179211
return await self.hass.async_add_executor_job(self.get_usercodes)
180212

213+
@final
214+
async def async_internal_get_usercodes(self) -> dict[int, int | str]:
215+
"""
216+
Get dictionary of code slots and usercodes.
217+
218+
Called by data coordinator to get data for code slot sensors.
219+
220+
Key is code slot, value is usercode, e.g.:
221+
{
222+
1: '1234',
223+
'B': '5678',
224+
}
225+
"""
226+
async with self._aio_lock:
227+
return await self.async_get_usercodes()
228+
181229
@final
182230
def call_service(
183231
self,

tests/_base/test_provider.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ async def test_base(hass: HomeAssistant):
2727
with pytest.raises(NotImplementedError):
2828
lock.domain
2929
with pytest.raises(NotImplementedError):
30-
await lock.async_is_connection_up()
30+
await lock.async_internal_is_connection_up()
3131
with pytest.raises(HomeAssistantError):
32-
await lock.async_hard_refresh_codes()
32+
await lock.async_internal_hard_refresh_codes()
3333
with pytest.raises(HomeAssistantError):
34-
await lock.async_clear_usercode(1)
34+
await lock.async_internal_clear_usercode(1)
3535
with pytest.raises(HomeAssistantError):
36-
await lock.async_set_usercode(1, 1)
36+
await lock.async_internal_set_usercode(1, 1)
3737
with pytest.raises(NotImplementedError):
38-
await lock.async_get_usercodes()
38+
await lock.async_internal_get_usercodes()

tests/virtual/test_provider.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,18 @@ async def test_door_lock(hass: HomeAssistant):
2424
assert await lock.async_setup() is None
2525
assert lock.usercode_scan_interval == timedelta(minutes=1)
2626
assert lock.domain == "virtual"
27-
assert await lock.async_is_connection_up()
27+
assert await lock.async_internal_is_connection_up()
2828
assert lock._data == {}
29-
await lock.async_hard_refresh_codes()
29+
await lock.async_internal_hard_refresh_codes()
3030
assert lock._data == {}
3131
# we should not be able to clear a usercode that does not exist
3232
with pytest.raises(HomeAssistantError):
33-
await lock.async_clear_usercode(1)
33+
await lock.async_internal_clear_usercode(1)
3434

3535
# we should be able to set a usercode and see it in the data
36-
await lock.async_set_usercode(1, 1, "test")
36+
await lock.async_internal_set_usercode(1, 1, "test")
3737
assert lock._data["1"] == {"code": 1, "name": "test"}
38-
await lock.async_get_usercodes()
38+
await lock.async_internal_get_usercodes()
3939
assert lock._data["1"] == {"code": 1, "name": "test"}
4040

4141
# if we unload without removing permanently, the data should be saved
@@ -44,9 +44,9 @@ async def test_door_lock(hass: HomeAssistant):
4444
assert lock._data["1"] == {"code": 1, "name": "test"}
4545

4646
# we can clear a valid usercode
47-
await lock.async_set_usercode(2, 2, "test2")
47+
await lock.async_internal_set_usercode(2, 2, "test2")
4848
assert lock._data["2"] == {"code": 2, "name": "test2"}
49-
await lock.async_clear_usercode(2)
49+
await lock.async_internal_clear_usercode(2)
5050
assert "2" not in lock._data
5151

5252
# if we unload with removing permanently, the data should be removed

0 commit comments

Comments
 (0)