[alarm] Added `dismiss_interval` configuration.

This commit is contained in:
Fabio Manganiello 2023-12-18 03:01:27 +01:00
parent 250858fe99
commit 52fd64a162
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
5 changed files with 110 additions and 20 deletions

View File

@ -138,8 +138,8 @@
<br /> <br />
<span class="subtext"> <span class="subtext">
<span class="text"> <span class="text">
How long the interval should be paused after being triggered and How long the alarm should be paused after being triggered and
snoozed. manually snoozed.
</span> </span>
</span> </span>
</div> </div>
@ -150,6 +150,26 @@
</div> </div>
</div> </div>
<div class="row item">
<div class="name">
<label>
<i class="icon fas fa-xmark" />
Dismiss timeout
</label>
<br />
<span class="subtext">
<span class="text">
How long the alarm should run before being automatically dismissed.
</span>
</span>
</div>
<div class="value">
<TimeInterval :value="editForm.dismiss_interval"
@input="editForm.dismiss_interval = $event" />
</div>
</div>
<div class="row item"> <div class="row item">
<div class="name"> <div class="name">
<label> <label>
@ -247,6 +267,7 @@ export default {
'media_plugin', 'media_plugin',
'name', 'name',
'snooze_interval', 'snooze_interval',
'dismiss_interval',
'when', 'when',
].forEach(key => { ].forEach(key => {
if (this.editForm[key] !== this.value[key]) if (this.editForm[key] !== this.value[key])
@ -258,6 +279,17 @@ export default {
}, },
methods: { methods: {
actionsToArgs(actions) {
return actions?.map(action => {
if (action.name) {
action.action = action.name
delete action.name
}
return action
}) ?? []
},
onWhenInput(value, type) { onWhenInput(value, type) {
if (value == null) if (value == null)
return return
@ -302,7 +334,8 @@ export default {
media_plugin: this.editForm.media_plugin, media_plugin: this.editForm.media_plugin,
audio_volume: this.editForm.audio_volume, audio_volume: this.editForm.audio_volume,
snooze_interval: this.editForm.snooze_interval, snooze_interval: this.editForm.snooze_interval,
actions: this.editForm.actions, dismiss_interval: this.editForm.dismiss_interval,
actions: this.actionsToArgs(this.editForm.actions),
} }
} else { } else {
action = 'alarm.edit' action = 'alarm.edit'
@ -311,6 +344,9 @@ export default {
...this.changes, ...this.changes,
} }
if (this.changes.actions)
args.actions = this.actionsToArgs(this.changes.actions)
if (this.changes.name != null) { if (this.changes.name != null) {
args.name = this.value.name args.name = this.value.name
args.new_name = this.changes.name args.new_name = this.changes.name

View File

@ -26,6 +26,7 @@ if not is_defined('alarm'):
media_plugin = Column(String, nullable=True) media_plugin = Column(String, nullable=True)
audio_volume = Column(Integer, nullable=True) audio_volume = Column(Integer, nullable=True)
snooze_interval = Column(Integer, nullable=True) snooze_interval = Column(Integer, nullable=True)
dismiss_interval = Column(Integer, nullable=True)
actions = Column(JSON, nullable=True) actions = Column(JSON, nullable=True)
static = Column(Boolean, nullable=False, default=False) static = Column(Boolean, nullable=False, default=False)
condition_type = Column(String, nullable=False) condition_type = Column(String, nullable=False)

View File

@ -77,14 +77,15 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
def __init__( def __init__(
self, self,
alarms: Optional[Union[list, Dict[str, Any]]] = None, alarms: Optional[Union[List[dict], Dict[str, dict]]] = None,
media_plugin: Optional[str] = None, media_plugin: Optional[str] = None,
poll_interval: Optional[float] = 5.0, poll_interval: Optional[float] = 2.0,
snooze_interval: float = 300.0, snooze_interval: float = 300.0,
dismiss_interval: float = 300.0,
**kwargs, **kwargs,
): ):
""" """
:param alarms: List or name->value dict with the configured alarms. Example: :param alarms: List or name->value dict with the configured alarms.
:param media_plugin: Media plugin (instance of :param media_plugin: Media plugin (instance of
:class:`platypush.plugins.media.MediaPlugin`) that will be used to :class:`platypush.plugins.media.MediaPlugin`) that will be used to
play the alarm audio. It needs to be a supported local media play the alarm audio. It needs to be a supported local media
@ -92,11 +93,18 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
``media.gstreamer``, ``sound``, etc. If not specified, the first ``media.gstreamer``, ``sound``, etc. If not specified, the first
available configured local media plugin will be used. This only available configured local media plugin will be used. This only
applies to alarms that are configured to play an audio resource. applies to alarms that are configured to play an audio resource.
:param poll_interval: Poll interval in seconds (default: 5). :param poll_interval: (Internal) poll interval, in seconds (default: 2).
:param snooze_interval: Default snooze interval in seconds (default: 300). :param snooze_interval: Default snooze interval in seconds. This
specifies how long to wait between alarm runs when an alarm is
dismissed (default: 300).
:param dismiss_interval: Default dismiss interval in seconds. This
specifies how long an alarm should run without being manually
snoozed/dismissed before being automatically dismissed (default:
300).
""" """
super().__init__(poll_interval=poll_interval, **kwargs) super().__init__(poll_interval=poll_interval, **kwargs)
self.snooze_interval = snooze_interval self.snooze_interval = snooze_interval
self.dismiss_interval = dismiss_interval
self._db_lock = RLock() self._db_lock = RLock()
alarms = alarms or [] alarms = alarms or []
if isinstance(alarms, dict): if isinstance(alarms, dict):
@ -259,6 +267,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
audio_volume: Optional[Union[int, float]] = None, audio_volume: Optional[Union[int, float]] = None,
enabled: bool = True, enabled: bool = True,
snooze_interval: Optional[float] = None, snooze_interval: Optional[float] = None,
dismiss_interval: Optional[float] = None,
) -> Alarm: ) -> Alarm:
alarm = Alarm( alarm = Alarm(
when=when, when=when,
@ -269,6 +278,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
media_plugin=media_plugin or self.media_plugin, media_plugin=media_plugin or self.media_plugin,
audio_volume=audio_volume, audio_volume=audio_volume,
snooze_interval=snooze_interval or self.snooze_interval, snooze_interval=snooze_interval or self.snooze_interval,
dismiss_interval=dismiss_interval or self.dismiss_interval,
stop_event=self._should_stop, stop_event=self._should_stop,
on_change=self._on_alarm_update, on_change=self._on_alarm_update,
) )
@ -316,6 +326,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
audio_volume: Optional[Union[int, float]] = None, audio_volume: Optional[Union[int, float]] = None,
enabled: bool = True, enabled: bool = True,
snooze_interval: Optional[float] = None, snooze_interval: Optional[float] = None,
dismiss_interval: Optional[float] = None,
) -> dict: ) -> dict:
""" """
Add a new alarm. Add a new alarm.
@ -332,6 +343,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
:param audio_volume: Volume of the audio. :param audio_volume: Volume of the audio.
:param enabled: Whether the new alarm should be enabled (default: True). :param enabled: Whether the new alarm should be enabled (default: True).
:param snooze_interval: Snooze seconds before playing the alarm again. :param snooze_interval: Snooze seconds before playing the alarm again.
:param dismiss_interval: Dismiss seconds before stopping the alarm.
:return: The newly created alarm. :return: The newly created alarm.
""" """
if audio_file: if audio_file:
@ -349,6 +361,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
enabled=enabled, enabled=enabled,
audio_volume=audio_volume, audio_volume=audio_volume,
snooze_interval=snooze_interval, snooze_interval=snooze_interval,
dismiss_interval=dismiss_interval,
).to_dict() ).to_dict()
@action @action
@ -363,6 +376,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
audio_volume: Optional[Union[int, float]] = None, audio_volume: Optional[Union[int, float]] = None,
enabled: Optional[bool] = None, enabled: Optional[bool] = None,
snooze_interval: Optional[float] = None, snooze_interval: Optional[float] = None,
dismiss_interval: Optional[float] = None,
) -> dict: ) -> dict:
""" """
Edit an existing alarm. Edit an existing alarm.
@ -383,6 +397,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
:param audio_volume: Volume of the audio. :param audio_volume: Volume of the audio.
:param enabled: Whether the new alarm should be enabled. :param enabled: Whether the new alarm should be enabled.
:param snooze_interval: Snooze seconds before playing the alarm again. :param snooze_interval: Snooze seconds before playing the alarm again.
:param dismiss_interval: Dismiss seconds before stopping the alarm.
:return: The modified alarm. :return: The modified alarm.
""" """
alarm = self._get_alarm(name) alarm = self._get_alarm(name)
@ -410,6 +425,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
if audio_volume is not None if audio_volume is not None
else alarm.audio_volume, else alarm.audio_volume,
snooze_interval=snooze_interval or alarm.snooze_interval, snooze_interval=snooze_interval or alarm.snooze_interval,
dismiss_interval=dismiss_interval or alarm.dismiss_interval,
).to_dict() ).to_dict()
@action @action
@ -419,15 +435,25 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
:param name: Alarm name. :param name: Alarm name.
""" """
alarm = self._get_alarm(name) try:
alarm = self._get_alarm(name)
except AssertionError:
self.logger.warning('Alarm %s does not exist', name)
return
assert not alarm.static, ( assert not alarm.static, (
f'Alarm {name} is statically defined in the configuration, ' f'Alarm {name} is statically defined in the configuration, '
'cannot overwrite it programmatically' 'cannot overwrite it programmatically'
) )
alarm.stop()
with self._db.get_session() as session: with self._db.get_session() as session:
db_alarm = session.query(DbAlarm).filter_by(name=name).first() db_alarm = session.query(DbAlarm).filter_by(name=name).first()
assert db_alarm, f'Alarm {name} does not exist' if not db_alarm:
self.logger.warning('Alarm %s does not exist', name)
return
self._clear_alarm(db_alarm, session) self._clear_alarm(db_alarm, session)
@action @action
@ -507,6 +533,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
"media_plugin": "media.vlc", "media_plugin": "media.vlc",
"audio_volume": 10, "audio_volume": 10,
"snooze_interval": 300, "snooze_interval": 300,
"dismiss_interval": 300,
"actions": [ "actions": [
{ {
"action": "tts.say", "action": "tts.say",

View File

@ -61,7 +61,8 @@ class Alarm:
media_plugin: Optional[str] = None, media_plugin: Optional[str] = None,
audio_volume: Optional[Union[int, float]] = None, audio_volume: Optional[Union[int, float]] = None,
snooze_interval: float = 300, snooze_interval: float = 300,
poll_interval: float = 5, dismiss_interval: float = 300,
poll_interval: float = 2,
enabled: bool = True, enabled: bool = True,
static: bool = False, static: bool = False,
stop_event: Optional[threading.Event] = None, stop_event: Optional[threading.Event] = None,
@ -75,6 +76,7 @@ class Alarm:
self.media_plugin = media_plugin self.media_plugin = media_plugin
self.audio_volume = audio_volume self.audio_volume = audio_volume
self.snooze_interval = snooze_interval self.snooze_interval = snooze_interval
self.dismiss_interval = dismiss_interval
self.state = AlarmState.UNKNOWN self.state = AlarmState.UNKNOWN
self.timer: Optional[threading.Timer] = None self.timer: Optional[threading.Timer] = None
self.static = static self.static = static
@ -91,6 +93,7 @@ class Alarm:
self.stop_event = stop_event or threading.Event() self.stop_event = stop_event or threading.Event()
self.poll_interval = poll_interval self.poll_interval = poll_interval
self.on_change = on_change self.on_change = on_change
self._dismiss_timer: Optional[threading.Timer] = None
def _on_change(self): def _on_change(self):
if self.on_change: if self.on_change:
@ -209,6 +212,7 @@ class Alarm:
def dismiss(self): def dismiss(self):
self.state = AlarmState.DISMISSED self.state = AlarmState.DISMISSED
self.stop_audio() self.stop_audio()
self._clear_dismiss_timer()
get_bus().post(AlarmDismissedEvent(name=self.name)) get_bus().post(AlarmDismissedEvent(name=self.name))
self._on_change() self._on_change()
@ -216,6 +220,7 @@ class Alarm:
self._runtime_snooze_interval = interval or self.snooze_interval self._runtime_snooze_interval = interval or self.snooze_interval
self.state = AlarmState.SNOOZED self.state = AlarmState.SNOOZED
self.stop_audio() self.stop_audio()
self._clear_dismiss_timer()
get_bus().post( get_bus().post(
AlarmSnoozedEvent(name=self.name, interval=self._runtime_snooze_interval) AlarmSnoozedEvent(name=self.name, interval=self._runtime_snooze_interval)
) )
@ -230,19 +235,27 @@ class Alarm:
return return
interval = next_run - time.time() interval = next_run - time.time()
self.state = AlarmState.WAITING
self.timer = threading.Timer(interval, self.alarm_callback) self.timer = threading.Timer(interval, self.alarm_callback)
self.timer.start() self.timer.start()
self.state = AlarmState.WAITING self._clear_dismiss_timer()
self._on_change() self._on_change()
def stop(self): def stop(self):
self.state = AlarmState.SHUTDOWN self.state = AlarmState.SHUTDOWN
self.stop_audio()
if self.timer: if self.timer:
self.timer.cancel() self.timer.cancel()
self.timer = None self.timer = None
self._on_change() self._on_change()
def _clear_dismiss_timer(self):
if self._dismiss_timer:
self._dismiss_timer.cancel()
self._dismiss_timer = None
def _get_media_plugin(self) -> MediaPlugin: def _get_media_plugin(self) -> MediaPlugin:
plugin = get_plugin(self.media_plugin) plugin = get_plugin(self.media_plugin)
assert plugin and isinstance(plugin, MediaPlugin), ( assert plugin and isinstance(plugin, MediaPlugin), (
@ -265,16 +278,23 @@ class Alarm:
def stop_audio(self): def stop_audio(self):
self._get_media_plugin().stop() self._get_media_plugin().stop()
def _on_start(self):
if self.state != AlarmState.RUNNING:
self._dismiss_timer = threading.Timer(self.dismiss_interval, self.dismiss)
self._dismiss_timer.start()
self.state = AlarmState.RUNNING
get_bus().post(AlarmStartedEvent(name=self.name))
self._on_change()
if self.media_plugin and self.media:
self.play_audio()
self.actions.execute()
def alarm_callback(self): def alarm_callback(self):
while not self.should_stop(): while not self.should_stop():
if self.is_enabled(): if self.is_enabled():
self.state = AlarmState.RUNNING self._on_start()
get_bus().post(AlarmStartedEvent(name=self.name))
self._on_change()
if self.media_plugin and self.media:
self.play_audio()
self.actions.execute()
elif self.state != AlarmState.WAITING: elif self.state != AlarmState.WAITING:
self.state = AlarmState.WAITING self.state = AlarmState.WAITING
self._on_change() self._on_change()
@ -339,6 +359,7 @@ class Alarm:
'media_plugin': self.media_plugin, 'media_plugin': self.media_plugin,
'audio_volume': self.audio_volume, 'audio_volume': self.audio_volume,
'snooze_interval': self.snooze_interval, 'snooze_interval': self.snooze_interval,
'dismiss_interval': self.dismiss_interval,
'actions': self.actions.requests, 'actions': self.actions.requests,
'static': self.static, 'static': self.static,
'condition_type': self.condition_type.value, 'condition_type': self.condition_type.value,
@ -354,6 +375,7 @@ class Alarm:
audio_volume=alarm.audio_volume, # type: ignore audio_volume=alarm.audio_volume, # type: ignore
actions=alarm.actions, # type: ignore actions=alarm.actions, # type: ignore
snooze_interval=alarm.snooze_interval, # type: ignore snooze_interval=alarm.snooze_interval, # type: ignore
dismiss_interval=alarm.dismiss_interval, # type: ignore
enabled=bool(alarm.enabled), enabled=bool(alarm.enabled),
static=bool(alarm.static), static=bool(alarm.static),
state=getattr(AlarmState, str(alarm.state)), state=getattr(AlarmState, str(alarm.state)),
@ -375,6 +397,7 @@ class Alarm:
for req in self.actions.requests for req in self.actions.requests
], ],
snooze_interval=self.snooze_interval, snooze_interval=self.snooze_interval,
dismiss_interval=self.dismiss_interval,
enabled=self.is_enabled(), enabled=self.is_enabled(),
static=self.static, static=self.static,
condition_type=self.condition_type.value, condition_type=self.condition_type.value,

View File

@ -238,7 +238,10 @@ class MediaVlcPlugin(MediaPlugin):
def quit(self, *_, **__): def quit(self, *_, **__):
"""Quit the player (same as `stop`)""" """Quit the player (same as `stop`)"""
with self._stop_lock: with self._stop_lock:
assert self._player, 'No vlc instance is running' if not self._player:
self.logger.warning('No vlc instance is running')
return self.status()
self._player.stop() self._player.stop()
self._on_stop_event.wait(timeout=5) self._on_stop_event.wait(timeout=5)
self._reset_state() self._reset_state()