From ae46bb64160850b050f9c0f276d4470efb076a6b Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:02:35 +0100 Subject: [PATCH 01/76] debug logs on message emit failure (#43) --- ovos_bus_client/client/client.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ovos_bus_client/client/client.py b/ovos_bus_client/client/client.py index cd6b21d..bfc0578 100644 --- a/ovos_bus_client/client/client.py +++ b/ovos_bus_client/client/client.py @@ -165,14 +165,17 @@ def emit(self, message: Message): 'before emitting messages') self.connected_event.wait() + if hasattr(message, 'serialize'): + msg = message.serialize() + else: + msg = json.dumps(message.__dict__) try: - if hasattr(message, 'serialize'): - self.client.send(message.serialize()) - else: - self.client.send(json.dumps(message.__dict__)) + self.client.send(msg) except WebSocketConnectionClosedException: - LOG.warning('Could not send %s message because connection ' - 'has been closed', message.msg_type) + LOG.warning(f'Could not send {message.msg_type} message because connection ' + 'has been closed') + except Exception as e: + LOG.exception(f"failed to emit message {message.msg_type} with len {len(msg)}") def collect_responses(self, message: Message, min_timeout: Union[int, float] = 0.2, From b47ba28937e2170020d6276737235905c8177dff Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 1 Aug 2023 15:02:53 +0000 Subject: [PATCH 02/76] Increment Version to 0.0.6a1 --- ovos_bus_client/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 820f295..df66784 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 0 -VERSION_BUILD = 5 -VERSION_ALPHA = 0 +VERSION_BUILD = 6 +VERSION_ALPHA = 1 # END_VERSION_BLOCK From ece8eba2987022aae6fc2d9a49176aaff7209361 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 1 Aug 2023 15:03:20 +0000 Subject: [PATCH 03/76] Update Changelog --- CHANGELOG.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25fbee2..421db47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,12 @@ # Changelog -## [V0.0.5a2](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.5a2) (2023-06-27) +## [0.0.6a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a1) (2023-08-01) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.5a1...V0.0.5a2) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.5...0.0.6a1) **Merged pull requests:** -- Add unit test coverage for \#40 [\#41](https://github.com/OpenVoiceOS/ovos-bus-client/pull/41) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.5a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.5a1) (2023-06-21) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.4...V0.0.5a1) - -**Fixed bugs:** - -- hotfix/context deserialize [\#40](https://github.com/OpenVoiceOS/ovos-bus-client/pull/40) ([JarbasAl](https://github.com/JarbasAl)) +- debug logs on message emit failure [\#43](https://github.com/OpenVoiceOS/ovos-bus-client/pull/43) ([JarbasAl](https://github.com/JarbasAl)) From 6eb7c564980333019bf9b2545a5c51b5d824fe1c Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Fri, 4 Aug 2023 10:38:55 -0700 Subject: [PATCH 04/76] Init fix, logging updates, and unit tests (#45) * Add more logging to `scheduler.py` to troubleshoot missing callbacks * Add more logging to `scheduler.py` to troubleshoot missing callbacks * Add more logging to `scheduler.py` to troubleshoot missing callbacks * Fix init bug where a parameter is defined potentially after first reference Ensure stopping event is cleared on thread start Add unit test coverage for scheduler init --- ovos_bus_client/util/scheduler.py | 21 ++++++++++--- test/unittests/test_util.py | 49 +++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/ovos_bus_client/util/scheduler.py b/ovos_bus_client/util/scheduler.py index 9dc4c76..59d959e 100644 --- a/ovos_bus_client/util/scheduler.py +++ b/ovos_bus_client/util/scheduler.py @@ -96,10 +96,13 @@ def __init__(self, bus, self.update_event_handler) self.bus.on('mycroft.scheduler.get_event', self.get_event_handler) - if autostart: - self.start() self._stopping = Event() + if autostart: + self.start() + else: + # Not running + self._stopping.set() @property def is_running(self) -> bool: @@ -138,9 +141,14 @@ def run(self): """ Check events periodically until stopped """ + LOG.info("EventScheduler Started") + self._stopping.clear() while not self._stopping.wait(0.5): - self.check_state() - LOG.info(f"Stopped") + try: + self.check_state() + except Exception as e: + LOG.exception(e) + LOG.info("EventScheduler Stopped") def check_state(self): """ @@ -173,6 +181,7 @@ def check_state(self): # Finally, emit the queued up events that triggered for msg in pending_messages: + LOG.debug(f"Call scheduled event: {msg.msg_type}") self.bus.emit(msg) def schedule_event(self, event: str, sched_time: float, @@ -201,9 +210,13 @@ def schedule_event(self, event: str, sched_time: float, LOG.debug(f'Repeating event {event} is already scheduled, ' f'discarding') else: + LOG.debug(f"Scheduled event: {event} for time {sched_time}") # add received event and time event_list.append((sched_time, repeat, data, context)) self.events[event] = event_list + if sched_time < time.time(): + LOG.warning(f"Added event is scheduled in the past and " + f"will be called immediately: {event}") def schedule_event_handler(self, message: Message): """ diff --git a/test/unittests/test_util.py b/test/unittests/test_util.py index c6e2168..491619a 100644 --- a/test/unittests/test_util.py +++ b/test/unittests/test_util.py @@ -1,22 +1,53 @@ +import time import unittest +from os.path import isfile +from ovos_utils.messagebus import FakeBus + class TestScheduler(unittest.TestCase): def test_repeat_time(self): from ovos_bus_client.util.scheduler import repeat_time - # TODO + next_time = repeat_time(time.time(), -30) + self.assertGreaterEqual(next_time, time.time()) + + next_time = repeat_time(time.time() - 5, 10) + self.assertGreaterEqual(next_time, time.time()) + + +class TestEventScheduler(unittest.TestCase): + scheduler = None + bus = FakeBus() + test_schedule_name = f"test_{time.time()}.json" - def test_event_scheduler(self): + @classmethod + def setUpClass(cls) -> None: from ovos_bus_client.util.scheduler import EventScheduler - # TODO + cls.scheduler = EventScheduler(cls.bus, cls.test_schedule_name, False) - def test_event_container(self): - from ovos_bus_client.util.scheduler import EventContainer - # TODO + def test_00_init(self): + self.assertEqual(self.scheduler.events, dict()) + self.assertIsNotNone(self.scheduler.event_lock) + self.assertEqual(self.scheduler.bus, self.bus) + self.assertFalse(isfile(self.scheduler.schedule_file)) + self.assertEqual( + len(self.bus.ee.listeners("mycroft.scheduler.schedule_event")), 1) + self.assertEqual( + len(self.bus.ee.listeners("mycroft.scheduler.remove_event")), 1) + self.assertEqual( + len(self.bus.ee.listeners("mycroft.scheduler.update_event")), 1) + self.assertEqual( + len(self.bus.ee.listeners("mycroft.scheduler.get_event")), 1) + self.assertFalse(self.scheduler.is_running) - def test_event_scheduler_interface(self): - from ovos_bus_client.util.scheduler import EventSchedulerInterface - # TODO + self.scheduler.start() + timeout = time.time() + 2 + while not self.scheduler.is_running and time.time() < timeout: + time.sleep(0.2) + self.assertTrue(self.scheduler.is_running) + + self.scheduler._stopping.set() + self.assertFalse(self.scheduler.is_running) class TestUtils(unittest.TestCase): From bf5403ba778751bcd6bc36f14c230cdcbd4669c8 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 4 Aug 2023 17:39:12 +0000 Subject: [PATCH 05/76] Increment Version to 0.0.6a2 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index df66784..b64d8ef 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK From 202104a6f2245b731c05f82c884675e5623dffa0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 4 Aug 2023 17:39:43 +0000 Subject: [PATCH 06/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 421db47..0683d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a1) (2023-08-01) +## [0.0.6a2](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a2) (2023-08-04) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.5...0.0.6a1) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a1...0.0.6a2) + +**Merged pull requests:** + +- Init fix, logging updates, and unit tests [\#45](https://github.com/OpenVoiceOS/ovos-bus-client/pull/45) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.6a1](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a1) (2023-08-01) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.5...V0.0.6a1) **Merged pull requests:** From b522dd53c9e328035e154417e969bfa5d3f6145c Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:44:21 -0700 Subject: [PATCH 07/76] Remove duplicated classes from `ovos_utils` (#44) Co-authored-by: JarbasAI <33701864+JarbasAl@users.noreply.github.com> --- ovos_bus_client/util/scheduler.py | 361 +----------------------------- requirements.txt | 2 +- 2 files changed, 11 insertions(+), 352 deletions(-) diff --git a/ovos_bus_client/util/scheduler.py b/ovos_bus_client/util/scheduler.py index 59d959e..1028b41 100644 --- a/ovos_bus_client/util/scheduler.py +++ b/ovos_bus_client/util/scheduler.py @@ -35,6 +35,8 @@ from ovos_utils.log import LOG, log_deprecation, deprecated from ovos_utils.messagebus import FakeBus from ovos_utils.events import create_basic_wrapper +from ovos_utils.events import EventContainer as _EventContainer +from ovos_utils.events import EventSchedulerInterface as _SchedulerInterface from ovos_bus_client.message import Message @@ -341,356 +343,13 @@ def shutdown(self): self.store() -class EventContainer: - """ - Container tracking messagbus handlers. - - This container tracks events added by a skill, allowing unregistering - all events on shutdown. - """ - - def __init__(self, bus=None): - self.bus = bus or FakeBus() - self.events = [] - - def set_bus(self, bus): - self.bus = bus - - def add(self, name: str, handler: Callable, once: bool = False): - """ - Create event handler for executing intent or other event. - - Arguments: - name (string): IntentParser name - handler (func): Method to call - once (bool, optional): Event handler will be removed after it has - been run once. - """ - - def once_wrapper(message): - # Remove registered one-time handler before invoking, - # allowing them to re-schedule themselves. - self.remove(name) - handler(message) - - if handler: - if once: - self.bus.once(name, once_wrapper) - self.events.append((name, once_wrapper)) - else: - self.bus.on(name, handler) - self.events.append((name, handler)) - - LOG.debug('Added event: {}'.format(name)) - - def remove(self, name: str): - """ - Removes an event from bus emitter and events list. - - Args: - name (string): Name of Intent or Scheduler Event - Returns: - bool: True if found and removed, False if not found - """ - LOG.debug("Removing event {}".format(name)) - removed = False - for _name, _handler in list(self.events): - if name == _name: - try: - self.events.remove((_name, _handler)) - except ValueError: - LOG.error('Failed to remove event {}'.format(name)) - pass - removed = True - - # Because of function wrappers, the emitter doesn't always directly - # hold the _handler function, it sometimes holds something like - # 'wrapper(_handler)'. So a call like: - # self.bus.remove(_name, _handler) - # will not find it, leaving an event handler with that name left behind - # waiting to fire if it is ever re-installed and triggered. - # Remove all handlers with the given name, regardless of handler. - if removed: - self.bus.remove_all_listeners(name) - return removed - - def __iter__(self): - return iter(self.events) - - def clear(self): - """ - Unregister all registered handlers and clear the list of registered - events. - """ - for e, f in self.events: - self.bus.remove(e, f) - self.events = [] # Remove reference to wrappers - - -class EventSchedulerInterface: - """ - Interface for accessing the event scheduler over the message bus. - """ - - def __init__(self, name=None, sched_id=None, bus=None, skill_id=None): - # NOTE: can not rename or move sched_id/name arguments to keep api compatibility - if name: - log_deprecation("name argument has been deprecated! " - "use skill_id instead", "0.1.0") - if sched_id: - log_deprecation("sched_id argument has been deprecated! " - "use skill_id instead", "0.1.0") - - self.skill_id = skill_id or sched_id or name or self.__class__.__name__ - 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, sched_id: str): - """ - Attach the skill_id of the parent skill - - Args: - sched_id (str): skill_id of the parent skill - """ - # NOTE: can not rename sched_id kwarg to keep api compatibility - self.skill_id = sched_id - - def _create_unique_name(self, name: str) -> str: - """ - Return a name unique to this skill using the format - [skill_id]:[name]. - - Args: - name: Name to use internally +class EventContainer(_EventContainer): + def __init__(self, *args, **kwargs): + log_deprecation("Import from `ovos_utils.events`", "0.1.0") + _EventContainer.__init__(self, *args, **kwargs) - Returns: - str: name unique to this skill - """ - return self.skill_id + ':' + (name or '') - - def _schedule_event(self, handler: Callable, when: datetime, - data: dict, name: str, - repeat_interval: Optional[float] = 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. - - Args: - handler: method to be called - when (datetime): time (in system timezone) for first - calling the handler, or None to - initially trigger seconds - from now - data (dict, optional): data to send when the handler is called - name (str, optional): reference name, must be unique - repeat_interval (float/int): time in seconds between calls - context (dict, optional): message context to send - when the handler is called - """ - if isinstance(when, (int, float)) and when >= 0: - when = datetime.now() + timedelta(seconds=when) - 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} - context = context or {} - 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, - when: datetime, - data: Optional[dict] = None, - name: Optional[str] = None, - context: Optional[dict] = None): - """ - Schedule a single-shot event. - - Args: - handler: method to be called - when (datetime/int/float): datetime (in system timezone) or - number of seconds in the future when the - handler should be called - data (dict, optional): data to send when the handler is called - name (str, optional): reference name - NOTE: This will not warn or replace a - previously scheduled event of the same - name. - context (dict, optional): message context to send - when the handler is called - """ - self._schedule_event(handler, when, data, name, context=context) - - def schedule_repeating_event(self, handler: Callable, - when: Optional[datetime], - interval: Union[float, int], - data: Optional[dict] = None, - name: Optional[str] = None, - context: Optional[dict] = None): - """ - Schedule a repeating event. - - Args: - handler: method to be called - when (datetime): time (in system timezone) for first - calling the handler, or None to - initially trigger seconds - from now - interval (float/int): time in seconds between calls - data (dict, optional): data to send when the handler is called - name (str, optional): reference name, must be unique - context (dict, optional): message context to send - when the handler is called - """ - # 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 = datetime.now() + timedelta(seconds=interval) - self._schedule_event(handler, when, data, name, interval, - context=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 = data or {} - data = { - 'event': self._create_unique_name(name), - 'data': data - } - self.bus.emit(Message('mycroft.schedule.update_event', - data=data, context={"skill_id": self.skill_id})) - - 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): - self.bus.emit(Message('mycroft.scheduler.remove_event', - data=data, - context={"skill_id": self.skill_id})) - - 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}' - msg = Message('mycroft.scheduler.get_event', data=data, - context={"skill_id": self.skill_id}) - 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() - - @property - @deprecated("self.sched_id has been deprecated! use self.skill_id instead", - "0.1.0") - def sched_id(self): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and returns self.skill_id - """ - return self.skill_id - - @sched_id.setter - @deprecated("self.sched_id has been deprecated! use self.skill_id instead", - "0.1.0") - def sched_id(self, skill_id): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and sets self.skill_id - """ - self.skill_id = skill_id - - @property - @deprecated("self.name has been deprecated! use self.skill_id instead", - "0.1.0") - def name(self): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and returns self.skill_id - """ - return self.skill_id - - @name.setter - @deprecated("self.name has been deprecated! use self.skill_id instead", - "0.1.0") - def name(self, skill_id): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and sets self.skill_id - """ - self.skill_id = skill_id +class EventSchedulerInterface(_SchedulerInterface): + def __init__(self, *args, **kwargs): + log_deprecation("Import from `ovos_utils.events`", "0.1.0") + _SchedulerInterface.__init__(self, *args, **kwargs) diff --git a/requirements.txt b/requirements.txt index 8a8f681..56b5568 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ ovos-config >= 0.0.8, < 0.1.0 -ovos-utils >= 0.0.34, < 0.1.0 +ovos-utils >= 0.0.36a1, < 0.1.0 websocket-client>=0.54.0 pyee>=8.1.0, < 9.0.0 From b746ef36f5471c76e104f71bcc4aa7c60d208c7b Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 7 Aug 2023 19:44:39 +0000 Subject: [PATCH 08/76] Increment Version to 0.0.6a3 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index b64d8ef..c3a3e82 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK From 3f8d0cebb22364b1f706afaf57bf5ea0c7b88a13 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 7 Aug 2023 19:45:03 +0000 Subject: [PATCH 09/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0683d5d..9004f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a2](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a2) (2023-08-04) +## [0.0.6a3](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a3) (2023-08-07) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a1...0.0.6a2) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a2...0.0.6a3) + +**Merged pull requests:** + +- Remove duplicated classes from `ovos_utils` [\#44](https://github.com/OpenVoiceOS/ovos-bus-client/pull/44) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.6a2](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a2) (2023-08-04) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a1...V0.0.6a2) **Merged pull requests:** From 1ed65b54c15e20cd5b6e135324b538e268216c5a Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:34:03 -0700 Subject: [PATCH 10/76] Update EventScheduler init for backwards-compat. (#46) * Update scheduler init to wait for thread start for test compat. Add unit test coverage for started state after init * Address PR feedback for more precise event handling * More specific exception handling * Refactor exception handling to raise exceptions to caller * Handle storing events in case a different exception is raised --- ovos_bus_client/util/scheduler.py | 43 +++++++++++++++++++------------ test/unittests/test_util.py | 8 ++++++ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/ovos_bus_client/util/scheduler.py b/ovos_bus_client/util/scheduler.py index 1028b41..ba06481 100644 --- a/ovos_bus_client/util/scheduler.py +++ b/ovos_bus_client/util/scheduler.py @@ -21,18 +21,16 @@ import json import shutil - import time -from typing import Optional, Callable, Union +from typing import Optional from threading import Event -from datetime import datetime, timedelta from os.path import isfile, join, expanduser from threading import Thread, Lock from ovos_config.config import Configuration from ovos_config.locations import get_xdg_data_save_path, get_xdg_config_save_path -from ovos_utils.log import LOG, log_deprecation, deprecated +from ovos_utils.log import LOG, log_deprecation from ovos_utils.messagebus import FakeBus from ovos_utils.events import create_basic_wrapper from ovos_utils.events import EventContainer as _EventContainer @@ -99,12 +97,15 @@ def __init__(self, bus, self.bus.on('mycroft.scheduler.get_event', self.get_event_handler) + self._running = Event() self._stopping = Event() if autostart: self.start() + self._running.wait(10) else: - # Not running + # Explicitly define event states self._stopping.set() + self._running.clear() @property def is_running(self) -> bool: @@ -145,6 +146,7 @@ def run(self): """ LOG.info("EventScheduler Started") self._stopping.clear() + self._running.set() while not self._stopping.wait(0.5): try: self.check_state() @@ -329,18 +331,25 @@ def shutdown(self): """ Stop the running thread. """ - self._stopping.set() - # Remove listeners - self.bus.remove_all_listeners('mycroft.scheduler.schedule_event') - self.bus.remove_all_listeners('mycroft.scheduler.remove_event') - self.bus.remove_all_listeners('mycroft.scheduler.update_event') - # Wait for thread to finish - self.join() - # Prune event list in preparation for saving - self.clear_repeating() - self.clear_empty() - # Store all pending scheduled events - self.store() + try: + self._stopping.set() + # Remove listeners + self.bus.remove_all_listeners('mycroft.scheduler.schedule_event') + self.bus.remove_all_listeners('mycroft.scheduler.remove_event') + self.bus.remove_all_listeners('mycroft.scheduler.update_event') + # Wait for thread to finish + self.join(30) + # Prune event list in preparation for saving + self.clear_repeating() + self.clear_empty() + # Store all pending scheduled events + self.store() + self._running.clear() + except Exception as e: + self._running.clear() + if not isinstance(e, OSError): + self.store() + raise e class EventContainer(_EventContainer): diff --git a/test/unittests/test_util.py b/test/unittests/test_util.py index 491619a..2578863 100644 --- a/test/unittests/test_util.py +++ b/test/unittests/test_util.py @@ -49,6 +49,14 @@ def test_00_init(self): self.scheduler._stopping.set() self.assertFalse(self.scheduler.is_running) + def test_scheduler_init(self): + from ovos_bus_client.util.scheduler import EventScheduler + scheduler = EventScheduler(self.bus, self.test_schedule_name) + self.assertTrue(scheduler.is_running) + self.assertEqual(scheduler.bus, self.bus) + self.assertNotEqual(scheduler, self.scheduler) + scheduler.shutdown() + class TestUtils(unittest.TestCase): def test_create_echo_function(self): From 7f2ba4a75ee850b73580c859edde8cb0379b909c Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Tue, 29 Aug 2023 21:34:19 +0000 Subject: [PATCH 11/76] Increment Version to 0.0.6a4 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index c3a3e82..8f5c2f6 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK From 212699a6a60c2e57164214e7aaeb8e439a466a5d Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Tue, 29 Aug 2023 21:34:45 +0000 Subject: [PATCH 12/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9004f7e..a9cb79f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a3](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a3) (2023-08-07) +## [0.0.6a4](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a4) (2023-08-29) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a2...0.0.6a3) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a3...0.0.6a4) + +**Merged pull requests:** + +- Update EventScheduler init for backwards-compat. [\#46](https://github.com/OpenVoiceOS/ovos-bus-client/pull/46) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.6a3](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a3) (2023-08-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a2...V0.0.6a3) **Merged pull requests:** From 2e194b9fae71b48c7080802fc01b1f3bda5199af Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:31:44 +0100 Subject: [PATCH 13/76] add site_id (#47) --- ovos_bus_client/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 74ad1af..2da21ab 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -272,7 +272,8 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, max_time: int = 5, max_messages: int = 5, utterance_states: dict = None, lang: str = None, context: IntentContextManager = None, - valid_langs: List[str] = None): + valid_langs: List[str] = None, + site_id: str = "unknown"): """ Construct a session identifier @param session_id: string UUID for the session @@ -288,6 +289,7 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, """ self.session_id = session_id or str(uuid4()) self.lang = lang or get_default_lang() + self.site_id = site_id or "unknown" # indoors placement info self.valid_languages = valid_langs or _get_valid_langs() self.active_skills = active_skills or [] # [skill_id , timestamp] From 24deef6e589ea79fb6400a6f48c69a9eec43d1bb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 12 Sep 2023 14:32:02 +0000 Subject: [PATCH 14/76] Increment Version to 0.0.6a5 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 8f5c2f6..99472d2 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 4 +VERSION_ALPHA = 5 # END_VERSION_BLOCK From 26806ed6ac88ab97425250bfc157193f27d8e032 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 12 Sep 2023 14:32:32 +0000 Subject: [PATCH 15/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9cb79f..59ac475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a4](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a4) (2023-08-29) +## [0.0.6a5](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a5) (2023-09-12) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a3...0.0.6a4) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a4...0.0.6a5) + +**Implemented enhancements:** + +- add site\_id [\#47](https://github.com/OpenVoiceOS/ovos-bus-client/pull/47) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a4](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a4) (2023-08-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a3...V0.0.6a4) **Merged pull requests:** From b585d2cf204fc8567eb57eb511ff9045cacb5006 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:38:47 +0100 Subject: [PATCH 16/76] hotfix/site_id_seriallization (#48) --- ovos_bus_client/session.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 2da21ab..32ef828 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -407,7 +407,8 @@ def serialize(self) -> dict: "history": self.history, "lang": self.lang, "valid_languages": self.valid_languages, - "context": self.context.serialize() + "context": self.context.serialize(), + "site_id": self.site_id } def update_history(self, message: Message = None): @@ -448,6 +449,7 @@ def deserialize(data: dict): lang = data.get("lang") valid_langs = data.get("valid_languages") or _get_valid_langs() context = IntentContextManager.deserialize(data.get("context", {})) + site_id = data.get("site_id", "unknown") return Session(uid, active_skills=active, utterance_states=states, @@ -456,7 +458,8 @@ def deserialize(data: dict): lang=lang, valid_langs=valid_langs, max_messages=max_messages, - context=context) + context=context, + site_id=site_id) @staticmethod def from_message(message: Message = None): From 5bfc5c6dee073708f5d1dec76585b15edca4bc9d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 14 Sep 2023 19:39:08 +0000 Subject: [PATCH 17/76] Increment Version to 0.0.6a6 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 99472d2..0ff3acc 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 5 +VERSION_ALPHA = 6 # END_VERSION_BLOCK From 9b5006ade629fe4158e3990e99e22903e92a845e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 14 Sep 2023 19:39:36 +0000 Subject: [PATCH 18/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ac475..24408c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a5](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a5) (2023-09-12) +## [0.0.6a6](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a6) (2023-09-14) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a4...0.0.6a5) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a5...0.0.6a6) + +**Fixed bugs:** + +- hotfix/site\_id\_seriallization [\#48](https://github.com/OpenVoiceOS/ovos-bus-client/pull/48) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a5](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a5) (2023-09-12) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a4...V0.0.6a5) **Implemented enhancements:** From 416f0b2795ea574479ad49d0e5830a638cf00795 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:09:13 +0100 Subject: [PATCH 19/76] feat/pipeline_session (#49) --- ovos_bus_client/session.py | 39 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 32ef828..4a8fefd 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -1,14 +1,15 @@ import enum -import time import json +import time from threading import Lock -from uuid import uuid4 from typing import Optional, List, Tuple, Union, Iterable +from uuid import uuid4 -from ovos_bus_client.message import dig_for_message, Message -from ovos_utils.log import LOG, log_deprecation from ovos_config.config import Configuration from ovos_config.locale import get_default_lang +from ovos_utils.log import LOG, log_deprecation + +from ovos_bus_client.message import dig_for_message, Message class UtteranceState(str, enum.Enum): @@ -95,7 +96,7 @@ class IntentContextManager: def __init__(self, timeout: int = None, frame_stack: List[Tuple[IntentContextManagerFrame, - float]] = None, + float]] = None, greedy: bool = None, keywords: List[str] = None, max_frames: int = None): @@ -273,7 +274,8 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, utterance_states: dict = None, lang: str = None, context: IntentContextManager = None, valid_langs: List[str] = None, - site_id: str = "unknown"): + site_id: str = "unknown", + pipeline: List[str] = None): """ Construct a session identifier @param session_id: string UUID for the session @@ -299,12 +301,20 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, self.max_time = max_time # minutes self.max_messages = max_messages self.touch_time = int(time.time()) - if expiration_seconds is None: - expiration_seconds = Configuration().get('session', {}).get("ttl", - -1) - self.expiration_seconds = expiration_seconds - self.context = context or IntentContextManager(timeout=self.touch_time + - expiration_seconds) + self.expiration_seconds = expiration_seconds or \ + Configuration().get('session', {}).get("ttl", -1) + self.pipeline = pipeline or Configuration().get('intents', {}).get("pipeline") or [ + "converse", + "padatious_high", + "adapt", + "common_qa", + "fallback_high", + "padatious_medium", + "fallback_medium", + "padatious_low", + "fallback_low" + ] + self.context = context or IntentContextManager(timeout=self.touch_time + self.expiration_seconds) @property def active(self) -> bool: @@ -408,7 +418,8 @@ def serialize(self) -> dict: "lang": self.lang, "valid_languages": self.valid_languages, "context": self.context.serialize(), - "site_id": self.site_id + "site_id": self.site_id, + "pipeline": self.pipeline } def update_history(self, message: Message = None): @@ -450,6 +461,7 @@ def deserialize(data: dict): valid_langs = data.get("valid_languages") or _get_valid_langs() context = IntentContextManager.deserialize(data.get("context", {})) site_id = data.get("site_id", "unknown") + pipeline = data.get("pipeline", []) return Session(uid, active_skills=active, utterance_states=states, @@ -459,6 +471,7 @@ def deserialize(data: dict): valid_langs=valid_langs, max_messages=max_messages, context=context, + pipeline=pipeline, site_id=site_id) @staticmethod From efaf4a88bc1ddc71577fa2f1d7ba11a7d85d7a54 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 22 Sep 2023 20:09:32 +0000 Subject: [PATCH 20/76] Increment Version to 0.0.6a7 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 0ff3acc..2bdd5d7 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 6 +VERSION_ALPHA = 7 # END_VERSION_BLOCK From b269b20fb5a0b9c0b252e54f1be824671e1a5cfb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 22 Sep 2023 20:10:02 +0000 Subject: [PATCH 21/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24408c9..b4572a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a6](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a6) (2023-09-14) +## [0.0.6a7](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a7) (2023-09-22) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a5...0.0.6a6) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a6...0.0.6a7) + +**Implemented enhancements:** + +- feat/pipeline\_session [\#49](https://github.com/OpenVoiceOS/ovos-bus-client/pull/49) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a6](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a6) (2023-09-14) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a5...V0.0.6a6) **Fixed bugs:** From 9b0f4220031cdf4b548dbd98bee53f7b7baa045d Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:11:23 +0100 Subject: [PATCH 22/76] feat/session_per_bus_connection (#50) --- ovos_bus_client/client/client.py | 26 +++++++-- ovos_bus_client/session.py | 91 +++++++++++++------------------- test/unittests/test_session.py | 16 ++---- 3 files changed, 64 insertions(+), 69 deletions(-) diff --git a/ovos_bus_client/client/client.py b/ovos_bus_client/client/client.py index bfc0578..3185674 100644 --- a/ovos_bus_client/client/client.py +++ b/ovos_bus_client/client/client.py @@ -42,7 +42,7 @@ class MessageBusClient(_MessageBusClientBase): _config_cache = None def __init__(self, host=None, port=None, route=None, ssl=None, - emitter=None, cache=False): + emitter=None, cache=False, session=None): config_overrides = dict(host=host, port=port, route=route, ssl=ssl) if cache and self._config_cache: config = self._config_cache @@ -59,6 +59,14 @@ def __init__(self, host=None, port=None, route=None, ssl=None, self.connected_event = Event() self.started_running = False self.wrapped_funcs = {} + if session: + SessionManager.update(session) + else: + session = SessionManager.default_session + + self.session_id = session.session_id + self.on("ovos.session.update_default", + self.on_default_session_update) @staticmethod def build_url(host: str, port: int, route: str, ssl: bool) -> str: @@ -88,6 +96,7 @@ def on_open(self, *args): self.emitter.emit("open") # Restore reconnect timer to 5 seconds on sucessful connect self.retry = 5 + self.emit(Message("ovos.session.sync")) # request default session update def on_close(self, *args): """ @@ -140,10 +149,19 @@ def on_message(self, *args): else: message = args[1] parsed_message = Message.deserialize(message) - SessionManager.update(Session.from_message(parsed_message)) + sess = Session.from_message(parsed_message) + if sess.session_id != "default": + # 'default' can only be updated by core + SessionManager.update(sess) self.emitter.emit('message', message) self.emitter.emit(parsed_message.msg_type, parsed_message) + def on_default_session_update(self, message): + new_session = message.data["session_data"] + sess = Session.deserialize(new_session) + SessionManager.update(sess, make_default=True) + LOG.debug("synced default_session") + def emit(self, message: Message): """ Send a message onto the message bus. @@ -155,9 +173,11 @@ def emit(self, message: Message): message (Message): Message to send """ if "session" not in message.context: - sess = SessionManager.get(message) + sess = SessionManager.sessions.get(self.session_id) or \ + SessionManager.default_session message.context["session"] = sess.serialize() sess.update_history(message) + sess.touch() if not self.connected_event.wait(10): if not self.started_running: diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 4a8fefd..ae97858 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -5,12 +5,11 @@ from typing import Optional, List, Tuple, Union, Iterable from uuid import uuid4 +from ovos_bus_client.message import dig_for_message, Message from ovos_config.config import Configuration from ovos_config.locale import get_default_lang from ovos_utils.log import LOG, log_deprecation -from ovos_bus_client.message import dig_for_message, Message - class UtteranceState(str, enum.Enum): INTENT = "intent" # includes converse @@ -484,47 +483,46 @@ def from_message(message: Message = None): @return: Session object """ message = message or dig_for_message() - if message: + if message and "session" in message.context: lang = message.context.get("lang") or \ message.data.get("lang") - sid = None - if "session_id" in message.context: - sid = message.context["session_id"] - if "session" in message.context: - sess = message.context["session"] - if sid and "session_id" not in sess: - sess["session_id"] = sid - if "lang" not in sess: - sess["lang"] = lang - sess = Session.deserialize(sess) - elif sid: - sess = SessionManager.sessions.get(sid) or \ - Session(sid) - if lang: - sess.lang = lang - else: - sess = SessionManager.default_session - if not sess: - LOG.debug(f"Creating default session on reference") - sess = SessionManager.reset_default_session() - if sess and lang and sess.lang != lang: - sess.lang = lang - LOG.info(f"Updated default session lang to: {lang}") + sess = message.context["session"] + if "lang" not in sess: + sess["lang"] = lang + sess = Session.deserialize(sess) else: # new session LOG.warning(f"No message found, using default session") sess = SessionManager.default_session if sess and sess.expired(): - LOG.debug(f"Resolved session expired {sess.session_id}") - sess.touch() + LOG.debug(f"unexpiring session {sess.session_id}") return sess class SessionManager: """ Keeps track of the current active session. """ - default_session: Session = None + default_session: Session = Session("default") __lock = Lock() - sessions = {} + sessions = {"default": default_session} + bus = None + + @classmethod + def sync(cls, message=None): + if cls.bus: + message = message or Message("ovos.session.sync") + cls.bus.emit(message.reply("ovos.session.update_default", + {"session_data": cls.default_session.serialize()})) + + @classmethod + def connect_to_bus(cls, bus): + cls.bus = bus + cls.bus.on("ovos.session.sync", + cls.handle_default_session_request) + cls.sync() + + @classmethod + def handle_default_session_request(cls, message=None): + cls.sync(message) @staticmethod def prune_sessions(): @@ -545,17 +543,10 @@ def reset_default_session() -> Session: Define and return a new default_session """ with SessionManager.__lock: - sess = Session() - LOG.info(f"New Default Session Start: {sess.session_id}") - if not SessionManager.default_session: - SessionManager.default_session = sess - if SessionManager.default_session.session_id in \ - SessionManager.sessions: - LOG.debug(f"Removing expired default session from sessions") - SessionManager.sessions.pop( - SessionManager.default_session.session_id) - SessionManager.default_session = sess - SessionManager.sessions[sess.session_id] = sess + sess = Session("default") + LOG.info(f"Default Session reset") + SessionManager.default_session = SessionManager.sessions["default"] = sess + SessionManager.sync() return SessionManager.default_session @staticmethod @@ -568,9 +559,13 @@ def update(sess: Session, make_default: bool = False): if not sess: raise ValueError(f"Expected Session and got None") sess.touch() - SessionManager.sessions[sess.session_id] = sess if make_default: + sess.session_id = "default" + LOG.debug(f"replacing default session with: {sess.serialize()}") SessionManager.default_session = sess + else: + LOG.debug(f"session updated: {sess.session_id}") + SessionManager.sessions[sess.session_id] = sess @staticmethod def get(message: Optional[Message] = None) -> Session: @@ -590,20 +585,10 @@ def get(message: Optional[Message] = None) -> Session: SessionManager.sessions[msg_sess.session_id] = msg_sess return msg_sess else: - LOG.debug(f"No session from message.") + LOG.debug(f"No session from message, use default session") else: LOG.debug(f"No message, use default session") - # Default session, check if it needs to be (re)-created - if not sess or sess.expired(): - if sess is not None and sess.session_id in SessionManager.sessions: - LOG.debug(f"Removing expired default: {sess.session_id}") - SessionManager.sessions.pop(sess.session_id) - sess = SessionManager.reset_default_session() - else: - # Existing default, make sure lang is in sync with Configuration - sess.lang = Configuration().get('lang') or sess.lang - return sess @staticmethod diff --git a/test/unittests/test_session.py b/test/unittests/test_session.py index 7d5b1a9..a8487e2 100644 --- a/test/unittests/test_session.py +++ b/test/unittests/test_session.py @@ -262,19 +262,9 @@ def test_update(self): # TODO pass - @patch("ovos_bus_client.session.Configuration") - def test_get(self, config): - config.return_value = {'lang': 'en-us'} - self.assertEqual(config(), {'lang': 'en-us'}) - from ovos_bus_client.session import Session - session = self.SessionManager.get() - self.assertIsInstance(session, Session) - self.assertEqual(session.lang, 'en-us') - config.return_value = {'lang': 'es-es'} - - session = self.SessionManager.get() - self.assertIsInstance(session, Session) - self.assertEqual(session.lang, 'es-es') + def test_get(self): + # TODO - rewrite test, .get has no side effects now, lang update happens in ovos-core + pass def test_touch(self): # TODO From b905bc5a0a9f680c32058849c795d635d90e76ee Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Sep 2023 16:11:43 +0000 Subject: [PATCH 23/76] Increment Version to 0.0.6a8 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 2bdd5d7..fea9716 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 7 +VERSION_ALPHA = 8 # END_VERSION_BLOCK From 6bf4c7dcb7ef3047c432619e9d92230476c51a7e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Sep 2023 16:12:09 +0000 Subject: [PATCH 24/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4572a6..8e20b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a7](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a7) (2023-09-22) +## [0.0.6a8](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a8) (2023-09-29) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a6...0.0.6a7) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a7...0.0.6a8) + +**Implemented enhancements:** + +- feat/session\_per\_bus\_connection [\#50](https://github.com/OpenVoiceOS/ovos-bus-client/pull/50) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a7](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a7) (2023-09-22) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a6...V0.0.6a7) **Implemented enhancements:** From e5371a5c93a1c03919031cc63e821eb2ad096be5 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 30 Sep 2023 04:24:10 +0100 Subject: [PATCH 25/76] fix/session manager update (#51) --- ovos_bus_client/session.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index ae97858..8ae41d3 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -329,6 +329,7 @@ def touch(self): update the touch_time on the session """ self.touch_time = int(time.time()) + SessionManager.update(self) def expired(self) -> bool: """ @@ -347,6 +348,7 @@ def enable_response_mode(self, skill_id: str): @param skill_id: ID of skill expecting a response """ self.utterance_states[skill_id] = UtteranceState.RESPONSE.value + SessionManager.update(self) def disable_response_mode(self, skill_id: str): """ @@ -354,6 +356,7 @@ def disable_response_mode(self, skill_id: str): @param skill_id: ID of skill expecting a response """ self.utterance_states[skill_id] = UtteranceState.INTENT.value + SessionManager.update(self) def activate_skill(self, skill_id: str): """ @@ -364,6 +367,7 @@ def activate_skill(self, skill_id: str): self.deactivate_skill(skill_id) # add skill with timestamp to start of active list self.active_skills.insert(0, [skill_id, time.time()]) + SessionManager.update(self) def deactivate_skill(self, skill_id: str): """ @@ -374,6 +378,7 @@ def deactivate_skill(self, skill_id: str): if skill_id in active_ids: idx = active_ids.index(skill_id) self.active_skills.pop(idx) + SessionManager.update(self) def is_active(self, skill_id: str) -> bool: """ @@ -403,6 +408,7 @@ def clear(self): """ self.active_skills = [] self.history = [] + SessionManager.update(self) def serialize(self) -> dict: """ @@ -442,6 +448,7 @@ def update_history(self, message: Message = None): m["context"] = {} # clear personal data self.history.append((m, time.time())) self._prune_history() + SessionManager.update(self) @staticmethod def deserialize(data: dict): @@ -558,13 +565,15 @@ def update(sess: Session, make_default: bool = False): """ if not sess: raise ValueError(f"Expected Session and got None") - sess.touch() + if make_default: sess.session_id = "default" LOG.debug(f"replacing default session with: {sess.serialize()}") - SessionManager.default_session = sess else: LOG.debug(f"session updated: {sess.session_id}") + + if sess.session_id == "default": + SessionManager.default_session = sess SessionManager.sessions[sess.session_id] = sess @staticmethod @@ -598,4 +607,5 @@ def touch(message: Message = None): @param message: Message to get Session for to update """ - SessionManager.get(message).touch() + sess = SessionManager.get(message) + sess.touch() From 5c4285130f9941b767f4d77d01bff8d13cc6f8ce Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 30 Sep 2023 03:24:27 +0000 Subject: [PATCH 26/76] Increment Version to 0.0.6a9 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index fea9716..c94784b 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 8 +VERSION_ALPHA = 9 # END_VERSION_BLOCK From bfeb6fab77af0e4505402fd723468c84803c4d55 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 30 Sep 2023 03:24:59 +0000 Subject: [PATCH 27/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e20b2c..3d87fc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a8](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a8) (2023-09-29) +## [0.0.6a9](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a9) (2023-09-30) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a7...0.0.6a8) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a8...0.0.6a9) + +**Fixed bugs:** + +- fix/session manager update [\#51](https://github.com/OpenVoiceOS/ovos-bus-client/pull/51) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a8](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a8) (2023-09-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a7...V0.0.6a8) **Implemented enhancements:** From ca7519bc40ea0af57f0acec9bc570e5282279c3a Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 3 Oct 2023 19:47:42 +0100 Subject: [PATCH 28/76] refactor/simplify session (#53) --- ovos_bus_client/client/client.py | 2 +- ovos_bus_client/session.py | 83 +++++++++----------------------- test/unittests/test_message.py | 4 +- test/unittests/test_session.py | 13 ----- 4 files changed, 26 insertions(+), 76 deletions(-) diff --git a/ovos_bus_client/client/client.py b/ovos_bus_client/client/client.py index 3185674..df5872c 100644 --- a/ovos_bus_client/client/client.py +++ b/ovos_bus_client/client/client.py @@ -176,7 +176,7 @@ def emit(self, message: Message): sess = SessionManager.sessions.get(self.session_id) or \ SessionManager.default_session message.context["session"] = sess.serialize() - sess.update_history(message) + sess.touch() if not self.connected_event.wait(10): diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 8ae41d3..f6984ff 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -1,14 +1,14 @@ import enum -import json import time from threading import Lock from typing import Optional, List, Tuple, Union, Iterable from uuid import uuid4 -from ovos_bus_client.message import dig_for_message, Message from ovos_config.config import Configuration from ovos_config.locale import get_default_lang -from ovos_utils.log import LOG, log_deprecation +from ovos_utils.log import LOG + +from ovos_bus_client.message import dig_for_message, Message class UtteranceState(str, enum.Enum): @@ -268,8 +268,7 @@ def get_context(self, max_frames: int = None, class Session: def __init__(self, session_id: str = None, expiration_seconds: int = None, active_skills: List[List[Union[str, float]]] = None, - history: List[Tuple[Message, float]] = None, - max_time: int = 5, max_messages: int = 5, + history=None, max_time=None, max_messages=None, utterance_states: dict = None, lang: str = None, context: IntentContextManager = None, valid_langs: List[str] = None, @@ -280,9 +279,9 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, @param session_id: string UUID for the session @param expiration_seconds: TTL for session (-1 for no expiration) @param active_skills: List of list skill_id, last reference - @param history: List of tuple Message, timestamp - @param max_time: maximum time for history in minutes - @param max_messages: maximum number of messages to retain + @param history: DEPRECATED + @param max_time: DEPRECATED + @param max_messages: DEPRECATED @param utterance_states: dict of skill_id to UtteranceState @param lang: language associated with this Session @param context: IntentContextManager for this Session @@ -293,12 +292,9 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, self.site_id = site_id or "unknown" # indoors placement info self.valid_languages = valid_langs or _get_valid_langs() - self.active_skills = active_skills or [] # [skill_id , timestamp] - # TODO: Can active_skills be a list of tuples like history? - self.history = history or [] # (Message , timestamp) + self.active_skills = active_skills or [] # [skill_id , timestamp]# (Message , timestamp) self.utterance_states = utterance_states or {} # {skill_id: UtteranceState} - self.max_time = max_time # minutes - self.max_messages = max_messages + self.touch_time = int(time.time()) self.expiration_seconds = expiration_seconds or \ Configuration().get('session', {}).get("ttl", -1) @@ -315,6 +311,13 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, ] self.context = context or IntentContextManager(timeout=self.touch_time + self.expiration_seconds) + # deprecated - TODO remove 0.0.8 + if history is not None or max_time is not None or max_messages is not None: + LOG.warning("history, max_time and max_messages have been deprecated") + self.history = [] # (Message , timestamp) + self.max_time = 5 # minutes + self.max_messages = 5 + @property def active(self) -> bool: """ @@ -348,7 +351,7 @@ def enable_response_mode(self, skill_id: str): @param skill_id: ID of skill expecting a response """ self.utterance_states[skill_id] = UtteranceState.RESPONSE.value - SessionManager.update(self) + self.touch() def disable_response_mode(self, skill_id: str): """ @@ -356,7 +359,7 @@ def disable_response_mode(self, skill_id: str): @param skill_id: ID of skill expecting a response """ self.utterance_states[skill_id] = UtteranceState.INTENT.value - SessionManager.update(self) + self.touch() def activate_skill(self, skill_id: str): """ @@ -367,7 +370,7 @@ def activate_skill(self, skill_id: str): self.deactivate_skill(skill_id) # add skill with timestamp to start of active list self.active_skills.insert(0, [skill_id, time.time()]) - SessionManager.update(self) + self.touch() def deactivate_skill(self, skill_id: str): """ @@ -378,7 +381,7 @@ def deactivate_skill(self, skill_id: str): if skill_id in active_ids: idx = active_ids.index(skill_id) self.active_skills.pop(idx) - SessionManager.update(self) + self.touch() def is_active(self, skill_id: str) -> bool: """ @@ -386,29 +389,15 @@ def is_active(self, skill_id: str) -> bool: @param skill_id: ID of skill to check @return: True if the requested skill is active """ - self._prune_history() # TODO: Is this necessary or useful? active_ids = [s[0] for s in self.active_skills] return skill_id in active_ids - def _prune_history(self): - """ - Remove messages from history that are too old or exceed max_messages - """ - # filter old messages from history - now = time.time() - self.history = [m for m in self.history - if now - m[1] < 60 * self.max_time] - # keep only self.max_messages - if len(self.history) > self.max_messages: - self.history = self.history[self.max_messages * -1:] - def clear(self): """ - Clear history and active_skills + Clear active_skills """ self.active_skills = [] - self.history = [] - SessionManager.update(self) + self.touch() def serialize(self) -> dict: """ @@ -419,7 +408,6 @@ def serialize(self) -> dict: "active_skills": self.active_skills, "utterance_states": self.utterance_states, "session_id": self.session_id, - "history": self.history, "lang": self.lang, "valid_languages": self.valid_languages, "context": self.context.serialize(), @@ -432,23 +420,8 @@ def update_history(self, message: Message = None): Add a message to history and then prune history @param message: Message to append to history """ - message = message or dig_for_message() - if message: - try: - m = message.as_dict - except AttributeError: - log_deprecation("mycroft-bus-client has been deprecated, please" - " update your imports to use ovos-bus-client", - "0.0.4", - excluded_package_refs=["ovos_bus_client"]) - m = json.loads(message.serialize()) - except Exception as e: - LOG.exception(e) - m = json.loads(message.serialize()) - m["context"] = {} # clear personal data - self.history.append((m, time.time())) - self._prune_history() - SessionManager.update(self) + LOG.warning("update_history has been deprecated, " + "session no longer has a message history") @staticmethod def deserialize(data: dict): @@ -459,9 +432,6 @@ def deserialize(data: dict): """ uid = data.get("session_id") active = data.get("active_skills") or [] - history = data.get("history") or [] - max_time = data.get("max_time") or 5 - max_messages = data.get("max_messages") or 5 states = data.get("utterance_states") or {} lang = data.get("lang") valid_langs = data.get("valid_languages") or _get_valid_langs() @@ -471,11 +441,8 @@ def deserialize(data: dict): return Session(uid, active_skills=active, utterance_states=states, - history=history, - max_time=max_time, lang=lang, valid_langs=valid_langs, - max_messages=max_messages, context=context, pipeline=pipeline, site_id=site_id) @@ -569,8 +536,6 @@ def update(sess: Session, make_default: bool = False): if make_default: sess.session_id = "default" LOG.debug(f"replacing default session with: {sess.serialize()}") - else: - LOG.debug(f"session updated: {sess.session_id}") if sess.session_id == "default": SessionManager.default_session = sess diff --git a/test/unittests/test_message.py b/test/unittests/test_message.py index 40eae8c..33d51be 100644 --- a/test/unittests/test_message.py +++ b/test/unittests/test_message.py @@ -39,9 +39,7 @@ def test_serialize_deserialize(self): def test_session_serialize_deserialize(self): """Assert that a serized message is recreated when deserialized.""" s = Session() - for i in range(3): - s.update_history(Message(f'test_type_{i}', - context={"session": s.serialize()})) + SessionManager.update(s, make_default=True) source = Message('test_type', diff --git a/test/unittests/test_session.py b/test/unittests/test_session.py index a8487e2..29f25c8 100644 --- a/test/unittests/test_session.py +++ b/test/unittests/test_session.py @@ -140,10 +140,7 @@ def test_init(self): self.assertIsInstance(session.lang, str) self.assertIsInstance(session.valid_languages, list) self.assertEqual(session.active_skills, list()) - self.assertEqual(session.history, list()) self.assertEqual(session.utterance_states, dict()) - self.assertIsInstance(session.max_time, int) - self.assertIsInstance(session.max_messages, int) self.assertIsInstance(session.touch_time, int) self.assertIsInstance(session.expiration_seconds, int) self.assertIsInstance(session.context, IntentContextManager) @@ -194,10 +191,6 @@ def test_is_active(self): # TODO pass - def test_prune_history(self): - # TODO - pass - def test_clear(self): # TODO pass @@ -225,9 +218,7 @@ def test_serialize_deserialize(self): self.assertIsInstance(test_session.lang, str) self.assertIsInstance(test_session.valid_languages, list) self.assertIsInstance(test_session.active_skills, list) - self.assertIsInstance(test_session.history, list) self.assertIsInstance(test_session.utterance_states, dict) - self.assertIsInstance(test_session.max_time, int) self.assertIsInstance(test_session.touch_time, int) self.assertIsInstance(test_session.expiration_seconds, int) self.assertIsInstance(test_session.context, IntentContextManager) @@ -235,10 +226,6 @@ def test_serialize_deserialize(self): self.assertIsInstance(serialized, dict) self.assertIsInstance(serialized['context'], dict) - def test_update_history(self): - # TODO - pass - def test_from_message(self): # TODO pass From a9bd18b0433ff1ccbeb90eaa6abf245d40a3f232 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 3 Oct 2023 18:48:03 +0000 Subject: [PATCH 29/76] Increment Version to 0.0.6a10 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index c94784b..4f6cb4e 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 9 +VERSION_ALPHA = 10 # END_VERSION_BLOCK From 0ad0039347bfd2e4bc08de107a8602c7a951bcd1 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 3 Oct 2023 18:48:33 +0000 Subject: [PATCH 30/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d87fc6..0754369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a9](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a9) (2023-09-30) +## [0.0.6a10](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a10) (2023-10-03) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a8...0.0.6a9) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a9...0.0.6a10) + +**Merged pull requests:** + +- refactor/simplify session [\#53](https://github.com/OpenVoiceOS/ovos-bus-client/pull/53) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a9](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a9) (2023-09-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a8...V0.0.6a9) **Fixed bugs:** From 8dfaecd58ad023e128a4206d108e778f2e4fb5f5 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 3 Oct 2023 21:51:10 +0100 Subject: [PATCH 31/76] fix/default_session_overwrite (#54) --- ovos_bus_client/session.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index f6984ff..bdbeb37 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -556,8 +556,11 @@ def get(message: Optional[Message] = None) -> Session: if message: msg_sess = Session.from_message(message) if msg_sess: - SessionManager.sessions[msg_sess.session_id] = msg_sess - return msg_sess + if msg_sess.session_id == "default": # reserved namespace for ovos-core + LOG.debug(f"message is using default session") + else: + SessionManager.sessions[msg_sess.session_id] = msg_sess + return msg_sess else: LOG.debug(f"No session from message, use default session") else: From 41db5ff514a05e6d5db5a9ef3ecb926bfdc11ff5 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 3 Oct 2023 20:51:30 +0000 Subject: [PATCH 32/76] Increment Version to 0.0.6a11 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 4f6cb4e..f164a9d 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 10 +VERSION_ALPHA = 11 # END_VERSION_BLOCK From 96405feb9d66af8c3d083bf168a909bdd8fce4f7 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 3 Oct 2023 20:51:59 +0000 Subject: [PATCH 33/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0754369..5fc333a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a10](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a10) (2023-10-03) +## [0.0.6a11](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a11) (2023-10-03) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a9...0.0.6a10) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a10...0.0.6a11) + +**Fixed bugs:** + +- fix/default\_session\_overwrite [\#54](https://github.com/OpenVoiceOS/ovos-bus-client/pull/54) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a10](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a10) (2023-10-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a9...V0.0.6a10) **Merged pull requests:** From 6616569267d3bf31e16d632b2f28a400bfa02a09 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:47:14 +0100 Subject: [PATCH 34/76] fix/session_injection (#55) --- ovos_bus_client/client/client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ovos_bus_client/client/client.py b/ovos_bus_client/client/client.py index df5872c..85480a1 100644 --- a/ovos_bus_client/client/client.py +++ b/ovos_bus_client/client/client.py @@ -174,11 +174,9 @@ def emit(self, message: Message): """ if "session" not in message.context: sess = SessionManager.sessions.get(self.session_id) or \ - SessionManager.default_session + Session(self.session_id) message.context["session"] = sess.serialize() - sess.touch() - if not self.connected_event.wait(10): if not self.started_running: raise ValueError('You must execute run_forever() ' From ba8753e8cb571f7e58e1896de116e16ecb0b4790 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 4 Oct 2023 20:47:39 +0000 Subject: [PATCH 35/76] Increment Version to 0.0.6a12 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index f164a9d..1643074 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 11 +VERSION_ALPHA = 12 # END_VERSION_BLOCK From eca8d20b9057b2529db787ffc2f3dc5585772c60 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 4 Oct 2023 20:48:16 +0000 Subject: [PATCH 36/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fc333a..1d52065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a11](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a11) (2023-10-03) +## [0.0.6a12](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a12) (2023-10-04) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a10...0.0.6a11) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a11...0.0.6a12) + +**Fixed bugs:** + +- fix/session\_injection [\#55](https://github.com/OpenVoiceOS/ovos-bus-client/pull/55) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a11](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a11) (2023-10-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a10...V0.0.6a11) **Fixed bugs:** From 7530f7f0cde1525d9fb0f42c7b834f5f71b1c035 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 9 Oct 2023 09:34:15 -0500 Subject: [PATCH 37/76] feat: extend wait_for_response to accept list (#52) --- ovos_bus_client/client/client.py | 16 ++-- ovos_bus_client/client/waiter.py | 17 +++-- test/unittests/test_client.py | 122 +++++++++++++++---------------- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/ovos_bus_client/client/client.py b/ovos_bus_client/client/client.py index 85480a1..718e9e4 100644 --- a/ovos_bus_client/client/client.py +++ b/ovos_bus_client/client/client.py @@ -186,7 +186,7 @@ def emit(self, message: Message): if hasattr(message, 'serialize'): msg = message.serialize() else: - msg = json.dumps(message.__dict__) + msg = json.dumps(message.__dict__) try: self.client.send(msg) except WebSocketConnectionClosedException: @@ -269,7 +269,7 @@ def wait_for_message(self, message_type: str, return MessageWaiter(self, message_type).wait(timeout) def wait_for_response(self, message: Message, - reply_type: Optional[str] = None, + reply_type: Optional[Union[str, List[str]]] = None, timeout: Union[float, int] = 3.0) -> \ Optional[Message]: """ @@ -277,16 +277,22 @@ def wait_for_response(self, message: Message, Arguments: message (Message): message to send - reply_type (str): the message type of the expected reply. + reply_type (str | List[str]): the message type(s) of the expected reply. Defaults to ".response". timeout: seconds to wait before timeout, defaults to 3 Returns: The received message or None if the response timed out """ - message_type = reply_type or message.msg_type + '.response' + message_type = None + if isinstance(reply_type, list): + message_type = reply_type + elif isinstance(reply_type, str): + message_type = [reply_type] + elif reply_type is None: + message_type = [message.msg_type + '.response'] waiter = MessageWaiter(self, message_type) # Setup response handler - # Send message and wait for it's response + # Send message and wait for its response self.emit(message) return waiter.wait(timeout) diff --git a/ovos_bus_client/client/waiter.py b/ovos_bus_client/client/waiter.py index 5b07afe..e12c8cc 100644 --- a/ovos_bus_client/client/waiter.py +++ b/ovos_bus_client/client/waiter.py @@ -12,6 +12,7 @@ # from threading import Event +from typing import List, Union try: from mycroft_bus_client.client.waiter import MessageWaiter as _MessageWaiterBase @@ -28,20 +29,23 @@ class MessageWaiter: """Wait for a single message. Encapsulate the wait for a message logic separating the setup from - the actual waiting act so the waiting can be setuo, actions can be + the actual waiting act so the waiting can be setup, actions can be performed and _then_ the message can be waited for. - Argunments: + Arguments: bus: Bus to check for messages on - message_type: message type to wait for + message_type: message type(s) to wait for """ - def __init__(self, bus, message_type): + def __init__(self, bus, message_type: Union[str, List[str]]): self.bus = bus + if not isinstance(message_type, list): + message_type = [message_type] self.msg_type = message_type self.received_msg = None # Setup response handler self.response_event = Event() - self.bus.once(message_type, self._handler) + for msg in self.msg_type: + self.bus.once(msg, self._handler) def _handler(self, message): """Receive response data.""" @@ -61,7 +65,8 @@ def wait(self, timeout=3.0): if not self.response_event.is_set(): # Clean up the event handler try: - self.bus.remove(self.msg_type, self._handler) + for msg in self.msg_type: + self.bus.remove(msg, self._handler) except (ValueError, KeyError): # ValueError occurs on pyee 5.0.1 removing handlers # registered with once. diff --git a/test/unittests/test_client.py b/test/unittests/test_client.py index 71da8e5..563f005 100644 --- a/test/unittests/test_client.py +++ b/test/unittests/test_client.py @@ -11,7 +11,7 @@ # limitations under the License. import unittest -from unittest.mock import Mock +from unittest.mock import call, Mock, patch from pyee import ExecutorEventEmitter @@ -19,38 +19,34 @@ from ovos_bus_client.client.client import MessageBusClient, GUIWebsocketClient from ovos_bus_client.client import MessageWaiter, MessageCollector -WS_CONF = { - 'websocket': { - "host": "testhost", - "port": 1337, - "route": "/core", - "ssl": False - } -} +WS_CONF = {"websocket": {"host": "testhost", "port": 1337, "route": "/core", "ssl": False}} class TestClient(unittest.TestCase): def test_echo(self): from ovos_bus_client.client.client import echo + # TODO def test_inheritance(self): from mycroft_bus_client.client import MessageBusClient as _Client + self.assertTrue(issubclass(MessageBusClient, _Client)) class TestMessageBusClient(unittest.TestCase): from ovos_bus_client.client.client import MessageBusClient + client = MessageBusClient() def test_build_url(self): - url = MessageBusClient.build_url('localhost', 1337, '/core', False) - self.assertEqual(url, 'ws://localhost:1337/core') - ssl_url = MessageBusClient.build_url('sslhost', 443, '/core', True) - self.assertEqual(ssl_url, 'wss://sslhost:443/core') + url = MessageBusClient.build_url("localhost", 1337, "/core", False) + self.assertEqual(url, "ws://localhost:1337/core") + ssl_url = MessageBusClient.build_url("sslhost", 443, "/core", True) + self.assertEqual(ssl_url, "wss://sslhost:443/core") def test_create_client(self): - self.assertEqual(self.client.client.url, 'ws://0.0.0.0:8181/core') + self.assertEqual(self.client.client.url, "ws://0.0.0.0:8181/core") self.assertIsInstance(self.client.emitter, ExecutorEventEmitter) mock_emitter = Mock() @@ -85,9 +81,25 @@ def test_on_collect(self): # TODO pass - def test_wait_for_message(self): - # TODO - pass + @patch("ovos_bus_client.client.client.MessageWaiter") + def test_wait_for_message_str(self, mock_message_waiter): + # Arrange + test_message = Message("test.message") + self.client.emit = Mock() + # Act + self.client.wait_for_response(test_message) + # Assert + mock_message_waiter.assert_called_once_with(self.client, ["test.message.response"]) + + @patch("ovos_bus_client.client.client.MessageWaiter") + def test_wait_for_message_list(self, mock_message_waiter): + # Arrange + test_message = Message("test.message") + self.client.emit = Mock() + # Act + self.client.wait_for_response(test_message, ["test.message.response", "test.message.response2"]) + # Assert + mock_message_waiter.assert_called_once_with(self.client, ["test.message.response", "test.message.response2"]) def test_wait_for_response(self): # TODO @@ -145,75 +157,63 @@ def test_on_message(self): class TestMessageWaiter: def test_message_wait_success(self): bus = Mock() - waiter = MessageWaiter(bus, 'delayed.message') - bus.once.assert_called_with('delayed.message', waiter._handler) + waiter = MessageWaiter(bus, "delayed.message") + bus.once.assert_called_with("delayed.message", waiter._handler) - test_msg = Mock(name='test_msg') + test_msg = Mock(name="test_msg") waiter._handler(test_msg) # Inject response assert waiter.wait() == test_msg def test_message_wait_timeout(self): bus = Mock() - waiter = MessageWaiter(bus, 'delayed.message') - bus.once.assert_called_with('delayed.message', waiter._handler) + waiter = MessageWaiter(bus, "delayed.message") + bus.once.assert_called_with("delayed.message", waiter._handler) assert waiter.wait(0.3) is None + def test_message_converts_to_list(self): + bus = Mock() + waiter = MessageWaiter(bus, "test.message") + assert isinstance(waiter.msg_type, list) + bus.once.assert_called_with("test.message", waiter._handler) + + def test_multiple_messages(self): + bus = Mock() + waiter = MessageWaiter(bus, ["test.message", "test.message2"]) + bus.once.assert_has_calls([call("test.message", waiter._handler), call("test.message2", waiter._handler)]) + class TestMessageCollector: def test_message_wait_success(self): bus = Mock() - collector = MessageCollector(bus, Message('delayed.message'), - min_timeout=0.0, max_timeout=2.0) - - test_register = Mock(name='test_register') - test_register.data = { - 'query': collector.collect_id, - 'timeout': 5, - 'handler': 'test_handler1' - } + collector = MessageCollector(bus, Message("delayed.message"), min_timeout=0.0, max_timeout=2.0) + + test_register = Mock(name="test_register") + test_register.data = {"query": collector.collect_id, "timeout": 5, "handler": "test_handler1"} collector._register_handler(test_register) # Inject response - test_response = Mock(name='test_register') - test_response.data = { - 'query': collector.collect_id, - 'handler': 'test_handler1' - } + test_response = Mock(name="test_register") + test_response.data = {"query": collector.collect_id, "handler": "test_handler1"} collector._receive_response(test_response) assert collector.collect() == [test_response] def test_message_drop_invalid(self): bus = Mock() - collector = MessageCollector(bus, Message('delayed.message'), - min_timeout=0.0, max_timeout=2.0) - - valid_register = Mock(name='valid_register') - valid_register.data = { - 'query': collector.collect_id, - 'timeout': 5, - 'handler': 'test_handler1' - } - invalid_register = Mock(name='invalid_register') - invalid_register.data = { - 'query': 'asdf', - 'timeout': 5, - 'handler': 'test_handler1' - } + collector = MessageCollector(bus, Message("delayed.message"), min_timeout=0.0, max_timeout=2.0) + + valid_register = Mock(name="valid_register") + valid_register.data = {"query": collector.collect_id, "timeout": 5, "handler": "test_handler1"} + invalid_register = Mock(name="invalid_register") + invalid_register.data = {"query": "asdf", "timeout": 5, "handler": "test_handler1"} collector._register_handler(valid_register) # Inject response collector._register_handler(invalid_register) # Inject response - valid_response = Mock(name='valid_register') - valid_response.data = { - 'query': collector.collect_id, - 'handler': 'test_handler1' - } - invalid_response = Mock(name='invalid_register') - invalid_response.data = { - 'query': 'asdf', - 'handler': 'test_handler1' - } + valid_response = Mock(name="valid_register") + valid_response.data = {"query": collector.collect_id, "handler": "test_handler1"} + invalid_response = Mock(name="invalid_register") + invalid_response.data = {"query": "asdf", "handler": "test_handler1"} collector._receive_response(valid_response) collector._receive_response(invalid_response) assert collector.collect() == [valid_response] From 6b698cfee6b12a7443de0991c208e69705f37089 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Oct 2023 14:34:39 +0000 Subject: [PATCH 38/76] Increment Version to 0.0.6a13 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 1643074..e5f8e9e 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 12 +VERSION_ALPHA = 13 # END_VERSION_BLOCK From e49c451fe3088f0331b3cbeb9a3b4d0e581d929a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Oct 2023 14:35:10 +0000 Subject: [PATCH 39/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d52065..a61863c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a12](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a12) (2023-10-04) +## [0.0.6a13](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a13) (2023-10-09) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a11...0.0.6a12) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a12...0.0.6a13) + +**Implemented enhancements:** + +- feat: extend wait\_for\_response to accept list [\#52](https://github.com/OpenVoiceOS/ovos-bus-client/pull/52) ([mikejgray](https://github.com/mikejgray)) + +## [V0.0.6a12](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a12) (2023-10-04) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a11...V0.0.6a12) **Fixed bugs:** From f3659c2bd7332cc28e3a191c919658fdaf172b79 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:37:21 +0100 Subject: [PATCH 40/76] add simple_cli entrypoint (#56) --- ovos_bus_client/scripts.py | 43 ++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 44 insertions(+) diff --git a/ovos_bus_client/scripts.py b/ovos_bus_client/scripts.py index 78ad421..997d489 100644 --- a/ovos_bus_client/scripts.py +++ b/ovos_bus_client/scripts.py @@ -19,6 +19,8 @@ def ovos_speak(): raise SystemExit(2) client = MessageBusClient() client.run_in_thread() + if not client.connected_event.is_set(): + client.connected_event.wait() client.emit(Message("speak", {"utterance": utt, "lang": lang})) client.close() @@ -36,6 +38,8 @@ def ovos_say_to(): raise SystemExit(2) client = MessageBusClient() client.run_in_thread() + if not client.connected_event.is_set(): + client.connected_event.wait() client.emit(Message("recognizer_loop:utterance", {"utterances": [utt], "lang": lang})) client.close() @@ -43,5 +47,44 @@ def ovos_say_to(): def ovos_listen(): client = MessageBusClient() client.run_in_thread() + if not client.connected_event.is_set(): + client.connected_event.wait() client.emit(Message("mycroft.mic.listen")) client.close() + + +def simple_cli(): + args_count = len(sys.argv) + if args_count == 1: + lang = Configuration().get("lang", "en-us") + elif args_count == 2: + lang = sys.argv[1] + else: + print("USAGE: ovos-simple-cli [lang]") + return + + client = MessageBusClient() + client.run_in_thread() + if not client.connected_event.is_set(): + client.connected_event.wait() + lang = lang or Configuration().get("lang", "en-us") + + from ovos_bus_client.session import SessionManager, Session + sess = SessionManager.default_session + + while True: + try: + utt = input("Say:") + if utt == ":exit": + break + client.emit(Message("recognizer_loop:utterance", + {"utterances": [utt], "lang": lang}, + {"session": sess.serialize()})) + except KeyboardInterrupt: + break + + client.close() + + +if __name__ == "__main__": + simple_cli() \ No newline at end of file diff --git a/setup.py b/setup.py index 80a3209..4005343 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,7 @@ def get_version(): 'ovos-listen=ovos_bus_client.scripts:ovos_listen', 'ovos-speak=ovos_bus_client.scripts:ovos_speak', 'ovos-say-to=ovos_bus_client.scripts:ovos_say_to', + 'ovos-simple-cli=ovos_bus_client.scripts:simple_cli' ] } ) From 0a3b544d343d3f0bf9e3fcc6e0374d5ca700965b Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Oct 2023 18:37:41 +0000 Subject: [PATCH 41/76] Increment Version to 0.0.6a14 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index e5f8e9e..6adb133 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 13 +VERSION_ALPHA = 14 # END_VERSION_BLOCK From d58ac13136ce58617752477da5123df9e88c4298 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Oct 2023 18:38:09 +0000 Subject: [PATCH 42/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a61863c..599fca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a13](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a13) (2023-10-09) +## [0.0.6a14](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a14) (2023-10-16) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a12...0.0.6a13) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a13...0.0.6a14) + +**Implemented enhancements:** + +- add simple\_cli entrypoint [\#56](https://github.com/OpenVoiceOS/ovos-bus-client/pull/56) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a13](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a13) (2023-10-09) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a12...V0.0.6a13) **Implemented enhancements:** From fcc8a418b05303ecbdbf8873ec5a2663997ba251 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:39:14 +0100 Subject: [PATCH 43/76] rm/valid_langs_session (#57) --- ovos_bus_client/session.py | 19 +++++-------------- test/unittests/test_session.py | 33 --------------------------------- 2 files changed, 5 insertions(+), 47 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index bdbeb37..7a8fb94 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -16,11 +16,6 @@ class UtteranceState(str, enum.Enum): RESPONSE = "response" -def _get_valid_langs() -> List[str]: - return list(set([get_default_lang()] + - Configuration().get("secondary_langs", []))) - - class IntentContextManagerFrame: def __init__(self, entities: List[dict] = None, metadata: dict = None): """ @@ -285,13 +280,12 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, @param utterance_states: dict of skill_id to UtteranceState @param lang: language associated with this Session @param context: IntentContextManager for this Session - @param valid_langs: list of configured valid languages + @param valid_langs: DEPRECATED """ self.session_id = session_id or str(uuid4()) self.lang = lang or get_default_lang() self.site_id = site_id or "unknown" # indoors placement info - self.valid_languages = valid_langs or _get_valid_langs() self.active_skills = active_skills or [] # [skill_id , timestamp]# (Message , timestamp) self.utterance_states = utterance_states or {} # {skill_id: UtteranceState} @@ -313,10 +307,12 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, # deprecated - TODO remove 0.0.8 if history is not None or max_time is not None or max_messages is not None: - LOG.warning("history, max_time and max_messages have been deprecated") + LOG.warning("valid_langs , history, max_time and max_messages have been deprecated") self.history = [] # (Message , timestamp) self.max_time = 5 # minutes self.max_messages = 5 + self.valid_languages = list(set([get_default_lang()] + + Configuration().get("secondary_langs", []))) @property def active(self) -> bool: @@ -409,7 +405,6 @@ def serialize(self) -> dict: "utterance_states": self.utterance_states, "session_id": self.session_id, "lang": self.lang, - "valid_languages": self.valid_languages, "context": self.context.serialize(), "site_id": self.site_id, "pipeline": self.pipeline @@ -434,7 +429,6 @@ def deserialize(data: dict): active = data.get("active_skills") or [] states = data.get("utterance_states") or {} lang = data.get("lang") - valid_langs = data.get("valid_languages") or _get_valid_langs() context = IntentContextManager.deserialize(data.get("context", {})) site_id = data.get("site_id", "unknown") pipeline = data.get("pipeline", []) @@ -442,7 +436,6 @@ def deserialize(data: dict): active_skills=active, utterance_states=states, lang=lang, - valid_langs=valid_langs, context=context, pipeline=pipeline, site_id=site_id) @@ -556,9 +549,7 @@ def get(message: Optional[Message] = None) -> Session: if message: msg_sess = Session.from_message(message) if msg_sess: - if msg_sess.session_id == "default": # reserved namespace for ovos-core - LOG.debug(f"message is using default session") - else: + if msg_sess.session_id != "default": # reserved namespace for ovos-core SessionManager.sessions[msg_sess.session_id] = msg_sess return msg_sess else: diff --git a/test/unittests/test_session.py b/test/unittests/test_session.py index 29f25c8..76f8190 100644 --- a/test/unittests/test_session.py +++ b/test/unittests/test_session.py @@ -10,37 +10,6 @@ def test_utterance_state(self): self.assertIsInstance(state, UtteranceState) self.assertIsInstance(state, str) - @patch("ovos_bus_client.session.get_default_lang") - @patch("ovos_bus_client.session.Configuration") - def test_get_valid_langs(self, config, default_lang): - config.return_value = { - "secondary_langs": ["en-us", "es-mx", "fr-ca"] - } - default_lang.return_value = "en-us" - from ovos_bus_client.session import _get_valid_langs - # Test default in secondary - langs = _get_valid_langs() - self.assertIsInstance(langs, list) - self.assertEqual(len(langs), len(set(langs))) - self.assertEqual(set(langs), {"en-us", "es-mx", "fr-ca"}) - - # Test default not in secondary - default_lang.return_value = "pt-pt" - langs = _get_valid_langs() - self.assertIsInstance(langs, list) - self.assertEqual(len(langs), len(set(langs))) - self.assertEqual(set(langs), {"en-us", "es-mx", "fr-ca", "pt-pt"}) - - # Test no secondary - config.return_value = {} - langs = _get_valid_langs() - self.assertEqual(langs, [default_lang.return_value]) - - # Test invalid secondary lang config - config.return_value = {"secondary_langs": None} - with self.assertRaises(TypeError): - _get_valid_langs() - class TestIntentContextManagerFrame(unittest.TestCase): def test_serialize_deserialize(self): @@ -138,7 +107,6 @@ def test_init(self): session = Session() self.assertIsInstance(session.session_id, str) self.assertIsInstance(session.lang, str) - self.assertIsInstance(session.valid_languages, list) self.assertEqual(session.active_skills, list()) self.assertEqual(session.utterance_states, dict()) self.assertIsInstance(session.touch_time, int) @@ -216,7 +184,6 @@ def test_serialize_deserialize(self): self.assertIsInstance(test_session, Session) self.assertIsInstance(test_session.session_id, str) self.assertIsInstance(test_session.lang, str) - self.assertIsInstance(test_session.valid_languages, list) self.assertIsInstance(test_session.active_skills, list) self.assertIsInstance(test_session.utterance_states, dict) self.assertIsInstance(test_session.touch_time, int) From 5e78f83987bae175f0f80abcfee5c44e4c8377ab Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 23 Oct 2023 21:39:31 +0000 Subject: [PATCH 44/76] Increment Version to 0.0.6a15 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 6adb133..6ca1525 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 14 +VERSION_ALPHA = 15 # END_VERSION_BLOCK From 5f2ecec3592d0773208ac0aee75b8e8d051ac489 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 23 Oct 2023 21:40:00 +0000 Subject: [PATCH 45/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 599fca2..3e781f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a14](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a14) (2023-10-16) +## [0.0.6a15](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a15) (2023-10-23) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a13...0.0.6a14) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a14...0.0.6a15) + +**Fixed bugs:** + +- rm/valid\_langs\_session [\#57](https://github.com/OpenVoiceOS/ovos-bus-client/pull/57) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a14](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a14) (2023-10-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a13...V0.0.6a14) **Implemented enhancements:** From fd6f1a684f7fa919664300089dfdb28b95a02ff0 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:25:12 +0100 Subject: [PATCH 46/76] fix/huge_integer_error (#58) --- ovos_bus_client/session.py | 6 +++--- test/unittests/test_session.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 7a8fb94..6f41e76 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -96,7 +96,7 @@ def __init__(self, timeout: int = None, config = Configuration().get('context', {}) if timeout is None: - timeout = config.get('timeout', 2) + timeout = config.get('timeout', 2) * 60 # minutes to seconds if greedy is None: greedy = config.get('greedy', False) if keywords is None: @@ -105,7 +105,7 @@ def __init__(self, timeout: int = None, max_frames = config.get('max_frames', 3) self.frame_stack = frame_stack or [] - self.timeout = timeout * 60 # minutes to seconds + self.timeout = timeout self.context_keywords = keywords self.context_max_frames = max_frames self.context_greedy = greedy @@ -125,7 +125,7 @@ def deserialize(data: dict): @param data: serialized (dict) data @return: IntentContextManager for the specified data """ - timeout = data.get("timeout", 2) + timeout = data.get("timeout", 2 * 60) framestack = [(IntentContextManagerFrame.deserialize(f), t) for (f, t) in data.get("frame_stack", [])] return IntentContextManager(timeout, framestack) diff --git a/test/unittests/test_session.py b/test/unittests/test_session.py index 76f8190..96d6869 100644 --- a/test/unittests/test_session.py +++ b/test/unittests/test_session.py @@ -177,7 +177,7 @@ def test_serialize_deserialize(self): new_ctx = new_serial.pop('context') self.assertEqual(new_serial, serialized) self.assertEqual(ctx['frame_stack'], new_ctx['frame_stack']) - self.assertGreater(new_ctx['timeout'], ctx['timeout']) + self.assertEqual(new_ctx['timeout'], ctx['timeout']) # Test default value deserialize test_session = Session.deserialize(dict()) From 91c27cbdc8f728c87fb4cb7f47be8c44c99a6334 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 23 Oct 2023 22:25:35 +0000 Subject: [PATCH 47/76] Increment Version to 0.0.6a16 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 6ca1525..1ff6a02 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 15 +VERSION_ALPHA = 16 # END_VERSION_BLOCK From 1ea7cce7b44508167d2e7dddf29aec87e2b24b0a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 23 Oct 2023 22:26:05 +0000 Subject: [PATCH 48/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e781f9..d30ebc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a15](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a15) (2023-10-23) +## [0.0.6a16](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a16) (2023-10-23) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a14...0.0.6a15) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a15...0.0.6a16) + +**Fixed bugs:** + +- fix/huge\_integer\_error [\#58](https://github.com/OpenVoiceOS/ovos-bus-client/pull/58) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a15](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a15) (2023-10-23) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a14...V0.0.6a15) **Fixed bugs:** From d00dd55c061598b8808fc0b10e70c4ad8e5b82ee Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 24 Oct 2023 19:07:23 +0100 Subject: [PATCH 49/76] fix/init_of_context_manager (#60) --- ovos_bus_client/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 6f41e76..fc401df 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -303,7 +303,7 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, "padatious_low", "fallback_low" ] - self.context = context or IntentContextManager(timeout=self.touch_time + self.expiration_seconds) + self.context = context or IntentContextManager() # deprecated - TODO remove 0.0.8 if history is not None or max_time is not None or max_messages is not None: From cc723b78725499531175aa578603fa2a4b79318f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 24 Oct 2023 18:07:54 +0000 Subject: [PATCH 50/76] Increment Version to 0.0.6a17 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 1ff6a02..91723b3 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 16 +VERSION_ALPHA = 17 # END_VERSION_BLOCK From 45521eb7e98336decbdde60dcaf46243a4f10ca4 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 24 Oct 2023 18:08:28 +0000 Subject: [PATCH 51/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d30ebc0..9ed0e8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a16](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a16) (2023-10-23) +## [0.0.6a17](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a17) (2023-10-24) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a15...0.0.6a16) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a16...0.0.6a17) + +**Fixed bugs:** + +- fix/init\_of\_context\_manager [\#60](https://github.com/OpenVoiceOS/ovos-bus-client/pull/60) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a16](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a16) (2023-10-23) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a15...V0.0.6a16) **Fixed bugs:** From 678eed7d2ecff5af03f0f0b7a0e2952013e4a6f1 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 24 Oct 2023 22:00:54 +0100 Subject: [PATCH 52/76] remove deprecated (#61) remove deprecated kwargs that were only present during the alpha release cycle while Session was being introduced these need removing before the first stable released including Session --- ovos_bus_client/session.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index fc401df..003947d 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -263,10 +263,8 @@ def get_context(self, max_frames: int = None, class Session: def __init__(self, session_id: str = None, expiration_seconds: int = None, active_skills: List[List[Union[str, float]]] = None, - history=None, max_time=None, max_messages=None, utterance_states: dict = None, lang: str = None, context: IntentContextManager = None, - valid_langs: List[str] = None, site_id: str = "unknown", pipeline: List[str] = None): """ @@ -274,13 +272,9 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, @param session_id: string UUID for the session @param expiration_seconds: TTL for session (-1 for no expiration) @param active_skills: List of list skill_id, last reference - @param history: DEPRECATED - @param max_time: DEPRECATED - @param max_messages: DEPRECATED @param utterance_states: dict of skill_id to UtteranceState @param lang: language associated with this Session @param context: IntentContextManager for this Session - @param valid_langs: DEPRECATED """ self.session_id = session_id or str(uuid4()) self.lang = lang or get_default_lang() @@ -305,15 +299,6 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, ] self.context = context or IntentContextManager() - # deprecated - TODO remove 0.0.8 - if history is not None or max_time is not None or max_messages is not None: - LOG.warning("valid_langs , history, max_time and max_messages have been deprecated") - self.history = [] # (Message , timestamp) - self.max_time = 5 # minutes - self.max_messages = 5 - self.valid_languages = list(set([get_default_lang()] + - Configuration().get("secondary_langs", []))) - @property def active(self) -> bool: """ From 1dcde555f788e1dcc88bfb32787c4e40f4d14149 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 24 Oct 2023 21:01:12 +0000 Subject: [PATCH 53/76] Increment Version to 0.0.6a18 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 91723b3..dac7635 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 17 +VERSION_ALPHA = 18 # END_VERSION_BLOCK From 1480e1f02e50041bb5ef21765c8405426073a74f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 24 Oct 2023 21:01:45 +0000 Subject: [PATCH 54/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed0e8b..fe50365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a17](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a17) (2023-10-24) +## [0.0.6a18](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a18) (2023-10-24) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a16...0.0.6a17) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a17...0.0.6a18) + +**Merged pull requests:** + +- remove deprecated [\#61](https://github.com/OpenVoiceOS/ovos-bus-client/pull/61) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a17](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a17) (2023-10-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a16...V0.0.6a17) **Fixed bugs:** From e36dbeba9eb31a756a8379e4c5b0c92a91140de8 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:54:45 -0700 Subject: [PATCH 55/76] Improved session logging (#62) Co-authored-by: Daniel McKnight --- ovos_bus_client/session.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 003947d..346da50 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -443,8 +443,14 @@ def from_message(message: Message = None): sess["lang"] = lang sess = Session.deserialize(sess) else: + if message: + LOG.warning(f"No session context in message:{message.msg_type}") + LOG.debug(f"Update ovos-bus-client or add `session` to " + f"`message.context` where emitted. " + f"context={message.context}") + else: + LOG.warning(f"No message found, using default session") # new session - LOG.warning(f"No message found, using default session") sess = SessionManager.default_session if sess and sess.expired(): LOG.debug(f"unexpiring session {sess.session_id}") From 5c2dfe7c6231cc697f163b2d593c4e1e70a11f6c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 25 Oct 2023 00:54:59 +0000 Subject: [PATCH 56/76] Increment Version to 0.0.6a19 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index dac7635..3592084 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 18 +VERSION_ALPHA = 19 # END_VERSION_BLOCK From 273b8a935c902089b17724fb63d61e63d2d27bcb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 25 Oct 2023 00:55:31 +0000 Subject: [PATCH 57/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe50365..a226519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a18](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a18) (2023-10-24) +## [0.0.6a19](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a19) (2023-10-25) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a17...0.0.6a18) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a18...0.0.6a19) + +**Merged pull requests:** + +- Improved session logging [\#62](https://github.com/OpenVoiceOS/ovos-bus-client/pull/62) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.6a18](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a18) (2023-10-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a17...V0.0.6a18) **Merged pull requests:** From b049eb82770d6f8a2b4195e38974a75743672e66 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 8 Dec 2023 19:53:48 +0000 Subject: [PATCH 58/76] feat/tts_prefs (#64) --- ovos_bus_client/session.py | 50 ++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/ovos_bus_client/session.py b/ovos_bus_client/session.py index 346da50..beca8d6 100644 --- a/ovos_bus_client/session.py +++ b/ovos_bus_client/session.py @@ -1,7 +1,7 @@ import enum import time from threading import Lock -from typing import Optional, List, Tuple, Union, Iterable +from typing import Optional, List, Tuple, Union, Iterable, Dict from uuid import uuid4 from ovos_config.config import Configuration @@ -17,7 +17,7 @@ class UtteranceState(str, enum.Enum): class IntentContextManagerFrame: - def __init__(self, entities: List[dict] = None, metadata: dict = None): + def __init__(self, entities: List[dict] = None, metadata: Dict = None): """ Manages entities and context for a single frame of conversation. Provides simple equality querying. @@ -36,7 +36,7 @@ def serialize(self) -> dict: "metadata": self.metadata} @staticmethod - def deserialize(data: dict): + def deserialize(data: Dict): """ Build an IntentContextManagerFrame from serialized data @param data: serialized (dict) frame data @@ -44,7 +44,7 @@ def deserialize(data: dict): """ return IntentContextManagerFrame(**data) - def metadata_matches(self, query: dict = None) -> bool: + def metadata_matches(self, query: Dict = None) -> bool: """ Returns key matches to metadata Asserts that the contents of query exist within (logical subset of) @@ -65,7 +65,7 @@ def metadata_matches(self, query: dict = None) -> bool: return result - def merge_context(self, tag: dict, metadata: dict): + def merge_context(self, tag: Dict, metadata: Dict): """ merge into contextManagerFrame new entity and metadata. Appends tag as new entity and adds keys in metadata to keys in @@ -119,7 +119,7 @@ def serialize(self) -> dict: in self.frame_stack]} @staticmethod - def deserialize(data: dict): + def deserialize(data: Dict): """ Build an IntentContextManager from serialized data @param data: serialized (dict) data @@ -130,7 +130,7 @@ def deserialize(data: dict): for (f, t) in data.get("frame_stack", [])] return IntentContextManager(timeout, framestack) - def update_context(self, entities: dict): + def update_context(self, entities: Dict): """ Updates context with keyword from the intent. @@ -162,7 +162,7 @@ def remove_context(self, context_id: str): self.frame_stack = [(f, t) for (f, t) in self.frame_stack if context_id in f.entities[0].get('data', [])] - def inject_context(self, entity: dict, metadata: dict = None): + def inject_context(self, entity: Dict, metadata: Dict = None): """ Add context to the first frame in the stack. If no frame metadata doesn't match the passed metadata then a new one is inserted. @@ -263,10 +263,12 @@ def get_context(self, max_frames: int = None, class Session: def __init__(self, session_id: str = None, expiration_seconds: int = None, active_skills: List[List[Union[str, float]]] = None, - utterance_states: dict = None, lang: str = None, + utterance_states: Dict = None, lang: str = None, context: IntentContextManager = None, site_id: str = "unknown", - pipeline: List[str] = None): + pipeline: List[str] = None, + stt_prefs: Dict = None, + tts_prefs: Dict = None): """ Construct a session identifier @param session_id: string UUID for the session @@ -277,7 +279,9 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, @param context: IntentContextManager for this Session """ self.session_id = session_id or str(uuid4()) + self.lang = lang or get_default_lang() + self.site_id = site_id or "unknown" # indoors placement info self.active_skills = active_skills or [] # [skill_id , timestamp]# (Message , timestamp) @@ -299,6 +303,20 @@ def __init__(self, session_id: str = None, expiration_seconds: int = None, ] self.context = context or IntentContextManager() + if not stt_prefs: + stt = Configuration().get("stt", {}) + sttm = stt.get("module", "ovos-stt-plugin-server") + stt_prefs = {"plugin_id": sttm, + "config": stt.get(sttm) or {}} + self.stt_preferences = stt_prefs + + if not tts_prefs: + tts = Configuration().get("tts", {}) + ttsm = tts.get("module", "ovos-tts-plugin-server") + tts_prefs = {"plugin_id": ttsm, + "config": tts.get(ttsm) or {}} + self.tts_preferences = tts_prefs + @property def active(self) -> bool: """ @@ -392,7 +410,9 @@ def serialize(self) -> dict: "lang": self.lang, "context": self.context.serialize(), "site_id": self.site_id, - "pipeline": self.pipeline + "pipeline": self.pipeline, + "stt": self.stt_preferences, + "tts": self.tts_preferences } def update_history(self, message: Message = None): @@ -404,7 +424,7 @@ def update_history(self, message: Message = None): "session no longer has a message history") @staticmethod - def deserialize(data: dict): + def deserialize(data: Dict): """ Build a Session object from dict data @param data: dict serialized Session object @@ -417,13 +437,17 @@ def deserialize(data: dict): context = IntentContextManager.deserialize(data.get("context", {})) site_id = data.get("site_id", "unknown") pipeline = data.get("pipeline", []) + tts = data.get("tts_preferences", {}) + stt = data.get("stt_preferences", {}) return Session(uid, active_skills=active, utterance_states=states, lang=lang, context=context, pipeline=pipeline, - site_id=site_id) + site_id=site_id, + tts_prefs=tts, + stt_prefs=stt) @staticmethod def from_message(message: Message = None): From ae580934176dc202fb03eee2c180b13c0d662cb9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 8 Dec 2023 19:54:08 +0000 Subject: [PATCH 59/76] Increment Version to 0.0.6a20 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 3592084..9f57b14 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 19 +VERSION_ALPHA = 20 # END_VERSION_BLOCK From 880a22e2f14ff43105f84997ab32248671105edb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 8 Dec 2023 19:54:34 +0000 Subject: [PATCH 60/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a226519..ded25c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a19](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a19) (2023-10-25) +## [0.0.6a20](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a20) (2023-12-08) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a18...0.0.6a19) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a19...0.0.6a20) + +**Implemented enhancements:** + +- feat/tts\_prefs [\#64](https://github.com/OpenVoiceOS/ovos-bus-client/pull/64) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a19](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a19) (2023-10-25) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a18...V0.0.6a19) **Merged pull requests:** From 796029825b7cba578a7b7e595b1aa9282ef380a5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 14 Dec 2023 03:16:00 -0800 Subject: [PATCH 61/76] Update to stable dependency specs (#65) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56b5568..01af550 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ ovos-config >= 0.0.8, < 0.1.0 -ovos-utils >= 0.0.36a1, < 0.1.0 +ovos-utils >= 0.0.36, < 0.1.0 websocket-client>=0.54.0 pyee>=8.1.0, < 9.0.0 From 94b0f1c9ac1bdc991506004235ae30a50112d673 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 14 Dec 2023 11:16:20 +0000 Subject: [PATCH 62/76] Increment Version to 0.0.6a21 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 9f57b14..f9f94db 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 20 +VERSION_ALPHA = 21 # END_VERSION_BLOCK From 6443f966a9bd274d383821eb4d5b4522ec992daf Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 14 Dec 2023 11:16:45 +0000 Subject: [PATCH 63/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ded25c2..b762498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a20](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a20) (2023-12-08) +## [0.0.6a21](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a21) (2023-12-14) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a19...0.0.6a20) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a20...0.0.6a21) + +**Merged pull requests:** + +- Update to stable dependency specs [\#65](https://github.com/OpenVoiceOS/ovos-bus-client/pull/65) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.6a20](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a20) (2023-12-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a19...V0.0.6a20) **Implemented enhancements:** From 01ffd2150308a7c74081bdbf595f7a40e48ae3f3 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:35:48 +0000 Subject: [PATCH 64/76] feat/bus_apis (#66) companion to https://github.com/OpenVoiceOS/ovos-utils/issues/205 --- ovos_bus_client/apis/__init__.py | 0 ovos_bus_client/apis/enclosure.py | 337 ++++++++++++++++++++++++++++++ ovos_bus_client/apis/ocp.py | 272 ++++++++++++++++++++++++ ovos_bus_client/client/client.py | 3 +- setup.py | 1 + 5 files changed, 611 insertions(+), 2 deletions(-) create mode 100644 ovos_bus_client/apis/__init__.py create mode 100644 ovos_bus_client/apis/enclosure.py create mode 100644 ovos_bus_client/apis/ocp.py diff --git a/ovos_bus_client/apis/__init__.py b/ovos_bus_client/apis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ovos_bus_client/apis/enclosure.py b/ovos_bus_client/apis/enclosure.py new file mode 100644 index 0000000..9100482 --- /dev/null +++ b/ovos_bus_client/apis/enclosure.py @@ -0,0 +1,337 @@ +from ovos_bus_client.message import Message, dig_for_message + + +class EnclosureAPI: + """ + This API is intended to be used to interface with the hardware + that is running Mycroft. It exposes all possible commands which + can be sent to a Mycroft enclosure implementation. + + Different enclosure implementations may implement this differently + and/or may ignore certain API calls completely. For example, + the eyes_color() API might be ignore on a Mycroft that uses simple + LEDs which only turn on/off, or not at all on an implementation + where there is no face at all. + """ + + def __init__(self, bus=None, skill_id=""): + self.bus = bus + self.skill_id = skill_id + + def set_bus(self, bus): + self.bus = bus + + def set_id(self, skill_id): + self.skill_id = skill_id + + def _get_source_message(self): + return dig_for_message() or \ + Message("", context={"destination": ["enclosure"], + "skill_id": self.skill_id}) + + def register(self, skill_id=""): + """Registers a skill as active. Used for speak() and speak_dialog() + to 'patch' a previous implementation. Somewhat hacky. + DEPRECATED - unused + """ + source_message = self._get_source_message() + skill_id = skill_id or self.skill_id + self.bus.emit(source_message.forward("enclosure.active_skill", + {"skill_id": skill_id})) + + def reset(self): + """The enclosure should restore itself to a started state. + Typically this would be represented by the eyes being 'open' + and the mouth reset to its default (smile or blank). + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.reset")) + + def system_reset(self): + """The enclosure hardware should reset any CPUs, etc.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.reset")) + + def system_mute(self): + """Mute (turn off) the system speaker.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.mute")) + + def system_unmute(self): + """Unmute (turn on) the system speaker.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.unmute")) + + def system_blink(self, times): + """The 'eyes' should blink the given number of times. + Args: + times (int): number of times to blink + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.blink", + {'times': times})) + + def eyes_on(self): + """Illuminate or show the eyes.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.on")) + + def eyes_off(self): + """Turn off or hide the eyes.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.off")) + + def eyes_blink(self, side): + """Make the eyes blink + Args: + side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.blink", + {'side': side})) + + def eyes_narrow(self): + """Make the eyes look narrow, like a squint""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.narrow")) + + def eyes_look(self, side): + """Make the eyes look to the given side + Args: + side (str): 'r' for right + 'l' for left + 'u' for up + 'd' for down + 'c' for crossed + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.look", + {'side': side})) + + def eyes_color(self, r=255, g=255, b=255): + """Change the eye color to the given RGB color + Args: + r (int): 0-255, red value + g (int): 0-255, green value + b (int): 0-255, blue value + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.color", + {'r': r, 'g': g, 'b': b})) + + def eyes_setpixel(self, idx, r=255, g=255, b=255): + """Set individual pixels of the Mark 1 neopixel eyes + Args: + idx (int): 0-11 for the right eye, 12-23 for the left + r (int): The red value to apply + g (int): The green value to apply + b (int): The blue value to apply + """ + source_message = self._get_source_message() + if idx < 0 or idx > 23: + raise ValueError(f'idx ({idx}) must be between 0-23') + self.bus.emit(source_message.forward("enclosure.eyes.setpixel", + {'idx': idx, + 'r': r, 'g': g, 'b': b})) + + def eyes_fill(self, percentage): + """Use the eyes as a type of progress meter + Args: + percentage (int): 0-49 fills the right eye, 50-100 also covers left + """ + source_message = self._get_source_message() + if percentage < 0 or percentage > 100: + raise ValueError(f'percentage ({percentage}) must be between 0-100') + self.bus.emit(source_message.forward("enclosure.eyes.fill", + {'percentage': percentage})) + + def eyes_brightness(self, level=30): + """Set the brightness of the eyes in the display. + Args: + level (int): 1-30, bigger numbers being brighter + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.level", + {'level': level})) + + def eyes_reset(self): + """Restore the eyes to their default (ready) state.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.reset")) + + def eyes_spin(self): + """Make the eyes 'roll' + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.spin")) + + def eyes_timed_spin(self, length): + """Make the eyes 'roll' for the given time. + Args: + length (int): duration in milliseconds of roll, None = forever + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.timedspin", + {'length': length})) + + def eyes_volume(self, volume): + """Indicate the volume using the eyes + Args: + volume (int): 0 to 11 + """ + source_message = self._get_source_message() + if volume < 0 or volume > 11: + raise ValueError('volume ({}) must be between 0-11'. + format(str(volume))) + self.bus.emit(source_message.forward("enclosure.eyes.volume", + {'volume': volume})) + + def mouth_reset(self): + """Restore the mouth display to normal (blank)""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.reset")) + + def mouth_talk(self): + """Show a generic 'talking' animation for non-synched speech""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.talk")) + + def mouth_think(self): + """Show a 'thinking' image or animation""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.think")) + + def mouth_listen(self): + """Show a 'thinking' image or animation""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.listen")) + + def mouth_smile(self): + """Show a 'smile' image or animation""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.smile")) + + def mouth_viseme(self, start, viseme_pairs): + """ Send mouth visemes as a list in a single message. + + Args: + start (int): Timestamp for start of speech + viseme_pairs: Pairs of viseme id and cumulative end times + (code, end time) + + codes: + 0 = shape for sounds like 'y' or 'aa' + 1 = shape for sounds like 'aw' + 2 = shape for sounds like 'uh' or 'r' + 3 = shape for sounds like 'th' or 'sh' + 4 = neutral shape for no sound + 5 = shape for sounds like 'f' or 'v' + 6 = shape for sounds like 'oy' or 'ao' + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.viseme_list", + {"start": start, + "visemes": viseme_pairs})) + + def mouth_text(self, text=""): + """Display text (scrolling as needed) + Args: + text (str): text string to display + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.text", + {'text': text})) + + def mouth_display(self, img_code="", x=0, y=0, refresh=True): + """Display images on faceplate. Currently supports images up to 16x8, + or half the face. You can use the 'x' parameter to cover the other + half of the faceplate. + Args: + img_code (str): text string that encodes a black and white image + x (int): x offset for image + y (int): y offset for image + refresh (bool): specify whether to clear the faceplate before + displaying the new image or not. + Useful if you'd like to display multiple images + on the faceplate at once. + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward('enclosure.mouth.display', + {'img_code': img_code, + 'xOffset': x, + 'yOffset': y, + 'clearPrev': refresh})) + + def mouth_display_png(self, image_absolute_path, + invert=False, x=0, y=0, refresh=True): + """ Send an image to the enclosure. + + Args: + image_absolute_path (string): The absolute path of the image + invert (bool): inverts the image being drawn. + x (int): x offset for image + y (int): y offset for image + refresh (bool): specify whether to clear the faceplate before + displaying the new image or not. + Useful if you'd like to display muliple images + on the faceplate at once. + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.display_image", + {'img_path': image_absolute_path, + 'xOffset': x, + 'yOffset': y, + 'invert': invert, + 'clearPrev': refresh})) + + def weather_display(self, img_code, temp): + """Show a the temperature and a weather icon + + Args: + img_code (char): one of the following icon codes + 0 = sunny + 1 = partly cloudy + 2 = cloudy + 3 = light rain + 4 = raining + 5 = stormy + 6 = snowing + 7 = wind/mist + temp (int): the temperature (either C or F, not indicated) + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.weather.display", + {'img_code': img_code, + 'temp': temp})) + + def activate_mouth_events(self): + """Enable movement of the mouth with speech""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward('enclosure.mouth.events.activate')) + + def deactivate_mouth_events(self): + """Disable movement of the mouth with speech""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward( + 'enclosure.mouth.events.deactivate')) + + def get_eyes_color(self): + """Get the eye RGB color for all pixels + Returns: + (list) pixels - list of (r,g,b) tuples for each eye pixel + """ + source_message = self._get_source_message() + message = source_message.forward("enclosure.eyes.rgb.get") + response = self.bus.wait_for_response(message, "enclosure.eyes.rgb") + if response: + return response.data["pixels"] + raise TimeoutError("Enclosure took too long to respond") + + def get_eyes_pixel_color(self, idx): + """Get the RGB color for a specific eye pixel + Returns: + (r,g,b) tuples for selected pixel + """ + if idx < 0 or idx > 23: + raise ValueError(f'idx ({idx}) must be between 0-23') + return self.get_eyes_color()[idx] diff --git a/ovos_bus_client/apis/ocp.py b/ovos_bus_client/apis/ocp.py new file mode 100644 index 0000000..40ba3ef --- /dev/null +++ b/ovos_bus_client/apis/ocp.py @@ -0,0 +1,272 @@ +# Copyright 2017 Mycroft AI Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from datetime import timedelta + +from os.path import abspath + +from ovos_utils.messagebus import get_mycroft_bus +from ovos_bus_client.message import Message, dig_for_message + + +def ensure_uri(s: str): + """ + Interpret paths as file:// uri's. + + Args: + s: string path to be checked + + Returns: + if s is uri, s is returned otherwise file:// is prepended + """ + if isinstance(s, str): + if '://' not in s: + return 'file://' + abspath(s) + else: + return s + elif isinstance(s, (tuple, list)): # Handle (mime, uri) arg + if '://' not in s[0]: + return 'file://' + abspath(s[0]), s[1] + else: + return s + else: + raise ValueError('Invalid track') + + +class ClassicAudioServiceInterface: + """AudioService class for interacting with the audio subsystem + + Audio is managed by OCP in the default implementation, + usually this class should not be directly used, see OCPInterface instead + + Args: + bus: Mycroft messagebus connection + """ + + def __init__(self, bus=None): + self.bus = bus or get_mycroft_bus() + + def queue(self, tracks=None): + """Queue up a track to playing playlist. + + Args: + tracks: track uri or list of track uri's + Each track can be added as a tuple with (uri, mime) + to give a hint of the mime type to the system + """ + tracks = tracks or [] + if isinstance(tracks, (str, tuple)): + tracks = [tracks] + elif not isinstance(tracks, list): + raise ValueError + tracks = [ensure_uri(t) for t in tracks] + self.bus.emit(Message('mycroft.audio.service.queue', + data={'tracks': tracks})) + + def play(self, tracks=None, utterance=None, repeat=None): + """Start playback. + + Args: + tracks: track uri or list of track uri's + Each track can be added as a tuple with (uri, mime) + to give a hint of the mime type to the system + utterance: forward utterance for further processing by the + audio service. + repeat: if the playback should be looped + """ + repeat = repeat or False + tracks = tracks or [] + utterance = utterance or '' + if isinstance(tracks, (str, tuple)): + tracks = [tracks] + elif not isinstance(tracks, list): + raise ValueError + tracks = [ensure_uri(t) for t in tracks] + self.bus.emit(Message('mycroft.audio.service.play', + data={'tracks': tracks, + 'utterance': utterance, + 'repeat': repeat})) + + def stop(self): + """Stop the track.""" + self.bus.emit(Message('mycroft.audio.service.stop')) + + def next(self): + """Change to next track.""" + self.bus.emit(Message('mycroft.audio.service.next')) + + def prev(self): + """Change to previous track.""" + self.bus.emit(Message('mycroft.audio.service.prev')) + + def pause(self): + """Pause playback.""" + self.bus.emit(Message('mycroft.audio.service.pause')) + + def resume(self): + """Resume paused playback.""" + self.bus.emit(Message('mycroft.audio.service.resume')) + + def get_track_length(self): + """ + getting the duration of the audio in seconds + """ + length = 0 + info = self.bus.wait_for_response( + Message('mycroft.audio.service.get_track_length'), + timeout=1) + if info: + length = info.data.get("length") or 0 + return length / 1000 # convert to seconds + + def get_track_position(self): + """ + get current position in seconds + """ + pos = 0 + info = self.bus.wait_for_response( + Message('mycroft.audio.service.get_track_position'), + timeout=1) + if info: + pos = info.data.get("position") or 0 + return pos / 1000 # convert to seconds + + def set_track_position(self, seconds): + """Seek X seconds. + + Arguments: + seconds (int): number of seconds to seek, if negative rewind + """ + self.bus.emit(Message('mycroft.audio.service.set_track_position', + {"position": seconds * 1000})) # convert to ms + + def seek(self, seconds=1): + """Seek X seconds. + + Args: + seconds (int): number of seconds to seek, if negative rewind + """ + if isinstance(seconds, timedelta): + seconds = seconds.total_seconds() + if seconds < 0: + self.seek_backward(abs(seconds)) + else: + self.seek_forward(seconds) + + def seek_forward(self, seconds=1): + """Skip ahead X seconds. + + Args: + seconds (int): number of seconds to skip + """ + if isinstance(seconds, timedelta): + seconds = seconds.total_seconds() + self.bus.emit(Message('mycroft.audio.service.seek_forward', + {"seconds": seconds})) + + def seek_backward(self, seconds=1): + """Rewind X seconds + + Args: + seconds (int): number of seconds to rewind + """ + if isinstance(seconds, timedelta): + seconds = seconds.total_seconds() + self.bus.emit(Message('mycroft.audio.service.seek_backward', + {"seconds": seconds})) + + def track_info(self): + """Request information of current playing track. + + Returns: + Dict with track info. + """ + info = self.bus.wait_for_response( + Message('mycroft.audio.service.track_info'), + reply_type='mycroft.audio.service.track_info_reply', + timeout=1) + return info.data if info else {} + + def available_backends(self): + """Return available audio backends. + + Returns: + dict with backend names as keys + """ + msg = Message('mycroft.audio.service.list_backends') + response = self.bus.wait_for_response(msg) + return response.data if response else {} + + @property + def is_playing(self): + """True if the audioservice is playing, else False.""" + return self.track_info() != {} + + +class OCPInterface: + """bus api interface for OCP subsystem + Args: + bus: Mycroft messagebus connection + """ + + def __init__(self, bus=None): + self.bus = bus or get_mycroft_bus() + + def _format_msg(self, msg_type, msg_data=None): + # this method ensures all skills are .forward from the utterance + # that triggered the skill, this ensures proper routing and metadata + msg_data = msg_data or {} + msg = dig_for_message() + if msg: + msg = msg.forward(msg_type, msg_data) + else: + msg = Message(msg_type, msg_data) + # at this stage source == skills, lets indicate audio service took over + sauce = msg.context.get("source") + if sauce == "skills": + msg.context["source"] = "audio_service" + return msg + + # OCP bus api + def queue(self, tracks): + """Queue up a track to OCP playing playlist. + + Args: + tracks: track dict or list of track dicts (OCP result style) + """ + + assert isinstance(tracks, list) + assert all(isinstance(t, dict) for t in tracks) + + msg = self._format_msg('ovos.common_play.playlist.queue', + {'tracks': tracks}) + self.bus.emit(msg) + + def play(self, tracks, utterance=None): + """Start playback. + Args: + tracks: track dict or list of track dicts (OCP result style) + utterance: forward utterance for further processing by OCP + """ + assert isinstance(tracks, list) + assert all(isinstance(t, dict) for t in tracks) + + utterance = utterance or '' + + msg = self._format_msg('ovos.common_play.play', + {"media": tracks[0], + "playlist": tracks, + "utterance": utterance}) + self.bus.emit(msg) diff --git a/ovos_bus_client/client/client.py b/ovos_bus_client/client/client.py index 718e9e4..54dfe65 100644 --- a/ovos_bus_client/client/client.py +++ b/ovos_bus_client/client/client.py @@ -150,8 +150,7 @@ def on_message(self, *args): message = args[1] parsed_message = Message.deserialize(message) sess = Session.from_message(parsed_message) - if sess.session_id != "default": - # 'default' can only be updated by core + if sess.session_id != "default": # 'default' can only be updated by core SessionManager.update(sess) self.emitter.emit('message', message) self.emitter.emit(parsed_message.msg_type, parsed_message) diff --git a/setup.py b/setup.py index 4005343..7eb8b4d 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ def get_version(): version=get_version(), packages=['ovos_bus_client', 'ovos_bus_client.client', + 'ovos_bus_client.apis', 'ovos_bus_client.util'], package_data={ '*': ['*.txt', '*.md'] From ef6009be2fe5ff3b7e1b9212dc55fbc0b7aeaf8e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 17:36:04 +0000 Subject: [PATCH 65/76] Increment Version to 0.0.6a22 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index f9f94db..37ef8f0 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 21 +VERSION_ALPHA = 22 # END_VERSION_BLOCK From f34270fca4c70e809c12e6c9f0b5dc547388fd64 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 17:36:33 +0000 Subject: [PATCH 66/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b762498..c026bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a21](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a21) (2023-12-14) +## [0.0.6a22](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a22) (2023-12-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a20...0.0.6a21) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a21...0.0.6a22) + +**Merged pull requests:** + +- feat/bus\_apis [\#66](https://github.com/OpenVoiceOS/ovos-bus-client/pull/66) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a21](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a21) (2023-12-14) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a20...V0.0.6a21) **Merged pull requests:** From ef6e47622971b2844df74bdb6344e8f18c11e2fc Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:44:28 +0000 Subject: [PATCH 67/76] feat/gui_bus_api (#67) companion to https://github.com/OpenVoiceOS/ovos-utils/pull/204 --- ovos_bus_client/apis/gui.py | 749 ++++++++++++++++++++++++++++++++++++ 1 file changed, 749 insertions(+) create mode 100644 ovos_bus_client/apis/gui.py diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py new file mode 100644 index 0000000..dc6742d --- /dev/null +++ b/ovos_bus_client/apis/gui.py @@ -0,0 +1,749 @@ +from os import walk +from os.path import join, splitext, isfile +from typing import List, Union, Optional, Callable + +from ovos_utils import resolve_ovos_resource_file, resolve_resource_file +from ovos_utils.log import LOG, log_deprecation +from ovos_utils.messagebus import get_mycroft_bus +from ovos_utils.gui import can_use_gui + +from ovos_bus_client.message import Message + + +def extend_about_data(about_data: Union[list, dict], + bus=None): + """ + Add more information to the "About" section in the GUI. + @param about_data: list of dict key, val information to add to the GUI + @param bus: MessageBusClient object to emit update on + """ + bus = bus or get_mycroft_bus() + if isinstance(about_data, list): + bus.emit(Message("smartspeaker.extension.extend.about", + {"display_list": about_data})) + elif isinstance(about_data, dict): + display_list = [about_data] + bus.emit(Message("smartspeaker.extension.extend.about", + {"display_list": display_list})) + else: + LOG.error("about_data is not a list or dictionary") + + +class GUIWidgets: + def __init__(self, bus=None): + self.bus = bus or get_mycroft_bus() + + def show_widget(self, widget_type, widget_data): + LOG.debug("Showing widget: " + widget_type) + self.bus.emit(Message("ovos.widgets.display", {"type": widget_type, "data": widget_data})) + + def remove_widget(self, widget_type, widget_data): + LOG.debug("Removing widget: " + widget_type) + self.bus.emit(Message("ovos.widgets.remove", {"type": widget_type, "data": widget_data})) + + def update_widget(self, widget_type, widget_data): + LOG.debug("Updating widget: " + widget_type) + self.bus.emit(Message("ovos.widgets.update", {"type": widget_type, "data": widget_data})) + + +class _GUIDict(dict): + """ + This is a helper dictionary subclass. It ensures that values changed + in it are propagated to the GUI service in real time. + """ + + def __init__(self, gui, **kwargs): + self.gui = gui + super().__init__(**kwargs) + + def __setitem__(self, key, value): + old = self.get(key) + if old != value: + super(_GUIDict, self).__setitem__(key, value) + self.gui._sync_data() + + +class GUIInterface: + """ + Interface to the Graphical User Interface, allows interaction with + the mycroft-gui from anywhere + + Values set in this class are synced to the GUI, accessible within QML + via the built-in sessionData mechanism. For example, in Python you can + write in a skill: + self.gui['temp'] = 33 + self.gui.show_page('Weather.qml') + Then in the Weather.qml you'd access the temp via code such as: + text: sessionData.time + """ + + def __init__(self, skill_id: str, bus=None, + remote_server: str = None, config: dict = None, + ui_directories: dict = None): + """ + Create an interface to the GUI module. Values set here are exposed to + the GUI client as sessionData + @param skill_id: ID of this interface + @param bus: MessagebusClient object to connect to + @param remote_server: Optional URL of a remote GUI server + @param config: dict gui Configuration + @param ui_directories: dict framework to directory containing resources + `all` key should reference a `gui` directory containing all + specific resource subdirectories + """ + if not config: + log_deprecation(f"Expected a dict config and got None.", "0.1.0") + try: + from ovos_config.config import read_mycroft_config + config = read_mycroft_config().get("gui", {}) + except ImportError: + LOG.warning("Config not provided and ovos_config not available") + config = dict() + self.config = config + if remote_server: + self.config["remote-server"] = remote_server + self._bus = bus + self.__session_data = {} # synced to GUI for use by this skill's pages + self._pages = [] + self.current_page_idx = -1 + self._skill_id = skill_id + self.on_gui_changed_callback = None + self._events = [] + self.ui_directories = ui_directories or dict() + if bus: + self.set_bus(bus) + + @property + def remote_url(self) -> Optional[str]: + """Returns configuration value for url of remote-server.""" + return self.config.get('remote-server') + + @remote_url.setter + def remote_url(self, val: str): + self.config["remote-server"] = val + + def set_bus(self, bus=None): + self._bus = bus or get_mycroft_bus() + self.setup_default_handlers() + + @property + def bus(self): + """ + Return the attached MessageBusClient + """ + return self._bus + + @bus.setter + def bus(self, val): + self.set_bus(val) + + @property + def skill_id(self) -> str: + """ + Return the ID of the module implementing this interface + """ + return self._skill_id + + @skill_id.setter + def skill_id(self, val: str): + self._skill_id = val + + @property + def page(self) -> Optional[str]: + """ + Return the active GUI page name to show + """ + return self._pages[self.current_page_idx] if len(self._pages) else None + + @property + def connected(self) -> bool: + """ + Returns True if at least 1 remote gui is connected or if gui is + installed and running locally, else False + """ + if not self.bus: + return False + return can_use_gui(self.bus) + + @property + def pages(self) -> List[str]: + """ + Get a list of the active page ID's managed by this interface + """ + return self._pages + + def build_message_type(self, event: str) -> str: + """ + Ensure the specified event prepends this interface's `skill_id` + """ + if not event.startswith(f'{self.skill_id}.'): + event = f'{self.skill_id}.' + event + return event + + # events + def setup_default_handlers(self): + """ + Sets the handlers for the default messages. + """ + msg_type = self.build_message_type('set') + self.bus.on(msg_type, self.gui_set) + self._events.append((msg_type, self.gui_set)) + self.bus.on("gui.request_page_upload", self.upload_gui_pages) + if self.ui_directories: + LOG.debug("Volunteering gui page upload") + self.bus.emit(Message("gui.volunteer_page_upload", + {'skill_id': self.skill_id}, + {'source': self.skill_id, "destination": ["gui"]})) + + def upload_gui_pages(self, message: Message): + """ + Emit a response Message with all known GUI files managed by + this interface for the requested infrastructure + @param message: `gui.request_page_upload` Message requesting pages + """ + if not self.ui_directories: + LOG.debug("No UI resources to upload") + return + + requested_skill = message.data.get("skill_id") or self._skill_id + if requested_skill != self._skill_id: + # GUI requesting a specific skill to upload other than this one + return + + request_res_type = message.data.get("framework") or "all" if "all" in \ + self.ui_directories else "qt5" + # Note that ui_directory "all" is a special case that will upload all + # gui files, including all framework subdirectories + if request_res_type not in self.ui_directories: + LOG.warning(f"Requested UI files not available: {request_res_type}") + return + LOG.debug(f"Requested upload resources for: {request_res_type}") + pages = dict() + # `pages` keys are unique identifiers in the scope of this interface; + # if ui_directory is "all", then pages are prefixed with `/` + res_dir = self.ui_directories[request_res_type] + for path, _, files in walk(res_dir): + for file in files: + try: + full_path: str = join(path, file) + page_name = full_path.replace(f"{res_dir}/", "", 1) + with open(full_path, 'rb') as f: + file_bytes = f.read() + pages[page_name] = file_bytes.hex() + except Exception as e: + LOG.exception(f"{file} not uploaded: {e}") + # Note that `pages` in this context include file extensions + self.bus.emit(message.forward("gui.page.upload", + {"__from": self.skill_id, + "framework": request_res_type, + "pages": pages})) + + def register_handler(self, event: str, handler: Callable): + """ + Register a handler for GUI events. + + will be prepended with self.skill_id.XXX if missing in event + + When using the triggerEvent method from Qt + triggerEvent("event", {"data": "cool"}) + + Args: + event (str): event to catch + handler: function to handle the event + """ + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + event = self.build_message_type(event) + self._events.append((event, handler)) + self.bus.on(event, handler) + + def set_on_gui_changed(self, callback: Callable): + """ + Registers a callback function to run when a value is + changed from the GUI. + + Arguments: + callback: Function to call when a value is changed + """ + self.on_gui_changed_callback = callback + + # internals + def gui_set(self, message: Message): + """ + Handler catching variable changes from the GUI. + + Arguments: + message: Messagebus message + """ + for key in message.data: + self[key] = message.data[key] + if self.on_gui_changed_callback: + self.on_gui_changed_callback() + + def _sync_data(self): + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + data = self.__session_data.copy() + data.update({'__from': self.skill_id}) + self.bus.emit(Message("gui.value.set", data)) + + def __setitem__(self, key, value): + """Implements set part of dict-like behaviour with named keys.""" + old = self.__session_data.get(key) + if old == value: # no need to sync + return + + # cast to helper dict subclass that syncs data + if isinstance(value, dict) and not isinstance(value, _GUIDict): + value = _GUIDict(self, **value) + + self.__session_data[key] = value + + # emit notification (but not needed if page has not been shown yet) + if self.page: + self._sync_data() + + def __getitem__(self, key): + """Implements get part of dict-like behaviour with named keys.""" + return self.__session_data[key] + + def get(self, *args, **kwargs): + """Implements the get method for accessing dict keys.""" + return self.__session_data.get(*args, **kwargs) + + def __contains__(self, key): + """ + Implements the "in" operation. + """ + return self.__session_data.__contains__(key) + + def clear(self): + """ + Reset the value dictionary, and remove namespace from GUI. + + This method does not close the GUI for a Skill. For this purpose see + the `release` method. + """ + self.__session_data = {} + self._pages = [] + self.current_page_idx = -1 + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + self.bus.emit(Message("gui.clear.namespace", + {"__from": self.skill_id})) + + def send_event(self, event_name: str, + params: Union[dict, list, str, int, float, bool] = None): + """ + Trigger a gui event. + + Arguments: + event_name (str): name of event to be triggered + params: json serializable object containing any parameters that + should be sent along with the request. + """ + params = params or {} + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + self.bus.emit(Message("gui.event.send", + {"__from": self.skill_id, + "event_name": event_name, + "params": params})) + + def _pages2uri(self, page_names: List[str]) -> List[str]: + """ + Get a list of resolved URIs from a list of string page names. + @param page_names: List of GUI resource names (file basenames) to locate + @return: List of resolved paths to the requested pages + """ + # TODO: This method resolves absolute file paths. These will no longer + # be used with the implementation of `ovos-gui` + page_urls = [] + extra_dirs = list(self.ui_directories.values()) or list() + for name in page_names: + # Prefer plugin-specific resources first, then fallback to core + page = resolve_ovos_resource_file(name, extra_dirs) or \ + resolve_ovos_resource_file(join('ui', name), extra_dirs) or \ + resolve_resource_file(name, self.config) or \ + resolve_resource_file(join('ui', name), self.config) + + if page: + if self.remote_url: + page_urls.append(self.remote_url + "/" + page) + elif page.startswith("file://"): + page_urls.append(page) + else: + page_urls.append("file://" + page) + else: + # This is expected; with `ovos-gui`, pages are referenced by ID + # rather than filename in order to support multiple frameworks + LOG.debug(f"Requested page not resolved to a file: {page}") + LOG.debug(f"Resolved pages: {page_urls}") + return page_urls + + @staticmethod + def _normalize_page_name(page_name: str) -> str: + """ + Normalize a requested GUI resource + @param page_name: string name of a GUI resource + @return: normalized string name (`.qml` removed for other GUI support) + """ + if isfile(page_name): + log_deprecation("GUI resources should specify a resource name and " + "not a file path.", "0.1.0") + return page_name + file, ext = splitext(page_name) + if ext == ".qml": + log_deprecation("GUI resources should exclude gui-specific file " + f"extensions. This call should probably pass " + f"`{file}`, instead of `{page_name}`", "0.1.0") + return file + + return page_name + + # base gui interactions + def show_page(self, name: str, override_idle: Union[bool, int] = None, + override_animations: bool = False): + """ + Request to show a page in the GUI. + @param name: page resource requested + @param override_idle: number of seconds to override display for; + if True, override display indefinitely + @param override_animations: if True, disables all GUI animations + """ + self.show_pages([name], 0, override_idle, override_animations) + + def show_pages(self, page_names: List[str], index: int = 0, + override_idle: Union[bool, int] = None, + override_animations: bool = False): + """ + Request to show a list of pages in the GUI. + @param page_names: list of page resources requested + @param index: position to insert pages at (default 0) + @param override_idle: number of seconds to override display for; + if True, override display indefinitely + @param override_animations: if True, disables all GUI animations + """ + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + if isinstance(page_names, str): + page_names = [page_names] + if not isinstance(page_names, list): + raise ValueError('page_names must be a list') + + if index > len(page_names): + LOG.error('Default index is larger than page list length') + index = len(page_names) - 1 + + # TODO: deprecate sending page_urls after ovos_gui implementation + page_urls = self._pages2uri(page_names) + page_names = [self._normalize_page_name(n) for n in page_names] + + self._pages = page_names + self.current_page_idx = index + + # First sync any data... + data = self.__session_data.copy() + data.update({'__from': self.skill_id}) + LOG.debug(f"Updating gui data: {data}") + self.bus.emit(Message("gui.value.set", data)) + + # finally tell gui what to show + self.bus.emit(Message("gui.page.show", + {"page": page_urls, + "page_names": page_names, + "ui_directories": self.ui_directories, + "index": index, + "__from": self.skill_id, + "__idle": override_idle, + "__animations": override_animations})) + + def remove_page(self, page: str): + """ + Remove a single page from the GUI. + @param page: Name of page to remove + """ + self.remove_pages([page]) + + def remove_pages(self, page_names: List[str]): + """ + Request to remove a list of pages from the GUI. + @param page_names: list of page resources requested + """ + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + if isinstance(page_names, str): + page_names = [page_names] + if not isinstance(page_names, list): + raise ValueError('page_names must be a list') + # TODO: deprecate sending page_urls after ovos_gui implementation + page_urls = self._pages2uri(page_names) + page_names = [self._normalize_page_name(n) for n in page_names] + self.bus.emit(Message("gui.page.delete", + {"page": page_urls, + "page_names": page_names, + "__from": self.skill_id})) + + # Utils / Templates + + # backport - PR https://github.com/MycroftAI/mycroft-core/pull/2862 + def show_notification(self, content: str, duration: int = 10, + action: str = None, noticetype: str = "transient", + style: str = "info", + callback_data: Optional[dict] = None): + """Display a Notification on homepage in the GUI. + Arguments: + content (str): Main text content of a notification, Limited + to two visual lines. + duration (int): seconds to display notification for + action (str): Callback to any event registered by the skill + to perform a certain action when notification is clicked. + noticetype (str): + transient: 'Default' displays a notification with a timeout. + sticky: displays a notification that sticks to the screen. + style (str): + info: 'Default' displays a notification with information styling + warning: displays a notification with warning styling + success: displays a notification with success styling + error: displays a notification with error styling + callback_data (dict): data dictionary available to use with action + """ + # TODO: Define enums for style and noticetype + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + # GUI does not accept NONE type, send an empty dict + # Sending NONE will corrupt entries in the model + callback_data = callback_data or dict() + self.bus.emit(Message("ovos.notification.api.set", + data={ + "duration": duration, + "sender": self.skill_id, + "text": content, + "action": action, + "type": noticetype, + "style": style, + "callback_data": callback_data + })) + + def show_controlled_notification(self, content: str, style: str = "info"): + """ + Display a controlled Notification in the GUI. + Arguments: + content (str): Main text content of a notification, Limited + to two visual lines. + style (str): + info: 'Default' displays a notification with information styling + warning: displays a notification with warning styling + success: displays a notification with success styling + error: displays a notification with error styling + """ + # TODO: Define enum for style + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + self.bus.emit(Message("ovos.notification.api.set.controlled", + data={ + "sender": self.skill_id, + "text": content, + "style": style + })) + + def remove_controlled_notification(self): + """ + Remove a controlled Notification in the GUI. + """ + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + self.bus.emit(Message("ovos.notification.api.remove.controlled")) + + def show_text(self, text: str, title: Optional[str] = None, + override_idle: Union[int, bool] = None, + override_animations: bool = False): + """ + Display a GUI page for viewing simple text. + + Arguments: + text (str): Main text content. It will auto-paginate + title (str): A title to display above the text content. + override_idle (boolean, int): + True: Takes over the resting page indefinitely + (int): Delays resting page for the specified number of + seconds. + override_animations (boolean): + True: Disables showing all platform skill animations. + False: 'Default' always show animations. + """ + self["text"] = text + self["title"] = title + self.show_page("SYSTEM_TextFrame.qml", override_idle, + override_animations) + + def show_image(self, url: str, caption: Optional[str] = None, + title: Optional[str] = None, + fill: str = None, background_color: str = None, + override_idle: Union[int, bool] = None, + override_animations: bool = False): + """ + Display a GUI page for viewing an image. + + Arguments: + url (str): Pointer to the image + caption (str): A caption to show under the image + title (str): A title to display above the image content + fill (str): Fill type supports 'PreserveAspectFit', + 'PreserveAspectCrop', 'Stretch' + background_color (str): A background color for + the page in hex i.e. #000000 + override_idle (boolean, int): + True: Takes over the resting page indefinitely + (int): Delays resting page for the specified number of + seconds. + override_animations (boolean): + True: Disables showing all platform skill animations. + False: 'Default' always show animations. + """ + self["image"] = url + self["title"] = title + self["caption"] = caption + self["fill"] = fill + self["background_color"] = background_color + self.show_page("SYSTEM_ImageFrame.qml", override_idle, + override_animations) + + def show_animated_image(self, url: str, caption: Optional[str] = None, + title: Optional[str] = None, + fill: str = None, background_color: str = None, + override_idle: Union[int, bool] = None, + override_animations: bool = False): + """ + Display a GUI page for viewing an image. + + Args: + url (str): Pointer to the .gif image + caption (str): A caption to show under the image + title (str): A title to display above the image content + fill (str): Fill type supports 'PreserveAspectFit', + 'PreserveAspectCrop', 'Stretch' + background_color (str): A background color for + the page in hex i.e. #000000 + override_idle (boolean, int): + True: Takes over the resting page indefinitely + (int): Delays resting page for the specified number of + seconds. + override_animations (boolean): + True: Disables showing all platform skill animations. + False: 'Default' always show animations. + """ + self["image"] = url + self["title"] = title + self["caption"] = caption + self["fill"] = fill + self["background_color"] = background_color + self.show_page("SYSTEM_AnimatedImageFrame.qml", override_idle, + override_animations) + + def show_html(self, html: str, resource_url: Optional[str] = None, + override_idle: Union[int, bool] = None, + override_animations: bool = False): + """ + Display an HTML page in the GUI. + + Args: + html (str): HTML text to display + resource_url (str): Pointer to HTML resources + override_idle (boolean, int): + True: Takes over the resting page indefinitely + (int): Delays resting page for the specified number of + seconds. + override_animations (boolean): + True: Disables showing all platform skill animations. + False: 'Default' always show animations. + """ + self["html"] = html + self["resourceLocation"] = resource_url + self.show_page("SYSTEM_HtmlFrame.qml", override_idle, + override_animations) + + def show_url(self, url: str, override_idle: Union[int, bool] = None, + override_animations: bool = False): + """ + Display an HTML page in the GUI. + + Args: + url (str): URL to render + override_idle (boolean, int): + True: Takes over the resting page indefinitely + (int): Delays resting page for the specified number of + seconds. + override_animations (boolean): + True: Disables showing all platform skill animations. + False: 'Default' always show animations. + """ + self["url"] = url + self.show_page("SYSTEM_UrlFrame.qml", override_idle, + override_animations) + + def show_input_box(self, title: Optional[str] = None, + placeholder: Optional[str] = None, + confirm_text: Optional[str] = None, + exit_text: Optional[str] = None, + override_idle: Union[int, bool] = None, + override_animations: bool = False): + """ + Display a fullscreen UI for a user to enter text and confirm or cancel + @param title: title of input UI should describe what the input is + @param placeholder: default text hint to show in an empty entry box + @param confirm_text: text to display on the submit/confirm button + @param exit_text: text to display on the cancel/exit button + @param override_idle: if True, takes over the resting page indefinitely + else Delays resting page for the specified number of seconds. + @param override_animations: disable showing all platform animations + """ + self["title"] = title + self["placeholder"] = placeholder + self["skill_id_handler"] = self.skill_id + if not confirm_text: + self["confirm_text"] = "Confirm" + else: + self["confirm_text"] = confirm_text + + if not exit_text: + self["exit_text"] = "Exit" + else: + self["exit_text"] = exit_text + + self.show_page("SYSTEM_InputBox.qml", override_idle, + override_animations) + + def remove_input_box(self): + """ + Remove an input box shown by `show_input_box` + """ + LOG.info(f"GUI pages length {len(self._pages)}") + if len(self._pages) > 1: + self.remove_page("SYSTEM_InputBox.qml") + else: + self.release() + + def release(self): + """ + Signal that this skill is no longer using the GUI, + allow different platforms to properly handle this event. + Also calls self.clear() to reset the state variables + Platforms can close the window or go back to previous page + """ + if not self.bus: + raise RuntimeError("bus not set, did you call self.bind() ?") + self.clear() + self.bus.emit(Message("mycroft.gui.screen.close", + {"skill_id": self.skill_id})) + + def shutdown(self): + """ + Shutdown gui interface. + + Clear pages loaded through this interface and remove the bus events + """ + if self.bus: + self.release() + for event, handler in self._events: + self.bus.remove(event, handler) From 1d77772bdb599bd4633c4b8db7fb63dfc6b1fb37 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 18:44:47 +0000 Subject: [PATCH 68/76] Increment Version to 0.0.6a23 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 37ef8f0..660676c 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 22 +VERSION_ALPHA = 23 # END_VERSION_BLOCK From 8eb33067eab0ecada96bfaf8296842dd15411238 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 18:45:12 +0000 Subject: [PATCH 69/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c026bd5..b6fae0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a22](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a22) (2023-12-28) +## [0.0.6a23](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a23) (2023-12-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a21...0.0.6a22) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a22...0.0.6a23) + +**Merged pull requests:** + +- feat/gui\_bus\_api [\#67](https://github.com/OpenVoiceOS/ovos-bus-client/pull/67) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a22](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a22) (2023-12-28) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a21...V0.0.6a22) **Merged pull requests:** From bebf0a749976167531fe4b5cdc05ed40b5bd29df Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 28 Dec 2023 23:08:59 +0000 Subject: [PATCH 70/76] refactor/bus_utils (#68) * refactor/bus_utils more utils from ovos_utils * fix test --- ovos_bus_client/apis/gui.py | 4 +- ovos_bus_client/apis/ocp.py | 2 +- ovos_bus_client/client/client.py | 3 +- ovos_bus_client/message.py | 4 +- ovos_bus_client/util/__init__.py | 162 +++++++++++++++++++++++++++++- ovos_bus_client/util/scheduler.py | 2 - requirements.txt | 4 +- test/unittests/test_client.py | 2 +- 8 files changed, 169 insertions(+), 14 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index dc6742d..94b3101 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -2,9 +2,9 @@ from os.path import join, splitext, isfile from typing import List, Union, Optional, Callable -from ovos_utils import resolve_ovos_resource_file, resolve_resource_file +from ovos_utils.file_utils import resolve_ovos_resource_file, resolve_resource_file from ovos_utils.log import LOG, log_deprecation -from ovos_utils.messagebus import get_mycroft_bus +from ovos_bus_client.util import get_mycroft_bus from ovos_utils.gui import can_use_gui from ovos_bus_client.message import Message diff --git a/ovos_bus_client/apis/ocp.py b/ovos_bus_client/apis/ocp.py index 40ba3ef..0174720 100644 --- a/ovos_bus_client/apis/ocp.py +++ b/ovos_bus_client/apis/ocp.py @@ -17,7 +17,7 @@ from os.path import abspath -from ovos_utils.messagebus import get_mycroft_bus +from ovos_bus_client.util import get_mycroft_bus from ovos_bus_client.message import Message, dig_for_message diff --git a/ovos_bus_client/client/client.py b/ovos_bus_client/client/client.py index 54dfe65..9747fa1 100644 --- a/ovos_bus_client/client/client.py +++ b/ovos_bus_client/client/client.py @@ -17,7 +17,6 @@ from ovos_bus_client.conf import load_message_bus_config, MessageBusClientConf, load_gui_message_bus_config from ovos_bus_client.message import Message, CollectionMessage, GUIMessage from ovos_bus_client.session import SessionManager, Session -from ovos_bus_client.util import create_echo_function try: from mycroft_bus_client import MessageBusClient as _MessageBusClientBase @@ -450,6 +449,8 @@ def echo(): """ Echo function repeating all input from a user. """ + + from ovos_bus_client.util import create_echo_function # TODO: Deprecate in 0.1.0 message_bus_client = MessageBusClient() diff --git a/ovos_bus_client/message.py b/ovos_bus_client/message.py index 98578c6..ef3e824 100644 --- a/ovos_bus_client/message.py +++ b/ovos_bus_client/message.py @@ -26,7 +26,6 @@ from copy import deepcopy from typing import Optional from binascii import hexlify, unhexlify -from ovos_utils.gui import _GUIDict from ovos_utils.log import LOG, deprecated from ovos_utils.security import encrypt, decrypt from ovos_config.config import Configuration @@ -131,6 +130,9 @@ def as_dict(self) -> dict: @staticmethod def _json_dump(value): + + from ovos_bus_client.apis.gui import _GUIDict + def serialize_item(x): try: if hasattr(x, "serialize"): diff --git a/ovos_bus_client/util/__init__.py b/ovos_bus_client/util/__init__.py index 8646e22..077babc 100644 --- a/ovos_bus_client/util/__init__.py +++ b/ovos_bus_client/util/__init__.py @@ -15,10 +15,21 @@ """ Tools and constructs that are useful together with the messagebus. """ +import json + +from ovos_config.config import read_mycroft_config +from ovos_config.locale import get_default_lang +from ovos_utils.json_helper import merge_dict +from ovos_bus_client import MessageBusClient +from ovos_bus_client.message import dig_for_message, Message +from ovos_bus_client.session import SessionManager from ovos_bus_client.util.scheduler import EventScheduler from ovos_bus_client.util.utils import create_echo_function -from ovos_bus_client.message import dig_for_message -from ovos_bus_client.session import SessionManager + +_DEFAULT_WS_CONFIG = {"host": "0.0.0.0", + "port": 8181, + "route": "/core", + "ssl": False} def get_message_lang(message=None): @@ -31,7 +42,150 @@ def get_message_lang(message=None): # new style session lang if not lang and "session_id" in message.context or "session" in message.context: sess = SessionManager.get(message) - lang = sess.lang + return sess.lang + + return get_default_lang() + + +def get_websocket(host, port, route='/', ssl=False, threaded=True): + """ + Returns a connection to a websocket + """ + + client = MessageBusClient(host, port, route, ssl) + if threaded: + client.run_in_thread() + return client + + +def get_mycroft_bus(host: str = None, port: int = None, route: str = None, + ssl: bool = None): + """ + Returns a connection to the mycroft messagebus + """ + config = read_mycroft_config().get('websocket') or dict() + host = host or config.get('host') or _DEFAULT_WS_CONFIG['host'] + port = port or config.get('port') or _DEFAULT_WS_CONFIG['port'] + route = route or config.get('route') or _DEFAULT_WS_CONFIG['route'] + if ssl is None: + ssl = config.get('ssl') if 'ssl' in config else \ + _DEFAULT_WS_CONFIG['ssl'] + return get_websocket(host, port, route, ssl) + + +def listen_for_message(msg_type, handler, bus=None): + """ + Continuously listens and reacts to a specific messagetype on the mycroft messagebus + + NOTE: when finished you should call bus.remove(msg_type, handler) + """ + bus = bus or get_mycroft_bus() + bus.on(msg_type, handler) + return bus + + +def listen_once_for_message(msg_type, handler, bus=None): + """ + listens and reacts once to a specific messagetype on the mycroft messagebus + """ + auto_close = bus is None + bus = bus or get_mycroft_bus() + + def _handler(message): + handler(message) + if auto_close: + bus.close() + + bus.once(msg_type, _handler) + return bus + + +def wait_for_reply(message, reply_type=None, timeout=3.0, bus=None): + """Send a message and wait for a response. + + Args: + message (FakeMessage or str or dict): message object or type to send + reply_type (str): the message type of the expected reply. + Defaults to ".response". + timeout: seconds to wait before timeout, defaults to 3 + Returns: + The received message or None if the response timed out + """ + auto_close = bus is None + bus = bus or get_mycroft_bus() + if isinstance(message, str): + try: + message = json.loads(message) + except: + pass + if isinstance(message, str): + message = Message(message) + elif isinstance(message, dict): + message = Message(message["type"], + message.get("data"), + message.get("context")) + elif not isinstance(message, Message): + raise ValueError + response = bus.wait_for_response(message, reply_type, timeout) + if auto_close: + bus.close() + return response + + +def send_message(message, data=None, context=None, bus=None): + auto_close = bus is None + bus = bus or get_mycroft_bus() + if isinstance(message, str): + if isinstance(data, dict) or isinstance(context, dict): + message = Message(message, data, context) + else: + try: + message = json.loads(message) + except: + message = Message(message) + if isinstance(message, dict): + message = Message(message["type"], + message.get("data"), + message.get("context")) + if not isinstance(message, Message): + raise ValueError + bus.emit(message) + if auto_close: + bus.close() + + +def send_binary_data_message(binary_data, msg_type="mycroft.binary.data", + msg_data=None, msg_context=None, bus=None): + msg_data = msg_data or {} + msg = { + "type": msg_type, + "data": merge_dict(msg_data, {"binary": binary_data.hex()}), + "context": msg_context or None + } + send_message(msg, bus=bus) + + +def send_binary_file_message(filepath, msg_type="mycroft.binary.file", + msg_context=None, bus=None): + with open(filepath, 'rb') as f: + binary_data = f.read() + msg_data = {"path": filepath} + send_binary_data_message(binary_data, msg_type=msg_type, msg_data=msg_data, + msg_context=msg_context, bus=bus) - return lang +def decode_binary_message(message): + if isinstance(message, str): + try: # json string + message = json.loads(message) + binary_data = message.get("binary") or message["data"]["binary"] + except: # hex string + binary_data = message + elif isinstance(message, dict): + # data field or serialized message + binary_data = message.get("binary") or message["data"]["binary"] + else: + # message object + binary_data = message.data["binary"] + # decode hex string + return bytearray.fromhex(binary_data) diff --git a/ovos_bus_client/util/scheduler.py b/ovos_bus_client/util/scheduler.py index ba06481..463a0b7 100644 --- a/ovos_bus_client/util/scheduler.py +++ b/ovos_bus_client/util/scheduler.py @@ -31,8 +31,6 @@ from ovos_config.config import Configuration from ovos_config.locations import get_xdg_data_save_path, get_xdg_config_save_path from ovos_utils.log import LOG, log_deprecation -from ovos_utils.messagebus import FakeBus -from ovos_utils.events import create_basic_wrapper from ovos_utils.events import EventContainer as _EventContainer from ovos_utils.events import EventSchedulerInterface as _SchedulerInterface from ovos_bus_client.message import Message diff --git a/requirements.txt b/requirements.txt index 01af550..034868b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ovos-config >= 0.0.8, < 0.1.0 -ovos-utils >= 0.0.36, < 0.1.0 +ovos-config >= 0.0.12a6, < 0.1.0 +ovos-utils >= 0.0.37a3, < 0.1.0 websocket-client>=0.54.0 pyee>=8.1.0, < 9.0.0 diff --git a/test/unittests/test_client.py b/test/unittests/test_client.py index 563f005..91f82ae 100644 --- a/test/unittests/test_client.py +++ b/test/unittests/test_client.py @@ -46,7 +46,7 @@ def test_build_url(self): self.assertEqual(ssl_url, "wss://sslhost:443/core") def test_create_client(self): - self.assertEqual(self.client.client.url, "ws://0.0.0.0:8181/core") + self.assertEqual(self.client.client.url, "ws://127.0.0.1:8181/core") self.assertIsInstance(self.client.emitter, ExecutorEventEmitter) mock_emitter = Mock() From b10c6ba45313bc46d7717c2376ea6c40979ab175 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 23:09:15 +0000 Subject: [PATCH 71/76] Increment Version to 0.0.6a24 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 660676c..f25a5e6 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 23 +VERSION_ALPHA = 24 # END_VERSION_BLOCK From 0526bbaa8d82a64357c145aede708c8114d5442c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 23:09:41 +0000 Subject: [PATCH 72/76] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6fae0b..0d409bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.6a23](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a23) (2023-12-28) +## [0.0.6a24](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a24) (2023-12-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a22...0.0.6a23) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a23...0.0.6a24) + +**Merged pull requests:** + +- refactor/bus\_utils [\#68](https://github.com/OpenVoiceOS/ovos-bus-client/pull/68) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6a23](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a23) (2023-12-28) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a22...V0.0.6a23) **Merged pull requests:** From e4e523ce7368f272719cf9c934267a5102ba92a0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 00:05:48 +0000 Subject: [PATCH 73/76] Increment Version to 0.0.6 --- ovos_bus_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index f25a5e6..89f8a72 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 6 -VERSION_ALPHA = 24 +VERSION_ALPHA = 0 # END_VERSION_BLOCK From 730ed17cf34134f20aecbf4b9cc28ed16a782040 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 00:06:22 +0000 Subject: [PATCH 74/76] Update Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d409bf..ddae5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [0.0.6a24](https://github.com/OpenVoiceOS/ovos-bus-client/tree/0.0.6a24) (2023-12-28) +## [V0.0.6a24](https://github.com/OpenVoiceOS/ovos-bus-client/tree/V0.0.6a24) (2023-12-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a23...0.0.6a24) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-bus-client/compare/V0.0.6a23...V0.0.6a24) **Merged pull requests:** From 24583f09a3959901c6f3201bb6e19afed48a256a Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 29 Dec 2023 00:10:38 +0000 Subject: [PATCH 75/76] Update requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 034868b..deca34a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ovos-config >= 0.0.12a6, < 0.1.0 -ovos-utils >= 0.0.37a3, < 0.1.0 +ovos-config >= 0.0.12, < 0.1.0 +ovos-utils >= 0.0.37, < 0.1.0 websocket-client>=0.54.0 pyee>=8.1.0, < 9.0.0 From 44322b0e4c9ad8de3bc36743de3db1b1595d2bc6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 00:10:52 +0000 Subject: [PATCH 76/76] Increment Version to 0.0.7a1 --- ovos_bus_client/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_bus_client/version.py b/ovos_bus_client/version.py index 89f8a72..b6e6a5d 100644 --- a/ovos_bus_client/version.py +++ b/ovos_bus_client/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 0 -VERSION_BUILD = 6 -VERSION_ALPHA = 0 +VERSION_BUILD = 7 +VERSION_ALPHA = 1 # END_VERSION_BLOCK