diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Alarm/AlarmEditor.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Alarm/AlarmEditor.vue index 1b5e44720..bacb7c8a6 100644 --- a/platypush/backend/http/webapp/src/components/panels/Entities/Alarm/AlarmEditor.vue +++ b/platypush/backend/http/webapp/src/components/panels/Entities/Alarm/AlarmEditor.vue @@ -138,8 +138,8 @@ <br /> <span class="subtext"> <span class="text"> - How long the interval should be paused after being triggered and - snoozed. + How long the alarm should be paused after being triggered and + manually snoozed. </span> </span> </div> @@ -150,6 +150,26 @@ </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="name"> <label> @@ -247,6 +267,7 @@ export default { 'media_plugin', 'name', 'snooze_interval', + 'dismiss_interval', 'when', ].forEach(key => { if (this.editForm[key] !== this.value[key]) @@ -258,6 +279,17 @@ export default { }, methods: { + actionsToArgs(actions) { + return actions?.map(action => { + if (action.name) { + action.action = action.name + delete action.name + } + + return action + }) ?? [] + }, + onWhenInput(value, type) { if (value == null) return @@ -302,7 +334,8 @@ export default { media_plugin: this.editForm.media_plugin, audio_volume: this.editForm.audio_volume, snooze_interval: this.editForm.snooze_interval, - actions: this.editForm.actions, + dismiss_interval: this.editForm.dismiss_interval, + actions: this.actionsToArgs(this.editForm.actions), } } else { action = 'alarm.edit' @@ -311,6 +344,9 @@ export default { ...this.changes, } + if (this.changes.actions) + args.actions = this.actionsToArgs(this.changes.actions) + if (this.changes.name != null) { args.name = this.value.name args.new_name = this.changes.name diff --git a/platypush/entities/alarm.py b/platypush/entities/alarm.py index 6a93c3345..7b2698a23 100644 --- a/platypush/entities/alarm.py +++ b/platypush/entities/alarm.py @@ -26,6 +26,7 @@ if not is_defined('alarm'): media_plugin = Column(String, nullable=True) audio_volume = Column(Integer, nullable=True) snooze_interval = Column(Integer, nullable=True) + dismiss_interval = Column(Integer, nullable=True) actions = Column(JSON, nullable=True) static = Column(Boolean, nullable=False, default=False) condition_type = Column(String, nullable=False) diff --git a/platypush/plugins/alarm/__init__.py b/platypush/plugins/alarm/__init__.py index 7aaa66f07..d0aef9b07 100644 --- a/platypush/plugins/alarm/__init__.py +++ b/platypush/plugins/alarm/__init__.py @@ -77,14 +77,15 @@ class AlarmPlugin(RunnablePlugin, EntityManager): def __init__( self, - alarms: Optional[Union[list, Dict[str, Any]]] = None, + alarms: Optional[Union[List[dict], Dict[str, dict]]] = None, media_plugin: Optional[str] = None, - poll_interval: Optional[float] = 5.0, + poll_interval: Optional[float] = 2.0, snooze_interval: float = 300.0, + dismiss_interval: float = 300.0, **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 :class:`platypush.plugins.media.MediaPlugin`) that will be used to 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 available configured local media plugin will be used. This only applies to alarms that are configured to play an audio resource. - :param poll_interval: Poll interval in seconds (default: 5). - :param snooze_interval: Default snooze interval in seconds (default: 300). + :param poll_interval: (Internal) poll interval, in seconds (default: 2). + :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) self.snooze_interval = snooze_interval + self.dismiss_interval = dismiss_interval self._db_lock = RLock() alarms = alarms or [] if isinstance(alarms, dict): @@ -259,6 +267,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): audio_volume: Optional[Union[int, float]] = None, enabled: bool = True, snooze_interval: Optional[float] = None, + dismiss_interval: Optional[float] = None, ) -> Alarm: alarm = Alarm( when=when, @@ -269,6 +278,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): media_plugin=media_plugin or self.media_plugin, audio_volume=audio_volume, snooze_interval=snooze_interval or self.snooze_interval, + dismiss_interval=dismiss_interval or self.dismiss_interval, stop_event=self._should_stop, on_change=self._on_alarm_update, ) @@ -316,6 +326,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): audio_volume: Optional[Union[int, float]] = None, enabled: bool = True, snooze_interval: Optional[float] = None, + dismiss_interval: Optional[float] = None, ) -> dict: """ Add a new alarm. @@ -332,6 +343,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): :param audio_volume: Volume of the audio. :param enabled: Whether the new alarm should be enabled (default: True). :param snooze_interval: Snooze seconds before playing the alarm again. + :param dismiss_interval: Dismiss seconds before stopping the alarm. :return: The newly created alarm. """ if audio_file: @@ -349,6 +361,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): enabled=enabled, audio_volume=audio_volume, snooze_interval=snooze_interval, + dismiss_interval=dismiss_interval, ).to_dict() @action @@ -363,6 +376,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): audio_volume: Optional[Union[int, float]] = None, enabled: Optional[bool] = None, snooze_interval: Optional[float] = None, + dismiss_interval: Optional[float] = None, ) -> dict: """ Edit an existing alarm. @@ -383,6 +397,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): :param audio_volume: Volume of the audio. :param enabled: Whether the new alarm should be enabled. :param snooze_interval: Snooze seconds before playing the alarm again. + :param dismiss_interval: Dismiss seconds before stopping the alarm. :return: The modified alarm. """ alarm = self._get_alarm(name) @@ -410,6 +425,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): if audio_volume is not None else alarm.audio_volume, snooze_interval=snooze_interval or alarm.snooze_interval, + dismiss_interval=dismiss_interval or alarm.dismiss_interval, ).to_dict() @action @@ -419,15 +435,25 @@ class AlarmPlugin(RunnablePlugin, EntityManager): :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, ( f'Alarm {name} is statically defined in the configuration, ' 'cannot overwrite it programmatically' ) + alarm.stop() + with self._db.get_session() as session: 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) @action @@ -507,6 +533,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager): "media_plugin": "media.vlc", "audio_volume": 10, "snooze_interval": 300, + "dismiss_interval": 300, "actions": [ { "action": "tts.say", diff --git a/platypush/plugins/alarm/_model.py b/platypush/plugins/alarm/_model.py index 406b0c058..eddb93c04 100644 --- a/platypush/plugins/alarm/_model.py +++ b/platypush/plugins/alarm/_model.py @@ -61,7 +61,8 @@ class Alarm: media_plugin: Optional[str] = None, audio_volume: Optional[Union[int, float]] = None, snooze_interval: float = 300, - poll_interval: float = 5, + dismiss_interval: float = 300, + poll_interval: float = 2, enabled: bool = True, static: bool = False, stop_event: Optional[threading.Event] = None, @@ -75,6 +76,7 @@ class Alarm: self.media_plugin = media_plugin self.audio_volume = audio_volume self.snooze_interval = snooze_interval + self.dismiss_interval = dismiss_interval self.state = AlarmState.UNKNOWN self.timer: Optional[threading.Timer] = None self.static = static @@ -91,6 +93,7 @@ class Alarm: self.stop_event = stop_event or threading.Event() self.poll_interval = poll_interval self.on_change = on_change + self._dismiss_timer: Optional[threading.Timer] = None def _on_change(self): if self.on_change: @@ -209,6 +212,7 @@ class Alarm: def dismiss(self): self.state = AlarmState.DISMISSED self.stop_audio() + self._clear_dismiss_timer() get_bus().post(AlarmDismissedEvent(name=self.name)) self._on_change() @@ -216,6 +220,7 @@ class Alarm: self._runtime_snooze_interval = interval or self.snooze_interval self.state = AlarmState.SNOOZED self.stop_audio() + self._clear_dismiss_timer() get_bus().post( AlarmSnoozedEvent(name=self.name, interval=self._runtime_snooze_interval) ) @@ -230,19 +235,27 @@ class Alarm: return interval = next_run - time.time() + self.state = AlarmState.WAITING self.timer = threading.Timer(interval, self.alarm_callback) self.timer.start() - self.state = AlarmState.WAITING + self._clear_dismiss_timer() self._on_change() def stop(self): self.state = AlarmState.SHUTDOWN + self.stop_audio() + if self.timer: self.timer.cancel() self.timer = None 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: plugin = get_plugin(self.media_plugin) assert plugin and isinstance(plugin, MediaPlugin), ( @@ -265,16 +278,23 @@ class Alarm: def stop_audio(self): 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): while not self.should_stop(): if self.is_enabled(): - 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() + self._on_start() elif self.state != AlarmState.WAITING: self.state = AlarmState.WAITING self._on_change() @@ -339,6 +359,7 @@ class Alarm: 'media_plugin': self.media_plugin, 'audio_volume': self.audio_volume, 'snooze_interval': self.snooze_interval, + 'dismiss_interval': self.dismiss_interval, 'actions': self.actions.requests, 'static': self.static, 'condition_type': self.condition_type.value, @@ -354,6 +375,7 @@ class Alarm: audio_volume=alarm.audio_volume, # type: ignore actions=alarm.actions, # type: ignore snooze_interval=alarm.snooze_interval, # type: ignore + dismiss_interval=alarm.dismiss_interval, # type: ignore enabled=bool(alarm.enabled), static=bool(alarm.static), state=getattr(AlarmState, str(alarm.state)), @@ -375,6 +397,7 @@ class Alarm: for req in self.actions.requests ], snooze_interval=self.snooze_interval, + dismiss_interval=self.dismiss_interval, enabled=self.is_enabled(), static=self.static, condition_type=self.condition_type.value, diff --git a/platypush/plugins/media/vlc/__init__.py b/platypush/plugins/media/vlc/__init__.py index 0965c9e7f..a80a7bef4 100644 --- a/platypush/plugins/media/vlc/__init__.py +++ b/platypush/plugins/media/vlc/__init__.py @@ -238,7 +238,10 @@ class MediaVlcPlugin(MediaPlugin): def quit(self, *_, **__): """Quit the player (same as `stop`)""" 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._on_stop_event.wait(timeout=5) self._reset_state()