Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
unit_tests:
strategy:
matrix:
python-version: [3.9, "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand Down Expand Up @@ -62,10 +62,3 @@ jobs:
run: |
pip install -U "pyee==8.1.0"
pytest --cov=ovos_bus_client --cov-report=xml --cov-append test/unittests
- name: Upload coverage
if: "${{ matrix.python-version == '3.9' }}"
uses: codecov/codecov-action@v5
with:
token: ${{secrets.CODECOV_TOKEN}}
files: coverage.xml
verbose: true
35 changes: 3 additions & 32 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,12 @@
# Changelog

## [1.3.7a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/1.3.7a1) (2025-11-06)
## [1.3.8a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/1.3.8a1) (2025-11-09)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/1.3.6a2...1.3.7a1)
[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/1.3.7...1.3.8a1)

**Merged pull requests:**

- fix: Update requirements.txt [\#175](https://github.com/OpenVoiceOS/ovos-bus-client/pull/175) ([JarbasAl](https://github.com/JarbasAl))

## [1.3.6a2](https://github.com/OpenVoiceOS/ovos-bus-client/tree/1.3.6a2) (2025-11-06)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/1.3.6a1...1.3.6a2)

**Implemented enhancements:**

- EventScheduler full CRUD [\#80](https://github.com/OpenVoiceOS/ovos-bus-client/issues/80)

**Merged pull requests:**

- Update setup.py [\#173](https://github.com/OpenVoiceOS/ovos-bus-client/pull/173) ([JarbasAl](https://github.com/JarbasAl))
- feat\(EventScheduler\): list\_events API [\#172](https://github.com/OpenVoiceOS/ovos-bus-client/pull/172) ([mikejgray](https://github.com/mikejgray))

## [1.3.6a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/1.3.6a1) (2025-09-05)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/1.3.5a1...1.3.6a1)

**Merged pull requests:**

- fix: make orjson optional [\#169](https://github.com/OpenVoiceOS/ovos-bus-client/pull/169) ([JarbasAl](https://github.com/JarbasAl))

## [1.3.5a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/1.3.5a1) (2025-06-16)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/1.3.4...1.3.5a1)

**Merged pull requests:**

- Update ovos-config requirement from \<2.0.0,\>=0.0.12 to \>=0.0.12,\<3.0.0 [\#167](https://github.com/OpenVoiceOS/ovos-bus-client/pull/167) ([dependabot[bot]](https://github.com/apps/dependabot))
- refactor: migrate bus api client to ovos-bus-client package [\#177](https://github.com/OpenVoiceOS/ovos-bus-client/pull/177) ([JarbasAl](https://github.com/JarbasAl))



Expand Down
67 changes: 34 additions & 33 deletions downstream_report.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
ovos-bus-client==1.3.6a1
├── ovos_wikipedia_solver==0.1.2 [requires: ovos-bus-client>=1.0.1]
ovos_bus_client==1.3.7
├── ovos_wikipedia_solver==0.1.2 [requires: ovos_bus_client>=1.0.1]
│ └── ovos-skill-wikipedia==0.8.13 [requires: ovos_wikipedia_solver>=0.0.1,<1.0.0]
├── ovos_gui==1.3.4 [requires: ovos-bus-client>=1.0.0,<2.0.0]
├── ovos_audio==1.1.0 [requires: ovos-bus-client>=0.0.8,<2.0.0]
├── ovos_gui==1.3.4 [requires: ovos_bus_client>=1.0.0,<2.0.0]
├── ovos_audio==1.1.0 [requires: ovos_bus_client>=0.0.8,<2.0.0]
│ └── ovos-skill-laugh==0.2.3 [requires: ovos_audio]
├── ovos-workshop==7.0.9a1 [requires: ovos-bus-client>=0.0.8,<2.0.0]
├── ovos-workshop==7.0.9a1 [requires: ovos_bus_client>=0.0.8,<2.0.0]
│ ├── ovos-skill-screenshot==0.0.7 [requires: ovos-workshop]
│ ├── ovos-skill-fallback-unknown==0.1.9 [requires: ovos-workshop>=6.0.0,<8.0.0]
│ ├── ovos-skill-parrot==0.1.26a1 [requires: ovos-workshop>=7.0.0,<8.0.0]
Expand Down Expand Up @@ -51,18 +51,18 @@ ovos-bus-client==1.3.6a1
│ ├── ovos-skill-wikipedia==0.8.13 [requires: ovos-workshop>=3.4.0,<8.0.0]
│ ├── ovos-skill-moviemaster==0.0.12 [requires: ovos-workshop>=0.0.11,<8.0.0]
│ └── ovos-skill-days-in-history==0.3.11 [requires: ovos-workshop>=3.1.0,<8.0.0]
├── ovos_PHAL_plugin_system==1.3.4 [requires: ovos-bus-client>=1.1.0,<2.0.0]
├── ovos_common_query_pipeline_plugin==1.1.9 [requires: ovos-bus-client]
├── ovos-skill-laugh==0.2.3 [requires: ovos-bus-client>=1.0.1]
├── ovos-skill-local-media==0.2.13a1 [requires: ovos-bus-client>=0.0.9,<2.0.0]
├── ovos-skill-pyradios==0.1.5 [requires: ovos-bus-client>=0.0.9]
├── ovos_core==2.1.1 [requires: ovos-bus-client>=1.3.6a1,<2.0.0]
├── ovos_PHAL_plugin_system==1.3.4 [requires: ovos_bus_client>=1.1.0,<2.0.0]
├── ovos_common_query_pipeline_plugin==1.1.9 [requires: ovos_bus_client]
├── ovos-skill-laugh==0.2.3 [requires: ovos_bus_client>=1.0.1]
├── ovos-skill-local-media==0.2.13a1 [requires: ovos_bus_client>=0.0.9,<2.0.0]
├── ovos-skill-pyradios==0.1.5 [requires: ovos_bus_client>=0.0.9]
├── ovos_core==2.1.1 [requires: ovos_bus_client>=1.3.6a1,<2.0.0]
│ └── ovoscope==0.7.2 [requires: ovos_core>=2.0.4a2]
├── ovos_m2v_pipeline==0.0.7 [requires: ovos-bus-client]
├── ovos_PHAL_plugin_wallpaper_manager==0.2.6 [requires: ovos-bus-client>=0.0.3,<2.0.0]
├── ovos_ww_plugin_vosk==0.1.8 [requires: ovos-bus-client>=0.0.6]
├── ovos-PHAL-plugin-oauth==0.1.3 [requires: ovos-bus-client>=0.0.3,<2.0.0]
├── ovos-plugin-manager==1.0.4a2 [requires: ovos-bus-client>=0.0.8,<2.0.0]
├── ovos_m2v_pipeline==0.0.7 [requires: ovos_bus_client]
├── ovos_PHAL_plugin_wallpaper_manager==0.2.6 [requires: ovos_bus_client>=0.0.3,<2.0.0]
├── ovos_ww_plugin_vosk==0.1.8 [requires: ovos_bus_client>=0.0.6]
├── ovos-PHAL-plugin-oauth==0.1.3 [requires: ovos_bus_client>=0.0.3,<2.0.0]
├── ovos-plugin-manager==1.0.4a2 [requires: ovos_bus_client>=0.0.8,<2.0.0]
│ ├── ovos-stt-plugin-citrinet==0.0.9 [requires: ovos-plugin-manager>=1.0.0,<2.0.0]
│ ├── ovos-ocp-m3u-plugin==0.0.2a2 [requires: ovos-plugin-manager]
│ │ └── ovos-ocp-news-plugin==0.1.2 [requires: ovos-ocp-m3u-plugin>=0.0.1,<1.0.0]
Expand Down Expand Up @@ -201,27 +201,28 @@ ovos-bus-client==1.3.6a1
│ ├── ovos-PHAL-plugin-mk2-fan-control==0.0.1 [requires: ovos-plugin-manager>=0.0.1]
│ ├── ovos-stt-plugin-fasterwhisper==0.0.1a8 [requires: ovos-plugin-manager>=0.0.23a10]
│ ├── ovos_phal_plugin_connectivity_events==0.1.3 [requires: ovos-plugin-manager>=0.0.21,<3.0.0]
│ ├── ovos-stt-plugin-whisper==0.1.4 [requires: ovos-plugin-manager>=0.0.26,<3.0.0]
│ ├── ovos_solver_failure_plugin==0.0.3 [requires: ovos-plugin-manager>=0.0.26,<3.0.0]
│ │ └── ovos_persona==0.6.24 [requires: ovos_solver_failure_plugin]
│ ├── ovos_tts_plugin_server==0.0.5a1 [requires: ovos-plugin-manager>=1.0.0,<3.0.0]
│ ├── ovos-stt-plugin-nos==0.2.0 [requires: ovos-plugin-manager>=0.0.24]
│ ├── ovos-tts-plugin-pico==0.0.4a2 [requires: ovos-plugin-manager>=0.0.1a12]
│ └── ovos_utterance_corrections_plugin==0.1.2 [requires: ovos-plugin-manager>=0.0.1,<3.0.0]
├── ovos-skill-naptime==0.3.16a1 [requires: ovos-bus-client>=1.2.0,<2.0.0]
├── ovos_plugin_common_play==1.2.2a1 [requires: ovos-bus-client>=0.0.7,<2.0.0]
├── ovos_PHAL_plugin_alsa==0.1.5 [requires: ovos-bus-client>=0.0.4,<2.0.0]
├── ovos-skill-alerts==0.1.28a1 [requires: ovos-bus-client>=0.0.3,<2.0.0]
├── ovos-dinkum-listener==0.5.1a1 [requires: ovos-bus-client>=1.3.4,<2.0.0]
├── ovos-skill-iss-location==0.2.16 [requires: ovos-bus-client>=1.0.1]
├── ovos_PHAL==0.2.11 [requires: ovos-bus-client>=0.0.8,<2.0.0]
├── ovos-skill-somafm==0.1.6a1 [requires: ovos-bus-client>=0.1.0]
├── ovos_gui_plugin_shell_companion==1.0.6 [requires: ovos-bus-client>=0.0.8,<2.0.0]
├── ovos-skill-homescreen==3.0.3 [requires: ovos-bus-client>=1.0.0,<2.0.0]
├── ovos-PHAL-plugin-balena-wifi==1.2.2 [requires: ovos-bus-client>=0.0.6,<2.0.0]
├── ovos-skill-news==0.4.6a1 [requires: ovos-bus-client>=0.0.9,<2.0.0]
├── ovos_phal_plugin_connectivity_events==0.1.3 [requires: ovos-bus-client>=0.0.3,<2.0.0]
├── ovos-skill-confucius-quotes==0.1.13 [requires: ovos-bus-client>=1.0.1]
├── ovos-skill-youtube-music==0.1.7 [requires: ovos-bus-client>=0.0.9]
├── ovos-messagebus==0.0.11a1 [requires: ovos-bus-client>=0.0.7,<2.0.0]
└── ovos-mark1-utils==0.0.1 [requires: ovos-bus-client]
├── ovos-skill-naptime==0.3.16a1 [requires: ovos_bus_client>=1.2.0,<2.0.0]
├── ovos_plugin_common_play==1.2.2a1 [requires: ovos_bus_client>=0.0.7,<2.0.0]
├── ovos_PHAL_plugin_alsa==0.1.5 [requires: ovos_bus_client>=0.0.4,<2.0.0]
├── ovos-skill-alerts==0.1.28a1 [requires: ovos_bus_client>=0.0.3,<2.0.0]
├── ovos-dinkum-listener==0.5.1a1 [requires: ovos_bus_client>=1.3.4,<2.0.0]
├── ovos-skill-iss-location==0.2.16 [requires: ovos_bus_client>=1.0.1]
├── ovos_PHAL==0.2.11 [requires: ovos_bus_client>=0.0.8,<2.0.0]
├── ovos-skill-somafm==0.1.6a1 [requires: ovos_bus_client>=0.1.0]
├── ovos_gui_plugin_shell_companion==1.0.6 [requires: ovos_bus_client>=0.0.8,<2.0.0]
├── ovos-skill-homescreen==3.0.3 [requires: ovos_bus_client>=1.0.0,<2.0.0]
├── ovos-PHAL-plugin-balena-wifi==1.2.2 [requires: ovos_bus_client>=0.0.6,<2.0.0]
├── ovos-skill-news==0.4.6a1 [requires: ovos_bus_client>=0.0.9,<2.0.0]
├── ovos_phal_plugin_connectivity_events==0.1.3 [requires: ovos_bus_client>=0.0.3,<2.0.0]
├── ovos-skill-confucius-quotes==0.1.13 [requires: ovos_bus_client>=1.0.1]
├── ovos-skill-youtube-music==0.1.7 [requires: ovos_bus_client>=0.0.9]
├── ovos-messagebus==0.0.11a1 [requires: ovos_bus_client>=0.0.7,<2.0.0]
└── ovos-mark1-utils==0.0.1 [requires: ovos_bus_client]
└── ovos-PHAL-plugin-mk1==0.1.3 [requires: ovos-mark1-utils>=0.0.1]
229 changes: 229 additions & 0 deletions ovos_bus_client/apis/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import time
from datetime import datetime, timedelta
from typing import Callable, Optional, Union

from ovos_utils.events import EventContainer, create_basic_wrapper
from ovos_bus_client.message import Message, dig_for_message
from ovos_utils.log import LOG
from ovos_config.locale import get_default_tz
from ovos_utils.time import now_local


class EventSchedulerInterface:
"""Interface for accessing the event scheduler over the message bus."""

def __init__(self, bus=None, skill_id=None):
self.skill_id = skill_id or self.__class__.__name__.lower()
self.bus = bus
self.events = EventContainer(bus)
self.scheduled_repeats = []

def set_bus(self, bus):
"""Attach the messagebus of the parent skill

Args:
bus (MessageBusClient): websocket connection to the messagebus
"""
self.bus = bus
self.events.set_bus(bus)

def set_id(self, skill_id: str):
"""
Attach the skill_id of the parent skill

Args:
skill_id (str): skill_id of the parent skill
"""
self.skill_id = skill_id

def _get_source_message(self):
message = dig_for_message() or Message("")
message.context['skill_id'] = self.skill_id
return message

def _create_unique_name(self, name: str) -> str:
"""
Return a name unique to this skill using the format [skill_id]:[name].
@param name: Name to use internally
@return name unique to this skill
"""
# TODO: Is a null name valid or should it raise an exception?
return self.skill_id + ':' + (name or '')

def _schedule_event(self, handler: Callable[..., None],
when: Union[datetime, int, float],
data: Optional[dict],
name: Optional[str],
repeat_interval: Optional[Union[float, int]] = None,
context: Optional[dict] = None):
"""
Underlying method for schedule_event and schedule_repeating_event.
Takes scheduling information and sends it off on the message bus.
@param handler: method to be called at the scheduled time(s)
@param when: time (tzaware or default to system tz) or delta seconds to
first call the handler
@param data: Message data to send to `handler
@param name: Event name, must be unique in the context of this object
@param repeat_interval: time in seconds between calls
@param context: Message context to send to `handler`

"""
if isinstance(when, (int, float)):
if when < 0:
raise ValueError(f"Expected datetime or positive int/float. "
f"got: {when}")
when = now_local() + timedelta(seconds=when)
if not isinstance(when, datetime):
raise TypeError(f"Expected datetime, int, or float but got: {when}")
if when.tzinfo is None:
# ensure correct timezone before conversion to unix timestamp
# naive datetime objects method relies on the platform C mktime() function to perform the conversion
# and may not match mycroft.conf
when = when.replace(tzinfo=get_default_tz())
if not name:
name = self.skill_id + handler.__name__
unique_name = self._create_unique_name(name)
if repeat_interval:
self.scheduled_repeats.append(name) # store "friendly name"

data = data or {}

def on_error(e):
LOG.exception(f'An error occurred executing the scheduled event: '
f'{e}')

wrapped = create_basic_wrapper(handler, on_error)
self.events.add(unique_name, wrapped, once=not repeat_interval)
event_data = {'time': when.timestamp(), # Epoch timestamp
'event': unique_name,
'repeat': repeat_interval,
'data': data}

message = self._get_source_message()
context = context or message.context
context["skill_id"] = self.skill_id
self.bus.emit(Message('mycroft.scheduler.schedule_event',
data=event_data, context=context))

def schedule_event(self, handler: Callable[..., None],
when: Union[datetime, int, float],
data: Optional[dict] = None,
name: Optional[str] = None,
context: Optional[dict] = None):
"""
Schedule a single-shot event.
@param handler: method to be called at the scheduled time(s)
@param when: time (tzaware or default to system tz) or delta seconds
to first call the handler
@param data: Message data to send to `handler
@param name: Event name, must be unique in the context of this object
@param context: Message context to send to `handler`
"""
self._schedule_event(handler, when, data, name, context=context)

def schedule_repeating_event(self,
handler: Callable[..., None],
when: Optional[Union[datetime, int, float]],
interval: Union[float, int],
data: Optional[dict] = None,
name: Optional[str] = None,
context: Optional[dict] = None):
"""
Schedule a repeating event.
@param handler: method to be called at the scheduled time(s)
@param when: time (tzaware or default to system tz) or delta seconds to
first call the handler. If None, first call is in `repeat_interval`
@param data: Message data to send to `handler
@param name: Event name, must be unique in the context of this object
@param interval: time in seconds between calls
@param context: Message context to send to `handler`
"""
# Ensure name is defined to avoid re-scheduling
name = name or self.skill_id + handler.__name__

# Do not schedule if this event is already scheduled by the skill
if name not in self.scheduled_repeats:
# If only interval is given set to trigger in [interval] seconds
# from now.
if not when:
when = now_local() + timedelta(seconds=interval)
self._schedule_event(handler, when, data, name, interval, context)
else:
LOG.debug('The event is already scheduled, cancel previous '
'event if this scheduling should replace the last.')

def update_scheduled_event(self, name: str, data: Optional[dict] = None):
"""
Change data of event.

Args:
name (str): reference name of event (from original scheduling)
data (dict): new data to update event with
"""
data = {
'event': self._create_unique_name(name),
'data': data or {}
}
message = self._get_source_message()
self.bus.emit(message.forward('mycroft.schedule.update_event', data))

def cancel_scheduled_event(self, name: str):
"""
Cancel a pending event. The event will no longer be scheduled.

Args:
name (str): reference name of event (from original scheduling)
"""
unique_name = self._create_unique_name(name)
data = {'event': unique_name}
if name in self.scheduled_repeats:
self.scheduled_repeats.remove(name)
if self.events.remove(unique_name):
message = self._get_source_message()
self.bus.emit(message.forward('mycroft.scheduler.remove_event', data))

def get_scheduled_event_status(self, name: str) -> int:
"""
Get scheduled event data and return the amount of time left

Args:
name (str): reference name of event (from original scheduling)

Returns:
int: the time left in seconds

Raises:
Exception: Raised if event is not found
"""
event_name = self._create_unique_name(name)
data = {'name': event_name}

reply_name = f'mycroft.event_status.callback.{event_name}'
message = self._get_source_message()
msg = message.forward('mycroft.scheduler.get_event', data)
status = self.bus.wait_for_response(msg, reply_type=reply_name)

if status:
event_time = int(status.data[0][0])
current_time = int(time.time())
time_left_in_seconds = event_time - current_time
LOG.info(time_left_in_seconds)
return time_left_in_seconds
else:
raise Exception("Event Status Messagebus Timeout")

def cancel_all_repeating_events(self):
"""
Cancel any repeating events started by the skill.
"""
# NOTE: Gotta make a copy of the list due to the removes that happen
# in cancel_scheduled_event().
for e in list(self.scheduled_repeats):
self.cancel_scheduled_event(e)

def shutdown(self):
"""
Shutdown the interface unregistering any event handlers.
"""
self.cancel_all_repeating_events()
self.events.clear()
Loading