[#340] Ironed out some bugs in the `alarm` integration.

- The alarm ID should be randomly generated - auto-increment IDs are
  subject to race conditions when alarms are created in separate
  processes.

- Clean up alarms that are not static and have been removed from the db.

- Better alarm shut down detection logic.
This commit is contained in:
Fabio Manganiello 2023-12-10 15:30:19 +01:00
parent ca57d3d7b3
commit 42574d054a
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
2 changed files with 24 additions and 14 deletions

View File

@ -167,6 +167,13 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
media_plugin=alarm.media_plugin or self.media_plugin, media_plugin=alarm.media_plugin or self.media_plugin,
) )
# Stop and remove alarms that are not statically configured no longer
# present in the db
for name, alarm in self.alarms.copy().items():
if not alarm.static and name not in alarms:
del self.alarms[name]
alarm.stop()
def _sync_alarms(self): def _sync_alarms(self):
with self._get_session() as session: with self._get_session() as session:
db_alarms = { db_alarms = {
@ -184,15 +191,16 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
self._synced = True self._synced = True
def _clear_alarm(self, alarm: DbAlarm, session: Session): def _clear_alarm(self, alarm: DbAlarm, session: Session):
self.alarms.pop(str(alarm.name), None) alarm_obj = self.alarms.pop(str(alarm.name), None)
if alarm_obj:
alarm_obj.stop()
session.delete(alarm) session.delete(alarm)
self._bus.post(EntityDeleteEvent(entity=alarm)) self._bus.post(EntityDeleteEvent(entity=alarm))
def _clear_expired_alarms(self, session: Session): def _clear_expired_alarms(self, session: Session):
expired_alarms = [ expired_alarms = [
alarm alarm for alarm in self.alarms.values() if alarm.should_stop()
for alarm in self.alarms.values()
if alarm.is_expired() and alarm.is_shut_down()
] ]
if not expired_alarms: if not expired_alarms:
@ -220,7 +228,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
iter( iter(
alarm alarm
for alarm in self.alarms.values() for alarm in self.alarms.values()
if alarm.state == AlarmState.RUNNING if alarm.state in {AlarmState.RUNNING, AlarmState.SNOOZED}
), ),
None, None,
) )
@ -233,6 +241,9 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
def _on_alarm_update(self, alarm: Alarm): def _on_alarm_update(self, alarm: Alarm):
with self._db_lock: with self._db_lock:
if alarm.should_stop():
return
self.publish_entities([alarm]) self.publish_entities([alarm])
def _add( def _add(

View File

@ -3,6 +3,7 @@ import enum
import os import os
import time import time
import threading import threading
from random import randint
from typing import Callable, Optional, Union from typing import Callable, Optional, Union
import croniter import croniter
@ -40,9 +41,6 @@ class Alarm:
Alarm model and controller. Alarm model and controller.
""" """
_alarms_count = 0
_id_lock = threading.RLock()
def __init__( def __init__(
self, self,
when: Union[str, int, float], when: Union[str, int, float],
@ -59,10 +57,7 @@ class Alarm:
on_change: Optional[Callable[['Alarm'], None]] = None, on_change: Optional[Callable[['Alarm'], None]] = None,
**_, **_,
): ):
with self._id_lock: self.id = randint(0, 65535)
self._alarms_count += 1
self.id = self._alarms_count
self.when = when self.when = when
self.name = name or f'Alarm_{self.id}' self.name = name or f'Alarm_{self.id}'
self.media = self._get_media_resource(media) self.media = self._get_media_resource(media)
@ -240,10 +235,10 @@ class Alarm:
sleep_time = self._runtime_snooze_interval sleep_time = self._runtime_snooze_interval
else: else:
self.state = AlarmState.WAITING self.state = AlarmState.WAITING
self._on_change()
break break
self._on_change()
self.wait_stop(self.poll_interval) self.wait_stop(self.poll_interval)
if self.state == AlarmState.SNOOZED: if self.state == AlarmState.SNOOZED:
@ -266,7 +261,11 @@ class Alarm:
self.stop_event.wait(timeout) self.stop_event.wait(timeout)
def should_stop(self): def should_stop(self):
return self.stop_event.is_set() or (self.is_expired() and self.is_shut_down()) return (
self.stop_event.is_set()
or (self.is_expired() and self.state == AlarmState.DISMISSED)
or self.state == AlarmState.SHUTDOWN
)
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {