diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..26e59a20
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "pip" # See documentation for possible values
+ directory: "/requirements" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index fc2630fb..85599026 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -23,7 +23,6 @@ jobs:
python -m pip install build wheel
- name: Install repo
run: |
- pip install -r requirements/test.txt
pip install -e .[extras]
- name: Generate coverage report
run: |
diff --git a/.github/workflows/publish_alpha.yml b/.github/workflows/publish_alpha.yml
index 3c0ef9d1..d17dc6e4 100644
--- a/.github/workflows/publish_alpha.yml
+++ b/.github/workflows/publish_alpha.yml
@@ -34,20 +34,6 @@ jobs:
runs-on: ubuntu-latest
needs: update_version
steps:
- - name: Create Release
- id: create_release
- uses: actions/create-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
- with:
- tag_name: V${{ needs.update_version.outputs.version }}
- release_name: Release ${{ needs.update_version.outputs.version }}
- body: |
- Changes in this Release
- ${{ needs.update_version.outputs.changelog }}
- draft: false
- prerelease: true
- commitish: dev
- name: Checkout Repository
uses: actions/checkout@v2
with:
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 53c1a11b..a24f52ef 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -37,7 +37,7 @@ jobs:
strategy:
max-parallel: 2
matrix:
- python-version: [ 3.7, 3.8, 3.9, "3.10" ]
+ python-version: [ 3.8, 3.9, "3.10" ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -52,7 +52,6 @@ jobs:
python -m pip install build wheel
- name: Install core repo
run: |
- pip install -r requirements/test.txt
pip install -e .[extras]
- name: Install test dependencies
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e64070d5..8291a017 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,41 +1,142 @@
# Changelog
-## [V0.0.37a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a4) (2023-12-28)
+## [Unreleased](https://github.com/OpenVoiceOS/ovos-utils/tree/HEAD)
-[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a3...V0.0.37a4)
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V...HEAD)
-**Merged pull requests:**
+**Implemented enhancements:**
-- deprecate bus utils [\#207](https://github.com/OpenVoiceOS/ovos-utils/pull/207) ([JarbasAl](https://github.com/JarbasAl))
+- feat/ocp\_stream\_utils [\#257](https://github.com/OpenVoiceOS/ovos-utils/pull/257) ([JarbasAl](https://github.com/JarbasAl))
+- new\_util/get\_sound\_duration [\#254](https://github.com/OpenVoiceOS/ovos-utils/pull/254) ([JarbasAl](https://github.com/JarbasAl))
+- Add Damerau-Levenshtein similarity matching [\#248](https://github.com/OpenVoiceOS/ovos-utils/pull/248) ([femelo](https://github.com/femelo))
+- fix/dont\_lie\_about\_being\_a\_uri [\#246](https://github.com/OpenVoiceOS/ovos-utils/pull/246) ([JarbasAl](https://github.com/JarbasAl))
-## [V0.0.37a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a3) (2023-12-28)
+**Fixed bugs:**
-[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a2...V0.0.37a3)
+- fix/plugin\_stream\_extraction [\#263](https://github.com/OpenVoiceOS/ovos-utils/pull/263) ([JarbasAl](https://github.com/JarbasAl))
+- fix/playlist\_deserialization [\#262](https://github.com/OpenVoiceOS/ovos-utils/pull/262) ([JarbasAl](https://github.com/JarbasAl))
+- fix/detect\_ovos\_gui\_app [\#258](https://github.com/OpenVoiceOS/ovos-utils/pull/258) ([JarbasAl](https://github.com/JarbasAl))
+- fix/ocp\_playlist [\#256](https://github.com/OpenVoiceOS/ovos-utils/pull/256) ([JarbasAl](https://github.com/JarbasAl))
+- fix/sound\_duration [\#255](https://github.com/OpenVoiceOS/ovos-utils/pull/255) ([JarbasAl](https://github.com/JarbasAl))
+- fix/log\_level\_cfg [\#252](https://github.com/OpenVoiceOS/ovos-utils/pull/252) ([JarbasAl](https://github.com/JarbasAl))
+- fix/log\_spam [\#251](https://github.com/OpenVoiceOS/ovos-utils/pull/251) ([JarbasAl](https://github.com/JarbasAl))
+- feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray))
**Closed issues:**
-- decouple concerns from bus/workshop [\#205](https://github.com/OpenVoiceOS/ovos-utils/issues/205)
-- ROADMAP - ovos-utils 0.1.0 [\#117](https://github.com/OpenVoiceOS/ovos-utils/issues/117)
+- "systemctl restart" not working when using with ovos-phal-plugin-system [\#259](https://github.com/OpenVoiceOS/ovos-utils/issues/259)
+- add some trivial unit test coverage that log config changes are reacted to [\#253](https://github.com/OpenVoiceOS/ovos-utils/issues/253)
+- log.py - New functions are missing test coverage [\#239](https://github.com/OpenVoiceOS/ovos-utils/issues/239)
+
+**Merged pull requests:**
+
+- log module unit test coverage [\#260](https://github.com/OpenVoiceOS/ovos-utils/pull/260) ([NeonDaniel](https://github.com/NeonDaniel))
+- deprecate/signal utils [\#249](https://github.com/OpenVoiceOS/ovos-utils/pull/249) ([JarbasAl](https://github.com/JarbasAl))
+
+## [V](https://github.com/OpenVoiceOS/ovos-utils/tree/V) (2024-03-10)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a16...V)
**Merged pull requests:**
-- LAST ALPHA [\#206](https://github.com/OpenVoiceOS/ovos-utils/pull/206) ([JarbasAl](https://github.com/JarbasAl))
+- chore\(docs\): add a long description to PyPi [\#229](https://github.com/OpenVoiceOS/ovos-utils/pull/229) ([mikejgray](https://github.com/mikejgray))
-## [V0.0.37a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a2) (2023-12-18)
+## [V0.1.0a16](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a16) (2024-03-10)
-[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a1...V0.0.37a2)
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a15...V0.1.0a16)
+
+**Merged pull requests:**
+
+- chore\(docs\): rename readme.md to README.md [\#230](https://github.com/OpenVoiceOS/ovos-utils/pull/230) ([mikejgray](https://github.com/mikejgray))
+
+## [V0.1.0a15](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a15) (2024-02-18)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a13...V0.1.0a15)
+
+**Merged pull requests:**
+
+- Option for systemd-timesyncd [\#200](https://github.com/OpenVoiceOS/ovos-utils/pull/200) ([builderjer](https://github.com/builderjer))
+
+## [V0.1.0a13](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a13) (2024-01-30)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a12...V0.1.0a13)
+
+**Merged pull requests:**
+
+- Update rapidfuzz requirement from ~=2.0 to ~=3.6 in /requirements [\#224](https://github.com/OpenVoiceOS/ovos-utils/pull/224) ([dependabot[bot]](https://github.com/apps/dependabot))
+- Update pexpect requirement from ~=4.6 to ~=4.9 in /requirements [\#223](https://github.com/OpenVoiceOS/ovos-utils/pull/223) ([dependabot[bot]](https://github.com/apps/dependabot))
+
+## [V0.1.0a12](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a12) (2024-01-25)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a11...V0.1.0a12)
**Fixed bugs:**
-- update imports for py 3.10 compat [\#202](https://github.com/OpenVoiceOS/ovos-utils/pull/202) ([JarbasAl](https://github.com/JarbasAl))
+- fix/ocp\_playlist [\#221](https://github.com/OpenVoiceOS/ovos-utils/pull/221) ([NeonJarbas](https://github.com/NeonJarbas))
-## [V0.0.37a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a1) (2023-11-08)
+## [V0.1.0a11](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a11) (2024-01-25)
-[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36...V0.0.37a1)
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a10...V0.1.0a11)
**Fixed bugs:**
-- elevate sound media role [\#201](https://github.com/OpenVoiceOS/ovos-utils/pull/201) ([emphasize](https://github.com/emphasize))
+- fix/restore deprecated OCP enums compat [\#220](https://github.com/OpenVoiceOS/ovos-utils/pull/220) ([NeonJarbas](https://github.com/NeonJarbas))
+
+## [V0.1.0a10](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a10) (2024-01-15)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a9...V0.1.0a10)
+
+**Merged pull requests:**
+
+- clarify datetime arg [\#219](https://github.com/OpenVoiceOS/ovos-utils/pull/219) ([emphasize](https://github.com/emphasize))
+
+## [V0.1.0a9](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a9) (2024-01-12)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a8...V0.1.0a9)
+
+**Implemented enhancements:**
+
+- OCP serialization [\#218](https://github.com/OpenVoiceOS/ovos-utils/pull/218) ([NeonJarbas](https://github.com/NeonJarbas))
+
+## [V0.1.0a8](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a8) (2024-01-08)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a7...V0.1.0a8)
+
+**Implemented enhancements:**
+
+- refactor/ocp\_utils [\#216](https://github.com/OpenVoiceOS/ovos-utils/pull/216) ([NeonJarbas](https://github.com/NeonJarbas))
+
+## [V0.1.0a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a7) (2024-01-06)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a6...V0.1.0a7)
+
+**Merged pull requests:**
+
+- refactor/ocp\_utils [\#215](https://github.com/OpenVoiceOS/ovos-utils/pull/215) ([NeonJarbas](https://github.com/NeonJarbas))
+
+## [V0.1.0a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a6) (2023-12-30)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a5...V0.1.0a6)
+
+**Implemented enhancements:**
+
+- Feat/ovos logs script [\#203](https://github.com/OpenVoiceOS/ovos-utils/pull/203) ([emphasize](https://github.com/emphasize))
+
+## [V0.1.0a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a5) (2023-12-30)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a4...V0.1.0a5)
+
+**Merged pull requests:**
+
+- fix/log\_spam [\#213](https://github.com/OpenVoiceOS/ovos-utils/pull/213) ([JarbasAl](https://github.com/JarbasAl))
+
+## [V0.1.0a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a4) (2023-12-29)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a3...V0.1.0a4)
+
+## [V0.1.0a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a3) (2023-12-29)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.38...V0.1.0a3)
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..7dd896e6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,87 @@
+# OVOS-utils
+
+collection of simple utilities for use across the mycroft ecosystem
+
+## Install
+
+```bash
+pip install ovos_utils
+```
+
+## Commandline scripts
+### ovos-logs
+ Small helper tool to quickly navigate the logs, create slices and quickview errors
+
+---------------
+- **ovos-logs slice [options]**
+
+ **Slice logs of a given time period. Defaults on the last service start (`-s`) until now (`-u`)**
+
+ _Different logs can be picked using the `-l` option. All logs will be included if not specified._
+ _Optionally the directory where the logs are stored (`-p`) and the file where the slices should be dumped (`-f`) can be specified._
+
+
+ _[ex: `ovos-logs slice`]_
+ _Slice all logs from service start up until now._
+
+ _[ex: `ovos-logs slice -s 17:05:20 -u 17:05:25`]_
+ _Slice all logs from 17:05:20 until 17:05:25._
+ _**no logs in that timeframe in other present logs_
+
+
+ _[ex: `ovos-logs slice -s 17:05:20 -u 17:05:25 -l skills`]_
+ _Slice skills.log from 17:05:20 until 17:05:25._
+
+ _[ex: `ovos-logs slice -s 17:05:20 -u 17:05:25 -f ~/testslice.log`]_
+ _Slice the logs from 17:05:20 until 17:05:25 on all log files and dump the slices in the file ~/testslice.log (default: `~/slice_.log`)._
+
+--------------
+
+- **ovos-logs list [-e|-w|-d|-x] [options]**
+
+ **List logs by severity (error/warning/debug/exception). A log level has to be specified - more than one can be listed**
+
+ _A start and end date can be specified using the `-s` and `-u` options. Defaults to the last service start until now._
+ _Different logs can be picked using the `-l` option. All logs will be included if not specified._
+ _Optionally, the directory where the logs are stored (`-p`) and the file where the slices should be dumped (`-f`) can be passed as arguments._
+
+ _[ex: `ovos-logs list -x`]_
+ _List the logs with level EXCEPTION (plus tracebacks) from the last service start until now._
+
+
+ _[ex: `ovos-logs list -w -e -s 20-12-2023 -l bus -l skills`]_
+ _List the logs with level WARNING and ERROR from the 20th of December 2023 until now from the logs bus.log and skills.log._
+
+---------------------
+
+- **ovos-logs reduce [options]**
+
+ **Downsize logs to a given size (in bytes) or remove entries before a given date.**
+
+ _Different logs can be included using the `-l` option. If not specified, all logs will be included._
+ _Optionally the directory where the logs are stored (`-p`) can be specified._
+
+ _[ex: `ovos-logs reduce`]_
+ _Downsize all logs to 0 bytes_
+
+ _[ex: `ovos-logs reduce -s 1000000`]_
+ _Downsize all logs to ~1MB (latest logs)_
+
+ _[ex: `ovos-logs reduce -d "1-12-2023 17:00"`]_
+ _Downsize all logs to entries after the specified date/time_
+
+ _[ex: `ovos-logs reduce -s 1000000 -l skills -l bus`]_
+ _Downsize skills.log and bus.log to ~1MB (latest logs)_
+
+---------------------
+
+- **ovos-logs show -l [servicelog]**
+
+ **Show logs**
+
+ _[ex: `ovos-logs show -l bus`]_
+ _Show the logs from bus.log._
+
+ _[ex: wrong servicelog]_
+ _**logs shown depending on the logs present in the folder_
+
diff --git a/examples/change_wakeword.py b/examples/change_wakeword.py
deleted file mode 100644
index c8e1d113..00000000
--- a/examples/change_wakeword.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from ovos_utils.configuration import update_mycroft_config
-from ovos_utils.lang.phonemes import get_phonemes
-
-
-def create_wakeword(word, sensitivity):
- # sensitivity is a bitch to do automatically
- # IDEA make some web ui or whatever to tweak it experimentally
- phonemes = get_phonemes(word)
- config = {
- "listener": {
- "wake_word": word
- },
- word: {
- "andromeda": {
- "module": "pocketsphinx",
- "phonemes": phonemes,
- "sample_rate": 16000,
- "threshold": sensitivity,
- "lang": "en-us"
- }
- }
- }
- update_mycroft_config(config)
-
-
-create_wakeword("andromeda", "1e-25")
\ No newline at end of file
diff --git a/examples/core_status.py b/examples/core_status.py
deleted file mode 100644
index 00423e31..00000000
--- a/examples/core_status.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from ovos_utils.skills import skills_loaded
-from time import sleep
-
-loaded = False
-while not loaded:
- loaded = skills_loaded()
- sleep(0.5)
-
-print("Skills all loaded!")
-
diff --git a/examples/count_utterances.py b/examples/count_utterances.py
deleted file mode 100644
index 8275326f..00000000
--- a/examples/count_utterances.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from ovos_utils.messagebus import listen_for_message
-from ovos_utils.log import LOG
-from ovos_utils import wait_for_exit_signal
-
-heard = 0
-spoken = 0
-
-
-def handle_speak(message):
- global spoken
- spoken += 1
- LOG.info("Mycroft spoke {n} sentences since start".format(n=spoken))
-
-
-def handle_hear(message):
- global heard
- heard += 1
- LOG.info("Mycroft responded to {n} sentences since start".format(n=heard))
-
-
-bus = listen_for_message("speak", handle_speak)
-listen_for_message("recognize_loop:utterance", handle_hear, bus=bus) # re utilize bus
-
-wait_for_exit_signal() # wait for ctrl+c
-
-# cleanup is a good practice!
-bus.remove_all_listeners("speak")
-bus.remove_all_listeners("recognize_loop:utterance")
-bus.close()
diff --git a/examples/fuzzywuzzy_intent_engine.py b/examples/fuzzywuzzy_intent_engine.py
deleted file mode 100644
index dd0d9d11..00000000
--- a/examples/fuzzywuzzy_intent_engine.py
+++ /dev/null
@@ -1,83 +0,0 @@
-from ovos_utils.intents.engines import BaseIntentEngine
-from ovos_utils.skills.intent_provider import IntentEngineSkill
-
-from mycroft.configuration.config import Configuration
-from mycroft.skills.core import MycroftSkill, Message
-
-from fuzzywuzzy import process
-
-
-class FuzzyEngine(BaseIntentEngine):
- def __init__(self):
- self.name = "fuzzy"
- BaseIntentEngine.__init__(self, self.name)
- self.config = Configuration.get().get(self.name, {})
-
- def calc_intent(self, query):
- """ return best intent for this query """
- data = {"conf": 0,
- "utterance": query,
- "name": None}
-
- best_match = None
- best_score = 0
- for intent in self.intent_samples:
- samples = self.intent_samples[intent]
- match, score = process.extractOne(query, samples)
- if score > best_score:
- best_score = score
- best_match = (intent, match)
-
- if best_match is not None:
- intent, match = best_match
- data["name"] = intent
- data["match"] = match
- data["conf"] = best_score
-
- entities = []
- for entity in self.entity_samples:
- samples = self.entity_samples
- for s in samples:
- if s in query:
- entities.append((entity, s))
-
- data["entities"] = [{entity: s} for entity, s in entities]
- return data
-
-
-# engine skill for mycroft
-class FuzzyEngineSkill(IntentEngineSkill):
- def initialize(self):
- priority = 5
- engine = FuzzyEngine()
- self.bind_engine(engine, priority)
-
-
-class FuzzySkill(MycroftSkill):
- def register_fuzzy_intent(self, name, samples, handler):
- message = "fuzzy:register_intent"
- name = str(self.skill_id) + ':' + name
- data = {"name": name, "samples": samples}
-
- self.bus.emit(Message(message, data))
- self.add_event(name, handler, 'mycroft.skill.handler')
-
- def register_fuzzy_entity(self, name, samples):
- message = "fuzzy:register_entity"
- name = str(self.skill_id) + ':' + name
- data = {"name": name, "samples": samples}
- self.bus.emit(Message(message, data))
-
-
-def create_skill():
- return FuzzySkill()
-
-# install this file as a regular skill (fuzzy_engine/__init__.py)
-
-## to import and create fuzzy skills
-
-# from os.path import dirname
-# skills_dir = dirname(dirname(__file__))
-# import sys
-# sys.path.append(skills_dir)
-# from fuzzy_engine import PadaosSkill
\ No newline at end of file
diff --git a/examples/gui_tracking.py b/examples/gui_tracking.py
deleted file mode 100644
index 58f27a63..00000000
--- a/examples/gui_tracking.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from ovos_utils.gui import GUITracker
-from ovos_utils import wait_for_exit_signal
-
-
-class MyGUIEventTracker(GUITracker):
- # GUI event handlers
- # user can/should subclass this
- def on_idle(self, namespace):
- print("IDLE", namespace)
- timestamp = self.idle_ts
-
- def on_active(self, namespace):
- # NOTE: page has not been loaded yet
- # event will fire right after this one
- print("ACTIVE", namespace)
- # check namespace values, they should all be set before this event
- values = self.gui_values[namespace]
-
- def on_new_page(self, page, namespace, index):
- print("NEW PAGE", namespace, index, namespace)
- # check all loaded pages
- for n in self.gui_pages: # list of named tuples
- nspace = n.name # namespace / skill_id
- pages = n.pages # ordered list of page uris
-
- def on_gui_value(self, namespace, key, value):
- # WARNING this will pollute logs quite a lot, and you will get
- # duplicates, better to check values on a different event,
- # demonstrated in on_active
- print("VALUE", namespace, key, value)
-
-
-g = MyGUIEventTracker()
-
-print("device has screen:", g.can_display())
-print("mycroft-gui installed:", g.is_gui_installed())
-print("gui connected:", g.is_gui_connected())
-
-
-# check registered idle screens
-print("Registered idle screens:")
-for name in g.idle_screens:
- namespace = g.idle_screens[name]
- print(" - ", name, ":", namespace)
-
-
-# just block listening for events until ctrl + C
-wait_for_exit_signal()
diff --git a/examples/intent_api.py b/examples/intent_api.py
deleted file mode 100644
index 733dac9f..00000000
--- a/examples/intent_api.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from ovos_utils.intents import IntentQueryApi
-from pprint import pprint
-
-
-intents = IntentQueryApi()
-
-pprint(intents.get_skill("who are you"))
-pprint(intents.get_skill("set volume to 100%"))
-
-exit()
-# loaded skills
-pprint(intents.get_skills_manifest())
-pprint(intents.get_active_skills())
-
-# intent parsing
-pprint(intents.get_adapt_intent("who are you"))
-pprint(intents.get_padatious_intent("who are you"))
-pprint(intents.get_intent("who are you")) # intent that will trigger
-
-# skill from utterance
-pprint(intents.get_skill("who are you"))
-
-# registered intents
-pprint(intents.get_adapt_manifest())
-pprint(intents.get_padatious_manifest())
-pprint(intents.get_intent_manifest()) # all of the above
-
-# registered vocab
-pprint(intents.get_entities_manifest()) # padatious entities / .entity files
-pprint(intents.get_vocab_manifest()) # adapt vocab / .voc files
-pprint(intents.get_regex_manifest()) # adapt regex / .rx files
-pprint(intents.get_keywords_manifest()) # all of the above
diff --git a/examples/live_translate_satellite.py b/examples/live_translate_satellite.py
deleted file mode 100644
index 40a7b88d..00000000
--- a/examples/live_translate_satellite.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from ovos_utils.messagebus import get_mycroft_bus, listen_for_message
-from ovos_utils import wait_for_exit_signal
-from ovos_utils.lang.translate import say_in_language
-
-# from ovos_utils.lang.translate import translate_to_mp3
-# from ovos_utils.sound import play_mp3
-
-bus_ip = "0.0.0.0" # enter a remote ip here, remember bus is unencrypted! careful with opening firewalls
-bus = get_mycroft_bus(host=bus_ip)
-
-TARGET_LANG = "pt"
-
-
-def translate(message):
- utterance = message.data["utterance"]
- say_in_language(utterance, lang=TARGET_LANG) # will play .mp3 directly
-
- # if you need more control
- # path = translate_to_mp3(utterance, lang=TARGET_LANG)
- # play_mp3(path, cmd="play %1") # using sox
-
-
-listen_for_message("speak", translate, bus=bus)
-
-
-wait_for_exit_signal() # wait for ctrl+c
-
-bus.remove_all_listeners("speak")
-bus.close()
diff --git a/examples/location.py b/examples/location.py
deleted file mode 100644
index 6e8dfda5..00000000
--- a/examples/location.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from ovos_utils.location import geolocate, reverse_geolocate
-from pprint import pprint
-
-
-full_addr = "798 N 1415 Rd, Lawrence, KS 66049, United States"
-vague_addr = "Lawrence Kansas"
-country = "Portugal"
-
-data = geolocate(country)
-pprint(data)
-
-
-lat = 38.9730682373638
-lon = -95.2361831665156
-
-data = reverse_geolocate(lat, lon)
-
-pprint(data)
\ No newline at end of file
diff --git a/examples/mark1_animations.py b/examples/mark1_animations.py
deleted file mode 100644
index 688c2056..00000000
--- a/examples/mark1_animations.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from ovos_utils.enclosure.mark1.faceplate.icons import HollowHeartIcon, \
- HeartIcon, SkullIcon, Boat
-from ovos_utils.enclosure.mark1.faceplate.animations import LeftRight, \
- HorizontalScroll
-
-
-class SailingBoat(Boat, HorizontalScroll):
- pass
-
-
-class MovingHeart(HeartIcon, LeftRight):
- pass
-
-
-class MovingHeart2(HollowHeartIcon, LeftRight):
- pass
-
-
-class MovingSkull(SkullIcon, LeftRight):
- pass
-
-
-from time import sleep
-from ovos_utils.messagebus import get_mycroft_bus
-
-
-bus = get_mycroft_bus("192.168.1.70")
-
-boat = MovingHeart(bus=bus)
-
-for faceplate in boat:
- faceplate.display()
- sleep(0.5)
\ No newline at end of file
diff --git a/examples/mark1_game_of_life.py b/examples/mark1_game_of_life.py
deleted file mode 100644
index 12f48cdc..00000000
--- a/examples/mark1_game_of_life.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from ovos_utils.enclosure.mark1.faceplate.cellular_automaton import GoL
-from ovos_utils.messagebus import get_mycroft_bus
-
-bus = get_mycroft_bus("192.168.1.70")
-
-
-game_of_life = GoL(bus=bus)
-
-
-def handle_new_frame(grid):
- grid.display(invert=False)
-
-
-game_of_life.run(0.5, handle_new_frame)
\ No newline at end of file
diff --git a/examples/mark1_image_rotate.py b/examples/mark1_image_rotate.py
deleted file mode 100644
index 4c43c105..00000000
--- a/examples/mark1_image_rotate.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from ovos_utils.enclosure.mark1.faceplate.icons import Boat, MusicIcon, StormIcon, \
- SnowIcon, SunnyIcon, PartlyCloudyIcon, PlusIcon, SkullIcon, CrossIcon, \
- HollowHeartIcon, HeartIcon, DeadFishIcon, InfoIcon, \
- ArrowLeftIcon, JarbasAI, WarningIcon
-from ovos_utils.messagebus import get_mycroft_bus
-from time import sleep
-
-bus = get_mycroft_bus("192.168.1.70")
-
-images = [Boat(bus=bus),
- MusicIcon(bus=bus),
- StormIcon(bus=bus),
- SunnyIcon(bus=bus),
- PartlyCloudyIcon(bus=bus),
- PlusIcon(bus=bus),
- CrossIcon(bus=bus),
- SnowIcon(bus=bus),
- SkullIcon(bus=bus),
- HeartIcon(bus=bus),
- HollowHeartIcon(bus=bus),
- ArrowLeftIcon(bus=bus),
- JarbasAI(bus=bus),
- WarningIcon(bus=bus),
- InfoIcon(bus=bus),
- DeadFishIcon(bus=bus)]
-
-from ovos_utils.enclosure.mark1.faceplate.icons import SpaceInvader1, \
- SpaceInvader2, SpaceInvader3, SpaceInvader4
-images = [SpaceInvader1(bus=bus),
- SpaceInvader2(bus=bus),
- SpaceInvader3(bus=bus),
- SpaceInvader4(bus=bus)]
-for faceplate in images:
- faceplate.print()
- faceplate.display()
- sleep(3)
diff --git a/examples/mark1_pixel_wise.py b/examples/mark1_pixel_wise.py
deleted file mode 100644
index c4cc4eb9..00000000
--- a/examples/mark1_pixel_wise.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from ovos_utils.enclosure.mark1.faceplate.icons import FaceplateGrid
-from ovos_utils.messagebus import get_mycroft_bus
-from time import sleep
-
-
-bus = get_mycroft_bus("192.168.1.70")
-
-
-grid = FaceplateGrid(bus=bus)
-
-# grid is white
-grid.display()
-sleep(2)
-
-# grid is black
-grid.invert()
-grid.display()
-sleep(2)
-
-# read pixels
-n_pixels = len(grid)
-assert grid.height == 8
-assert grid.width == 32
-assert grid[1][1] == 1 # 1 == black / pixel off
-
-try:
- grid[grid.height]
-except IndexError:
- pass # 0 <= i < grid.height
-try:
- grid[grid.height-1][grid.width]
-except IndexError:
- pass # 0 <= j < grid.width
-
-# create a dotted line
-grid[4][0] = 0 # white
-grid[4][5] = 0 # white
-grid[4][10] = 0 # white
-grid[4][15] = 0 # white
-grid[4][20] = 0 # white
-grid[4][25] = 0 # white
-grid[4][30] = 0 # white
-grid.display()
-
-# optionally disable invert
-# if 1 == white / pixel on
-# makes more sense to you
-sleep(2)
-grid.display(invert=False)
-
diff --git a/examples/mark1_space_invader.py b/examples/mark1_space_invader.py
deleted file mode 100644
index 906f990e..00000000
--- a/examples/mark1_space_invader.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from ovos_utils.enclosure.mark1.faceplate.cellular_automaton import \
- SpaceInvader
-from ovos_utils.messagebus import get_mycroft_bus
-from time import sleep
-
-bus = get_mycroft_bus("192.168.1.70")
-
-game_of_life = SpaceInvader(bus=bus)
-
-for grid in game_of_life:
- grid.display(invert=False)
- sleep(2)
diff --git a/examples/music.txt b/examples/music.txt
deleted file mode 100644
index b375a00c..00000000
--- a/examples/music.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-XXXXXXXXXXXXXX XXXXXXXXXXXXX
-XXXXXXXXXXXXXX XXXXXXXXXXXXX
-XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX
-XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX
-XXXXXXXXXXXXX XX XXXXXXXXXXXXX
-XXXXXXXXXXXX X XXXXXXXXXXXXX
-XXXXXXXXXXXXX XXX XXXXXXXXXXXXXX
\ No newline at end of file
diff --git a/examples/padaos_intent_engine.py b/examples/padaos_intent_engine.py
deleted file mode 100644
index 15eb223a..00000000
--- a/examples/padaos_intent_engine.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from ovos_utils.intents.engines import BaseIntentEngine
-from ovos_utils.skills.intent_provider import IntentEngineSkill
-
-from mycroft.configuration.config import Configuration
-from mycroft.skills.core import MycroftSkill, Message
-
-from padaos import IntentContainer
-
-
-class PadaosEngine(BaseIntentEngine):
- def __init__(self):
- self.name = "padaos"
- BaseIntentEngine.__init__(self, self.name)
- self.config = Configuration.get().get(self.name, {})
- self.container = IntentContainer()
-
- def add_intent(self, name, samples):
- self.container.add_intent(name, samples)
-
- def remove_intent(self, name):
- self.container.remove_intent(name)
-
- def add_entity(self, name, samples):
- self.container.add_entity(name, samples)
-
- def remove_entity(self, name):
- self.container.remove_entity(name)
-
- def train(self, single_thread=False):
- """ train all registered intents and entities"""
- # Padaos is simply regex, it handles this when registering
- pass
-
- def calc_intent(self, query):
- """ return best intent for this query """
- data = {"conf": 0,
- "utterance": query,
- "name": None}
- data.update(self.container.calc_intent(query))
- return data
-
-
-# engine skill for mycroft
-class PadaosEngineSkill(IntentEngineSkill):
- def initialize(self):
- priority = 4
- engine = PadaosEngine()
- self.bind_engine(engine, priority)
-
-
-class PadaosSkill(MycroftSkill):
- def register_padaos_intent(self, name, samples, handler):
- message = "padaos:register_intent"
- name = str(self.skill_id) + ':' + name
- data = {"name": name, "samples": samples}
-
- self.bus.emit(Message(message, data))
- self.add_event(name, handler, 'mycroft.skill.handler')
-
- def register_padaos_entity(self, name, samples):
- message = "padaos:register_entity"
- name = str(self.skill_id) + ':' + name
- data = {"name": name, "samples": samples}
- self.bus.emit(Message(message, data))
-
-
-def create_skill():
- return PadaosSkill()
-
-
-# install this file as a regular skill (padaos_engine/__init__.py)
-
-## to import and create padaos skills
-
-# from os.path import dirname
-# skills_dir = dirname(dirname(__file__))
-# import sys
-# sys.path.append(skills_dir)
-# from padaos_engine import PadaosSkill
\ No newline at end of file
diff --git a/examples/private_settings.py b/examples/private_settings.py
deleted file mode 100644
index 959ea93b..00000000
--- a/examples/private_settings.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from ovos_utils.skills.settings import PrivateSettings
-
-
-with PrivateSettings("testskill.jarbasai") as settings:
- print(settings.path) # ~/.cache/json_database/testskill.jarbasai.json
- settings["key"] = "value"
-
- meta = settings.settingsmeta
- # can be used for displaying in GUI
- """
- {'skillMetadata': {'sections': [{'fields': [{'label': 'Key',
- 'name': 'key',
- 'type': 'text',
- 'value': 'value'}],
- 'name': 'testskill.jarbasai'}]}}
- """
-
- # auto saved when leaving "with" context
- # you can also manually call settings.store() if not using "with" context
\ No newline at end of file
diff --git a/examples/remote_skill_settings.py b/examples/remote_skill_settings.py
deleted file mode 100644
index 293b8039..00000000
--- a/examples/remote_skill_settings.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from ovos_utils.skills.settings import get_all_remote_settings, get_remote_settings
-
-"""
-WARNING: selene backend does not use a proper skill_id, if you have
-skills with same name but different author settings will overwrite each
-other on the backend
-
-2 way sync with Selene is also not implemented and causes a lot of issues,
-these problems have been reported over 9 months ago (and counting)
-
-I strongly recommend you validate returned data as much as possible,
-maybe ask the user to confirm any action, DO NOT automate settings sync
-
-skill matching is currently done by checking "if {skill} in string"
-once mycroft fixes it on their side this will start using a proper
-unique identifier
-
-THIS METHOD IS NOT ALWAYS SAFE
-"""
-
-# get raw skill settings payload
-all_remote_settings = get_all_remote_settings()
-"""
-{'@0e813c09-8dbc-40ed-974c-5705274a7b55|mycroft-npr-news|20.08': {'custom_url': '',
- 'station': 'not_set',
- 'use_curl': True},
- '@0e813c09-8dbc-40ed-974c-5705274a7b55|mycroft-pairing|20.08': None,
- '@0e813c09-8dbc-40ed-974c-5705274a7b55|mycroft-volume|20.08': {'ducking': True},
- (...)
- 'mycroft-weather|20.08': {'units': 'default'},
- 'mycroft-wiki|20.08': None}
-"""
-
-# search remote settings for a specific skill
-voip_remote_settings = get_remote_settings("skill-voip")
-"""
-{'add_contact': False,
- 'auto_answer': True,
- 'auto_reject': False,
- 'auto_speech': 'I am busy, call again later',
- 'contact_address': 'user@sipxcom.com',
- 'contact_name': 'name here',
- 'delete_contact': False,
- 'gateway': 'sipxcom.com',
- 'password': 'SECRET',
- 'sipxcom_gateway': 'https://sipx.mattkeys.net',
- 'sipxcom_password': 'secret',
- 'sipxcom_sync': False,
- 'sipxcom_user': 'MattKeys',
- 'user': 'user'}
-"""
\ No newline at end of file
diff --git a/examples/send_file_over_bus.py b/examples/send_file_over_bus.py
deleted file mode 100644
index c7a602ea..00000000
--- a/examples/send_file_over_bus.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from ovos_utils.messagebus import send_binary_data_message, \
- send_binary_file_message, decode_binary_message, listen_for_message
-
-from time import sleep
-import json
-from os.path import dirname, join
-
-# random file
-my_file_path = join(dirname(__file__), "music.txt")
-with open(my_file_path, "rb") as f:
- original_binary = f.read()
-
-
-def receive_file(message):
- print("Receiving file")
- path = message.data["path"]
- print(path)
- hex_data = message.data["binary"]
-
- # all accepted decode formats
- binary_data = decode_binary_message(message)
- print(binary_data == original_binary)
- binary_data = decode_binary_message(hex_data)
- print(binary_data == original_binary)
- binary_data = decode_binary_message(message.data)
- print(binary_data == original_binary)
- binary_data = decode_binary_message(json.dumps(message.data))
- print(binary_data == original_binary)
- binary_data = decode_binary_message(message.serialize())
- print(binary_data == original_binary)
-
-
-listen_for_message("mycroft.binary.file", receive_file)
-sleep(1)
-send_binary_file_message(my_file_path)
-
-
-def receive_binary(message):
- print("Receiving binary data")
- binary_data = decode_binary_message(message)
- print(binary_data == original_binary)
-
-
-listen_for_message("mycroft.binary.data", receive_binary)
-sleep(1)
-send_binary_data_message(original_binary)
diff --git a/examples/translation_utils.py b/examples/translation_utils.py
deleted file mode 100644
index 7abe1534..00000000
--- a/examples/translation_utils.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from ovos_utils.lang import detect_lang, translate_text
-
-# detecting language
-detect_lang("olá eu chamo-me joaquim") # "pt"
-detect_lang("olá eu chamo-me joaquim", return_dict=True)
-"""{'confidence': 0.9999939001351439, 'language': 'pt'}"""
-
-detect_lang("hello world") # "en"
-detect_lang("This piece of text is in English. Този текст е на Български.", return_dict=True)
-"""{'confidence': 0.28571342657428966, 'language': 'en'}"""
-
-# translating text
-# - source lang will be auto detected using utils above
-# - default target language is english
-translate_text("olá eu chamo-me joaquim")
-"""Hello I call myself joaquim"""
-
-# - you should specify source lang whenever possible to save 1 api call
-translate_text("olá eu chamo-me joaquim", source_lang="pt", lang="es")
-"""Hola, me llamo Joaquim"""
diff --git a/examples/universal_chat.py b/examples/universal_chat.py
deleted file mode 100644
index c764bb1d..00000000
--- a/examples/universal_chat.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from ovos_utils.messagebus import send_message, listen_for_message
-from ovos_utils.lang.translate import translate_text
-from ovos_utils.lang.detect import detect_lang
-from ovos_utils.log import LOG
-
-
-OUTPUT_LANG = "pt" # received messages will be in this language
-MYCROFT_LANG = "en" # mycroft is configured in this language
-
-
-def handle_speak(message):
- utt = message.data["utterance"]
- utt = translate_text(utt, "pt") # source lang is auto detected
- print("MYCROFT:", utt)
-
-
-bus = listen_for_message("speak", handle_speak)
-
-print("Write in any language and mycroft will answer in {lang}".format(lang=OUTPUT_LANG))
-
-
-while True:
- try:
- utt = input("YOU:")
- lang = detect_lang("utt") # source lang is auto detected, this is optional
- if lang != MYCROFT_LANG:
- utt = translate_text(utt)
- send_message("recognizer_loop:utterance",
- {"utterances": [utt]},
- bus=bus # re-utilize the bus connection
- )
- except KeyboardInterrupt:
- break
- except Exception as e:
- LOG.exception(e)
-
-bus.remove_all_listeners("speak")
-bus.close()
diff --git a/examples/watchdog.py b/examples/watchdog.py
deleted file mode 100644
index 5e20fe25..00000000
--- a/examples/watchdog.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from ovos_utils.messagebus import send_message
-from ovos_utils.log import LOG
-from ovos_utils import create_daemon, wait_for_exit_signal
-import random
-from time import sleep
-
-
-def alert():
- LOG.info("Alerting user of some event using Mycroft")
- send_message("speak", {"utterance": "Alert! something happened"})
-
-
-def did_something_happen():
- while True:
- if random.choice([True, False]):
- alert()
- sleep(10)
-
-
-create_daemon(did_something_happen) # check for something in background
-wait_for_exit_signal() # wait for ctrl+c
\ No newline at end of file
diff --git a/ovos_utils/__init__.py b/ovos_utils/__init__.py
index 1fa3e1f7..98912ab8 100644
--- a/ovos_utils/__init__.py
+++ b/ovos_utils/__init__.py
@@ -13,17 +13,12 @@
import datetime
import re
from functools import lru_cache, wraps
-from os.path import isdir, join
from threading import Thread, Event
-from time import monotonic_ns
-from time import sleep
+from time import monotonic_ns, sleep
import kthread
-# TODO: Deprecate below imports
-from ovos_utils.file_utils import resolve_ovos_resource_file, resolve_resource_file
-from ovos_utils.network_utils import get_ip, get_external_ip, is_connected_dns, is_connected_http, is_connected
-from ovos_utils.log import LOG, deprecated
+from ovos_utils.log import LOG
def threaded_timeout(timeout=5):
@@ -33,7 +28,7 @@ def threaded_timeout(timeout=5):
Adapted from https://github.com/OpenJarbas/InGeo
@param timeout: Timeout in seconds to wait before terminating the process
"""
-
+
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
@@ -66,41 +61,11 @@ def func_wrapped():
class classproperty(property):
"""Decorator for a Class-level property.
Credit to Denis Rhyzhkov on Stackoverflow: https://stackoverflow.com/a/13624858/1280629"""
+
def __get__(self, owner_self, owner_cls):
return self.fget(owner_cls)
-@deprecated("Anything depending on `mycroft`"
- "should install `ovos-core` as a dependency", "0.1.0")
-def ensure_mycroft_import():
- # TODO: Deprecate in 0.1.0
- try:
- import mycroft
- except ImportError:
- import sys
- from ovos_utils import get_mycroft_root
- MYCROFT_ROOT_PATH = get_mycroft_root()
- if MYCROFT_ROOT_PATH is not None:
- sys.path.append(MYCROFT_ROOT_PATH)
- else:
- raise
-
-
-@deprecated("Code should import from the current"
- "namespace; other system paths are irrelevant.", "0.1.0")
-def get_mycroft_root():
- # TODO: Deprecate in 0.1.0
- paths = [
- "/opt/venvs/mycroft-core/lib/python3.7/site-packages/", # mark1/2
- "/opt/venvs/mycroft-core/lib/python3.4/site-packages/ ", # old mark1 installs
- "/home/pi/mycroft-core" # picroft
- ]
- for p in paths:
- if isdir(join(p, "mycroft")):
- return p
- return None
-
-
def timed_lru_cache(
_func=None, *, seconds: int = 7000, maxsize: int = 128, typed: bool = False
):
diff --git a/ovos_utils/configuration.py b/ovos_utils/configuration.py
deleted file mode 100644
index 01d95093..00000000
--- a/ovos_utils/configuration.py
+++ /dev/null
@@ -1,377 +0,0 @@
-import json
-from os import makedirs
-from os.path import join, expanduser, exists, isfile
-
-import ovos_utils.xdg_utils as xdg
-from ovos_utils.log import LOG, deprecated
-
-
-# TODO - deprecate this submodule in 0.1.0
-# note that a couple of these are also used inside ovos-utils
-# perhaps those usages should also move into workshop ?
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_default_lang():
- try:
- from ovos_config.locale import get_default_lang as _get
- return _get()
- except ImportError:
- return read_mycroft_config().get("lang", "en-us")
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def find_user_config():
- try:
- from ovos_config.locations import find_user_config as _get
- return _get()
- except ImportError:
-
- return join(get_xdg_config_save_path(), get_config_filename())
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_webcache_location():
- return join(get_xdg_config_save_path(), 'web_cache.json')
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_config_locations(default=True, web_cache=True, system=True,
- old_user=True, user=True):
- try:
- from ovos_config.locations import get_config_locations as _get
- return _get(default, web_cache, system, old_user, user)
- except ImportError:
- locs = []
- ovos_cfg = get_ovos_config()
- if default:
- locs.append(ovos_cfg["default_config_path"])
- if system:
- locs.append(f"/etc/{ovos_cfg['base_folder']}/{ovos_cfg['config_filename']}")
- if web_cache:
- locs.append(get_webcache_location())
- if old_user:
- locs.append(f"~/.{ovos_cfg['base_folder']}/{ovos_cfg['config_filename']}")
- if user:
- locs.append(f"{get_xdg_config_save_path()}/{ovos_cfg['config_filename']}")
- return locs
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_ovos_config():
- try:
- from ovos_config.meta import get_ovos_config as _get
- return _get()
- except ImportError:
- return {"xdg": True,
- "base_folder": "mycroft",
- "config_filename": "mycroft.conf"}
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_xdg_base():
- try:
- from ovos_config.meta import get_xdg_base as _get
- return _get()
- except ImportError:
- return "mycroft"
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_xdg_config_locations():
- # This includes both the user config and
- # /etc/xdg/mycroft/mycroft.conf
- xdg_paths = list(reversed(
- [join(p, get_config_filename())
- for p in get_xdg_config_dirs()]
- ))
- return xdg_paths
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_xdg_data_dirs():
- try:
- from ovos_config.locations import get_xdg_data_dirs as _get
- return _get()
- except ImportError:
- return [expanduser("~/.local/share/mycroft")]
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_xdg_config_dirs(folder=None):
- try:
- from ovos_config.locations import get_xdg_config_dirs as _get
- return _get()
- except ImportError:
- folder = folder or get_xdg_base()
- xdg_dirs = xdg.xdg_config_dirs() + [xdg.xdg_config_home()]
- return [join(path, folder) for path in xdg_dirs]
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_xdg_cache_save_path(folder=None):
- try:
- from ovos_config.locations import get_xdg_cache_save_path as _get
- return _get()
- except ImportError:
- folder = folder or get_xdg_base()
- return join(xdg.xdg_cache_home(), folder)
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_xdg_data_save_path():
- try:
- from ovos_config.locations import get_xdg_data_save_path as _get
- return _get()
- except ImportError:
- return expanduser("~/.local/share/mycroft")
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_xdg_config_save_path():
- try:
- from ovos_config.locations import get_xdg_config_save_path as _get
- return _get()
- except ImportError:
- return expanduser("~/.config/mycroft")
-
-
-@deprecated("XDG is always used.", "0.1.0")
-def is_using_xdg():
- """ DEPRECATED """
- return True
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def set_xdg_base(*args, **kwargs):
- try:
- from ovos_config.meta import set_xdg_base as _set
- _set(*args, **kwargs)
- except:
- pass
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def set_config_filename(*args, **kwargs):
- try:
- from ovos_config.meta import config_filename as _set
- _set(*args, **kwargs)
- except:
- pass
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_config_filename():
- try:
- from ovos_config.locale import get_config_filename as _get
- return _get()
- except ImportError:
- return "mycroft.conf"
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def get_ovos_default_config_paths():
- try:
- from ovos_config.meta import get_ovos_default_config_paths as _get
- return _get()
- except:
- return ["/etc/OpenVoiceOS/ovos.conf"]
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def read_mycroft_config():
- try:
- from ovos_config import Configuration
- return Configuration()
- except ImportError:
- pass
- path = expanduser(f"~/.config/mycroft/mycroft.conf")
- if isfile(path):
- with open(path) as f:
- return json.load(f)
- return {
- # TODO - default cfg
- "lang": "en-us"
- }
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def update_mycroft_config(config, path=None, bus=None):
- try:
- from ovos_config.config import update_mycroft_config as _update
- _update(config, path, bus)
- except ImportError:
- pass
- # save in default user location
- path = expanduser(f"~/.config/mycroft")
- makedirs(path, exist_ok=True)
- with open(f"{path}/mycroft.conf", "w") as f:
- json.dump(config, f, indent=2)
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def set_default_config(*args, **kwargs):
- try:
- from ovos_config.meta import set_default_config as _set
- _set(*args, **kwargs)
- except:
- pass
-
-
-@deprecated("configuration moved to the `ovos_config` package.", "0.1.0")
-def save_ovos_core_config(*args, **kwargs):
- try:
- from ovos_config.meta import save_ovos_config as _set
- _set(*args, **kwargs)
- except:
- pass
-
-
-try:
- from ovos_config.models import (
- LocalConf,
- ReadOnlyConfig,
- MycroftUserConfig,
- MycroftDefaultConfig,
- MycroftSystemConfig,
- MycroftXDGConfig
- )
-except ImportError:
- LOG.warning("configuration classes moved to the `ovos_config.models` package. "
- "This submodule will be removed in ovos_utils 0.1.0")
- from combo_lock import NamedLock
- import yaml
- from ovos_utils.json_helper import load_commented_json, merge_dict
-
-
- class LocalConf(dict):
- """Config dictionary from file."""
- allow_overwrite = True
- # lock is shared among all subclasses,
- # regardless of what file is being edited only one file should change at a time
- # this ensure orderly behaviour in anything monitoring changes,
- # eg FileWatcher util, configuration.patch bus handlers
- __lock = NamedLock("ovos_config")
-
- def __init__(self, path):
- super().__init__(self)
- self.path = path
- if path:
- self.load_local(path)
-
- def _get_file_format(self, path=None):
- """The config file format
- supported file extensions:
- - json (.json)
- - commented json (.conf)
- - yaml (.yaml/.yml)
-
- returns "yaml" or "json"
- """
- path = path or self.path
- if not path:
- return "dict"
- if path.endswith(".yml") or path.endswith(".yaml"):
- return "yaml"
- else:
- return "json"
-
- def load_local(self, path=None):
- """Load local json file into self.
-
- Args:
- path (str): file to load
- """
- path = path or self.path
- if not path:
- LOG.error(f"in memory configuration, nothing to load")
- return
- if exists(path) and isfile(path):
- with self.__lock:
- try:
- if self._get_file_format(path) == "yaml":
- with open(path) as f:
- config = yaml.safe_load(f)
- else:
- config = load_commented_json(path)
- if config:
- for key in config:
- self.__setitem__(key, config[key])
- LOG.debug(f"Configuration {path} loaded")
- else:
- LOG.debug(f"Empty config found at: {path}")
- except Exception as e:
- LOG.exception(f"Error loading configuration '{path}'")
- else:
- LOG.debug(f"Configuration '{path}' not defined, skipping")
-
- def reload(self):
- self.load_local(self.path)
-
- def store(self, path=None):
- path = path or self.path
- if not path:
- LOG.error(f"in memory configuration, no save location")
- return
- with self.__lock:
- if self._get_file_format(path) == "yaml":
- with open(path, 'w+') as f:
- yaml.dump(dict(self), f, allow_unicode=True,
- default_flow_style=False, sort_keys=False)
- else:
- with open(path, 'w+') as f:
- json.dump(self, f, indent=2)
-
- def merge(self, conf):
- merge_dict(self, conf)
-
-
- class ReadOnlyConfig(LocalConf):
- """ read only """
-
- def __init__(self, path, allow_overwrite=False):
- super().__init__(path)
- self.allow_overwrite = allow_overwrite
-
- def reload(self):
- old = self.allow_overwrite
- self.allow_overwrite = True
- super().reload()
- self.allow_overwrite = old
-
- def __setitem__(self, key, value):
- if not self.allow_overwrite:
- raise PermissionError(f"{self.path} is read only! it can not be modified at runtime")
- super().__setitem__(key, value)
-
- def merge(self, *args, **kwargs):
- if not self.allow_overwrite:
- raise PermissionError(f"{self.path} is read only! it can not be modified at runtime")
- super().merge(*args, **kwargs)
-
- def store(self, path=None):
- if not self.allow_overwrite:
- raise PermissionError(f"{self.path} is read only! it can not be modified at runtime")
- super().store(path)
-
-
- class MycroftDefaultConfig(ReadOnlyConfig):
- def __init__(self):
- super().__init__(join(get_xdg_config_save_path(), get_config_filename()))
-
- def set_root_config_path(self, root_config):
- # in case we got it wrong / non standard
- self.path = root_config
- self.reload()
-
-
- class MycroftSystemConfig(ReadOnlyConfig):
- def __init__(self, allow_overwrite=False):
- super().__init__("/etc/mycroft/mycroft.conf", allow_overwrite)
-
-
- class RemoteConf(LocalConf):
- def __init__(self, cache=get_webcache_location()):
- super(RemoteConf, self).__init__(cache)
-
-
- MycroftXDGConfig = MycroftUserConfig = MycroftDefaultConfig
diff --git a/ovos_utils/device_input.py b/ovos_utils/device_input.py
index 17e9b21b..6ab4bae5 100644
--- a/ovos_utils/device_input.py
+++ b/ovos_utils/device_input.py
@@ -1,5 +1,6 @@
import subprocess
from distutils.spawn import find_executable
+
from ovos_utils.gui import is_gui_installed
from ovos_utils.log import LOG
diff --git a/ovos_utils/enclosure/__init__.py b/ovos_utils/enclosure/__init__.py
deleted file mode 100644
index e9135711..00000000
--- a/ovos_utils/enclosure/__init__.py
+++ /dev/null
@@ -1,76 +0,0 @@
-from ovos_utils.system import MycroftRootLocations
-from ovos_utils.fingerprinting import detect_platform, MycroftPlatform
-from enum import Enum
-from os.path import exists
-from typing import Optional
-from ovos_utils.log import deprecated, log_deprecation
-
-log_deprecation("ovos_utils.enclosure has been deprecated!", "0.1.0")
-
-
-class MycroftEnclosures(str, Enum):
- # TODO: Deprecate in 0.1.0
- PICROFT = "picroft"
- BIGSCREEN = "kde"
- OVOS = "OpenVoiceOS"
- OLD_MARK1 = "mycroft_mark_1(old)"
- MARK1 = "mycroft_mark_1"
- MARK2 = "mycroft_mark_2"
- HOLMESV = "HolmesV"
- OLD_HOLMES = "mycroft-lib"
- GENERIC = "generic"
- OTHER = "unknown"
-
-
-@deprecated("This method is deprecated. Code should import from the current"
- "namespace; other system paths are irrelevant.", "0.1.0")
-def enclosure2rootdir(enclosure: MycroftEnclosures = None) -> Optional[str]:
- """
- Find the default installed core location for a specific platform.
- @param enclosure: MycroftEnclosures object to get root path for
- @return: string default root path
- """
- # TODO: Deprecate in 0.1.0
- enclosure = enclosure or detect_enclosure()
- if enclosure == MycroftEnclosures.OLD_MARK1:
- return MycroftRootLocations.OLD_MARK1
- elif enclosure == MycroftEnclosures.MARK1:
- return MycroftRootLocations.MARK1
- elif enclosure == MycroftEnclosures.MARK2:
- return MycroftRootLocations.MARK2
- elif enclosure == MycroftEnclosures.PICROFT:
- return MycroftPlatform.PICROFT
- elif enclosure == MycroftEnclosures.OVOS:
- return MycroftPlatform.OVOS
- elif enclosure == MycroftEnclosures.BIGSCREEN:
- return MycroftPlatform.BIGSCREEN
- return None
-
-
-@deprecated("This method is deprecated. Platform-specific code should"
- "use ovos_utils.fingerprinting.detect_platform directly", "0.1.0")
-def detect_enclosure() -> MycroftEnclosures:
- """
- Determine which enclosure is present on this file system.
- @return: MycroftEnclosures object detected
- """
- # TODO: Deprecate in 0.1.0
- platform = detect_platform()
- if platform == MycroftPlatform.MARK1:
- if exists(MycroftRootLocations.OLD_MARK1):
- return MycroftEnclosures.OLD_MARK1
- return MycroftEnclosures.MARK1
- elif platform == MycroftPlatform.MARK2:
- return MycroftEnclosures.MARK2
- elif platform == MycroftPlatform.PICROFT:
- return MycroftEnclosures.PICROFT
- elif platform == MycroftPlatform.OVOS:
- return MycroftEnclosures.OVOS
- elif platform == MycroftPlatform.BIGSCREEN:
- return MycroftEnclosures.BIGSCREEN
- elif platform == MycroftPlatform.HOLMESV:
- return MycroftEnclosures.HOLMESV
- elif platform == MycroftPlatform.OLD_HOLMES:
- return MycroftEnclosures.OLD_HOLMES
-
- return MycroftEnclosures.OTHER
diff --git a/ovos_utils/enclosure/api.py b/ovos_utils/enclosure/api.py
deleted file mode 100644
index 3427bdcb..00000000
--- a/ovos_utils/enclosure/api.py
+++ /dev/null
@@ -1,345 +0,0 @@
-from ovos_utils.log import log_deprecation
-
-log_deprecation("EnclosureApi has moved to ovos_bus_client.apis.enclosure", "0.1.0")
-
-
-try:
- from ovos_bus_client.apis.enclosure import EnclosureApi
-
-except ImportError:
- from ovos_utils.fakebus 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_utils/enclosure/mark1/__init__.py b/ovos_utils/enclosure/mark1/__init__.py
deleted file mode 100644
index 6f51bbdd..00000000
--- a/ovos_utils/enclosure/mark1/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from ovos_utils.enclosure.api import EnclosureAPI
-from ovos_utils.log import log_deprecation
-
-log_deprecation("ovos_utils.enclosure.mark1 moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0")
-
-
-class Mark1EnclosureAPI(EnclosureAPI):
- """ Mark1 enclosure, messagebus API"""
diff --git a/ovos_utils/enclosure/mark1/eyes/__init__.py b/ovos_utils/enclosure/mark1/eyes/__init__.py
deleted file mode 100644
index 334161eb..00000000
--- a/ovos_utils/enclosure/mark1/eyes/__init__.py
+++ /dev/null
@@ -1,513 +0,0 @@
-from ovos_utils.log import log_deprecation
-
-log_deprecation("ovos_utils.enclosure.mark1.eyes moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0")
-
-
-try:
- from ovos_mark1.eyes import *
-except:
- from ovos_utils.enclosure.mark1 import Mark1EnclosureAPI
- from ovos_utils.messagebus import get_mycroft_bus
- from ovos_utils.colors import Color
- from ovos_utils import rotate_list
- from time import sleep
-
-
- class EyePixel:
- def __init__(self, index, api, color=None):
- self.index = index
- self.api = api
- self.color = color or Color()
-
- @property
- def rgb(self):
- return self.color.rgb255
-
- def sync_color(self):
- r, g, b = self.api.get_eyes_pixel_color(self.index)
- color = Color.from_rgb(r, g, b)
- self.change_color(color)
-
- def update_color(self):
- self.change_color(self.color)
-
- def change_color(self, name):
- if isinstance(name, str):
- self.color = Color.from_name(name)
- elif isinstance(name, Color):
- self.color = name
- else:
- raise ValueError("not a Color object")
- r, g, b = self.rgb
- self.api.eyes_setpixel(self.index, r, g, b)
-
- def set_saturation(self, value):
- self.color.set_saturation(value)
- self.update_color()
-
- def set_luminance(self, value):
- self.color.set_luminance(value)
- self.update_color()
-
- def set_hue(self, value):
- self.color.set_hue(value)
- self.update_color()
-
- def __repr__(self):
- return "Pixel_" + str(self.index) + ":" + self.color.color_description
-
-
- class Eye(list):
- def __init__(self, pixel_range, bus=None, color=None):
- super().__init__()
- self.bus = bus or get_mycroft_bus()
- self.api = Mark1EnclosureAPI(self.bus)
- for idx in range(pixel_range[0], pixel_range[1]):
- pixel = EyePixel(idx, self.api)
- self.append(pixel)
- self.color = Color()
- if color:
- self.change_color(color)
- else:
- self.sync_color()
-
- def sync_color(self):
- for p in self:
- p.sync_color()
- sleep(0.05)
-
- def update_color(self):
- for p in self:
- p.update_color()
- sleep(0.05)
-
- def change_color(self, name):
- if isinstance(name, str):
- self.color = Color.from_name(name)
- elif isinstance(name, Color):
- self.color = name
- else:
- raise ValueError("not a Color object")
- for led in self:
- led.change_color(self.color)
- # writer bugs out if messages sent too fast
- sleep(0.05)
-
- def saturation_spin(self, speed=0.05):
- values = []
- for idx, pixel in enumerate(self):
- sat = 0.09 * idx
- pixel.set_saturation(sat)
- values.append(sat)
- sleep(speed)
-
- while True:
- values = rotate_list(values, -1)
- for idx, value in enumerate(values):
- self[idx].set_saturation(value)
- sleep(speed)
-
- def luminance_spin(self, speed=0.05):
- values = []
- for idx, pixel in enumerate(self):
- sat = 0.05 * idx
- pixel.set_luminance(sat)
- values.append(sat)
- sleep(speed)
-
- while True:
- values = rotate_list(values, -1)
- for idx, value in enumerate(values):
- self[idx].set_luminance(value)
- sleep(speed)
-
- def hue_spin(self, speed=0.05):
- values = []
- for idx, pixel in enumerate(self):
- sat = 0.083 * idx
- pixel.set_hue(sat)
- values.append(sat)
- sleep(speed)
-
- while True:
- values = rotate_list(values, -1)
- for idx, value in enumerate(values):
- self[idx].set_hue(value)
- sleep(speed)
-
- def set_hue(self, hue):
- for pixel in self:
- pixel.color.set_hue(hue)
- pixel.update_color()
-
- def set_luminance(self, value):
- for pixel in self:
- pixel.color.set_luminance(value)
- self.update_color()
-
- def set_saturation(self, value):
- for pixel in self:
- pixel.color.set_saturation(value)
- self.update_color()
-
- def on(self):
- self.set_luminance(1)
-
- def off(self):
- self.set_luminance(0)
-
- def blink_once(self):
- """
- Make the eye blink
- """
- raise NotImplementedError
-
- def blink(self, speed=0.5):
- """
- Make the right eye blink in a loop
- """
- while True:
- self.blink_once()
- sleep(speed)
-
-
- class RightEye(Eye):
- def __init__(self, bus, color=None):
- super().__init__(bus=bus, pixel_range=(12, 24), color=color)
-
- def sync_color(self):
- pixels = self.api.get_eyes_color()[12:]
- for idx, (r, g, b) in enumerate(pixels):
- self[idx].color = Color.from_rgb(r, g, b)
- self.update_color()
-
- def blink_once(self):
- """
- Make the right eye blink
- """
- self.api.eyes_blink("r")
-
-
- class LeftEye(Eye):
- def __init__(self, bus, color=None):
- super().__init__(bus=bus, pixel_range=(0, 12), color=color)
-
- def sync_color(self):
- pixels = self.api.get_eyes_color()[:12]
- for idx, (r, g, b) in enumerate(pixels):
- self[idx].color = Color.from_rgb(r, g, b)
- self.update_color()
-
- def blink_once(self):
- """
- Make the left eye blink
- """
- self.api.eyes_blink("l")
-
-
- class Eyes(list):
- def __init__(self, bus=None, color=None):
- super().__init__()
- self.bus = bus or get_mycroft_bus()
- self.api = Mark1EnclosureAPI(self.bus)
- self.right = RightEye(self.bus)
- self.left = LeftEye(self.bus)
- self.color = Color()
- if color:
- self.change_color(color)
- else:
- self.sync_color()
-
- def __getitem__(self, item):
- assert isinstance(item, int)
- assert 0 <= item <= 23
- if item < 12:
- return self.left[item]
- return self.right[item - 12]
-
- def __setitem__(self, key, value):
- assert isinstance(key, int)
- assert 0 <= key <= 23
- if key < 12:
- self.left[key] = value
- self.right[key] = value
-
- def __iter__(self):
- for i in range(len(self)):
- yield self[i]
-
- def __len__(self):
- return len(self.left) + len(self.right)
-
- def sync_color(self):
- """ updates internal color value to current color """
- pixels = self.api.get_eyes_color()
- for idx, (r, g, b) in enumerate(pixels):
- self[idx].color = Color.from_rgb(r, g, b)
-
- def update_color(self):
- """ updates arduino color to current pixels """
- for i in range(len(self) // 2):
- self.left[i].update_color()
- sleep(0.05)
- self.right[i].update_color()
- sleep(0.05)
-
- def change_color(self, name):
- """ changes color of both eyes """
- if isinstance(name, str):
- self.color = Color.from_name(name)
- elif isinstance(name, Color):
- self.color = name
- else:
- raise ValueError("not a Color object")
- r, g, b = self.color.rgb255
- self.api.eyes_color(r, g, b)
- for idx in range(len(self)):
- self[idx].color = self.color
-
- # animations
- def saturation_spin(self, speed=0.05):
- values = []
- for idx in range(len(self) // 2):
- sat = 0.09 * idx
- values.append(sat)
- self.left[idx].set_saturation(sat)
- sleep(0.03)
- self.right[idx].set_saturation(sat)
-
- while True:
- values = rotate_list(values, -1)
- for idx, value in enumerate(values):
- self.left[idx].set_saturation(value)
- sleep(0.03)
- self.right[idx].set_saturation(value)
- sleep(speed)
-
- def luminance_spin(self, speed=0.05):
- values = []
- for idx in range(len(self) // 2):
- sat = 0.05 * idx
- values.append(sat)
- self.left[idx].set_luminance(sat)
- sleep(0.03)
- self.right[idx].set_luminance(sat)
-
- while True:
- values = rotate_list(values, -1)
- for idx, value in enumerate(values):
- self.left[idx].set_luminance(value)
- sleep(0.03)
- self.right[idx].set_luminance(value)
- sleep(speed)
-
- def hue_spin(self, speed=0.05):
- values = []
- for idx in range(len(self) // 2):
- sat = 0.083 * idx
- values.append(sat)
- self.left[idx].set_hue(sat)
- sleep(0.03)
- self.right[idx].set_hue(sat)
-
- while True:
- values = rotate_list(values, -1)
- for idx, value in enumerate(values):
- for pixel in self:
- print(pixel)
- self.left[idx].set_hue(value)
- sleep(0.03)
- self.right[idx].set_hue(value)
- sleep(speed)
-
- def flash(self, speed=0.2):
- while True:
- sleep(speed)
- self.on()
- sleep(speed)
- self.off()
-
- def rainbow_flash(self, speed=0.2):
- colors = ["red", "orange", "yellow", "green", "cyan", "blue",
- "violet", "purple"]
- while True:
- for color in colors:
- sleep(speed)
- self.off()
- self.change_color(color)
- sleep(speed)
- self.on()
-
- def beacon(self, speed=0.1):
- values = [i + i for i in range(30)]
- while True:
- for value in values:
- self.set_brightness(value)
- sleep(speed)
- values.reverse()
-
- def rainbow_beacon(self, speed=0.1):
- values = [i + i for i in range(30)]
- values += reversed(values)
- colors = ["red", "orange", "yellow", "green", "cyan", "blue",
- "violet", "purple"]
- self.set_brightness(0)
- while True:
- for color in colors:
- for value in values:
- sleep(speed)
- self.set_brightness(value)
- self.change_color(color)
-
- # Arduino API
- def set_hue(self, hue):
- self.right.set_hue(hue)
- sleep(0.05)
- self.left.set_hue(hue)
-
- def set_brightness(self, level):
- """
- Set the brightness of the eyes in the display.
- Args:
- level (int): 1-30, bigger numbers being brighter
- """
- self.api.eyes_brightness(level)
-
- def spin(self):
- self.api.eyes_spin()
-
- def timed_spin(self, length):
- self.api.eyes_timed_spin(length)
-
- def reset(self):
- self.api.eyes_reset()
-
- def fill_once(self, percent):
- """
- Use the eyes as a type of progress meter
- Args:
- percent (int): 0-49 fills the right eye, 50-100 also covers left
- """
- self.api.eyes_fill(percent)
-
- def 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
- """
- self.api.eyes_look(side)
-
- def look_right(self):
- self.look("r")
-
- def look_left(self):
- self.look("l")
-
- def look_up(self):
- self.look("u")
-
- def look_down(self):
- self.look("d")
-
- def cross(self):
- self.look("c")
-
- def narrow(self):
- """Make the eyes look narrow, like a squint"""
- self.api.eyes_narrow()
-
- def on(self):
- """Illuminate or show the eyes."""
- self.api.eyes_on()
-
- def off(self):
- """Turn off or hide the eyes."""
- self.api.eyes_off()
-
- def blink_once(self, side="b"):
- """Make the eyes blink
- Args:
- side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both'
- """
- self.api.eyes_blink(side)
-
- def blink_right_once(self):
- self.right.blink_once()
-
- def blink_left_once(self):
- self.left.blink_once()
-
- def blink(self, speed=0.5):
- """
- Make the eyes blink in a loop
- """
- while True:
- self.blink_once()
- sleep(speed)
-
- def blink_right(self, speed=0.5):
- """
- Make the right eye blink in a loop
- """
- self.right.blink(speed)
-
- def blink_left(self, speed=0.5):
- """
- Make the left eyes blink in a loop
- """
- self.left.blink(speed)
-
- def blink_alternate(self, speed=0.5):
- """
- Make the eyes blink in a loop
- """
- while True:
- self.blink_right_once()
- sleep(speed)
- self.blink_left_once()
- sleep(speed)
-
- def up_down(self, speed=0.8):
- """
- Make the eyes blink in a loop
- """
- while True:
- self.look_up()
- sleep(speed)
- self.look_down()
- sleep(speed)
-
- def left_right(self, speed=0.8):
- """
- Make the eyes blink in a loop
- """
- while True:
- self.look_left()
- sleep(speed)
- self.look_right()
- sleep(speed)
-
- def fill(self, speed=0.1):
- values = [i for i in range(101)]
- values += reversed(values)
- while True:
- for percent in values:
- self.fill_once(percent)
- sleep(speed)
-
- def rainbow_fill(self, speed=0.1):
- values = [i for i in range(101)]
- values += reversed(values)
- colors = ["red", "orange", "yellow", "green", "cyan", "blue",
- "violet", "purple"]
- while True:
- for color in colors:
- for percent in values:
- self.fill_once(percent)
- sleep(speed)
- if percent == 100:
- self.change_color(color)
diff --git a/ovos_utils/enclosure/mark1/faceplate/__init__.py b/ovos_utils/enclosure/mark1/faceplate/__init__.py
deleted file mode 100644
index f6fedfbd..00000000
--- a/ovos_utils/enclosure/mark1/faceplate/__init__.py
+++ /dev/null
@@ -1,410 +0,0 @@
-from ovos_utils.enclosure.mark1 import Mark1EnclosureAPI
-from ovos_utils import create_loop
-from ovos_utils.log import LOG, log_deprecation
-from ovos_utils.messagebus import get_mycroft_bus
-import random
-from time import sleep
-from collections.abc import MutableSequence
-import copy
-
-log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0")
-
-
-try:
- from ovos_mark1.faceplate import *
-except ImportError:
-
- class FaceplateGrid(MutableSequence):
- encoded = None
- str_grid = None
- pad_char = "."
-
- def __init__(self, grid=None, bus=None):
- self.bus = bus or get_mycroft_bus()
- self._api = Mark1EnclosureAPI(self.bus)
- self.grid = []
- for x in range(8):
- self.grid.append([])
- for y in range(32):
- self.grid[x].append(0)
- if self.encoded:
- self.grid = self.decode(self.encoded).grid
- elif self.str_grid is not None:
- self.grid = FaceplateGrid(bus=self.bus)\
- .from_string(self.str_grid).grid
- elif grid is not None:
- self.grid = grid
-
- @property
- def height(self):
- return len(self.grid)
-
- @property
- def width(self):
- return max([len(r) for r in self.grid])
-
- def display(self, invert=True, clear=True, x_offset=0, y_offset=0):
- self._api.mouth_display(self.encode(invert),
- x_offset, y_offset, clear)
-
- def print(self, draw_padding=True, invert=False):
- print(self.to_string(draw_padding=draw_padding, invert=invert))
-
- def encode(self, invert=False):
- # to understand how this function works you need to understand how the
- # Mark I arduino proprietary encoding works to display to the faceplate
-
- # https://mycroft-ai.gitbook.io/docs/skill-development/displaying-information/mark-1-display
-
- # Each char value str_gridesents a width number starting with B=1
- # then increment 1 for the next. ie C=2
- width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
- 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
- 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a']
-
- height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
-
- encode = width_codes[self.width - 1]
- encode += height_codes[self.height - 1]
-
- # Turn the image pixels into binary values 1's and 0's
- # the Mark I face plate encoding uses binary values to
- # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...]
- binary_values = []
- for i in range(self.width): # pixels
- for j in range(self.height): # lines
- pixels = self.grid[j]
-
- if pixels[i] is None: # padding
- pixels[i] = 0
-
- if pixels[i] != 0:
- if invert is False:
- binary_values.append('1')
- else:
- binary_values.append('0')
- else:
- if invert is False:
- binary_values.append('0')
- else:
- binary_values.append('1')
- # these values are used to determine how binary values
- # needs to be grouped together
- number_of_bottom_pixel = 0
-
- if self.height > 4:
- number_of_top_pixel = 4
- number_of_bottom_pixel = self.height - 4
- else:
- number_of_top_pixel = self.height
-
- # this loop will group together the individual binary values
- # ie. binary_list = ['1111', '001', '0101', '100']
- binary_list = []
- binary_code = ''
- increment = 0
- alternate = False
- for val in binary_values:
- binary_code += val
- increment += 1
- if increment == number_of_top_pixel and alternate is False:
- # binary code is reversed for encoding
- binary_list.append(binary_code[::-1])
- increment = 0
- binary_code = ''
- alternate = True
- elif increment == number_of_bottom_pixel and alternate is True:
- binary_list.append(binary_code[::-1])
- increment = 0
- binary_code = ''
- alternate = False
- # Code to let the Mark I arduino know where to place the
- # pixels on the faceplate
- pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
- 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']
- for binary_values in binary_list:
- number = int(binary_values, 2)
- pixel_code = pixel_codes[number]
- encode += pixel_code
- return encode
-
- def decode(self, encoded, invert=False, pad=True):
- codes = list(encoded)
-
- # Each char value str_gridesents a width number starting with B=1
- # then increment 1 for the next. ie C=2
- width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
- 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
- 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a']
-
- height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
-
- height = height_codes.index(codes[1]) + 1
- width = width_codes.index(codes[0]) + 1
-
- # Code to let the Mark I arduino know where to place the
- # pixels on the faceplate
- pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
- 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']
- codes.reverse()
- binary_list = []
- for pixel_code in codes[:-2]:
- number = pixel_codes.index(pixel_code.upper())
- bin_str = str(bin(number))[2:]
- while not len(bin_str) == 4:
- bin_str = "0" + bin_str
- binary_list += [bin_str]
-
- binary_list.reverse()
-
- for idx, binary_code in enumerate(binary_list):
- # binary code is reversed for encoding
- binary_list[idx] = binary_code[::-1]
-
- binary_code = "".join(binary_list)
-
- # Turn the image pixels into binary values 1's and 0's
- # the Mark I face plate encoding uses binary values to
- # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...]
- grid = []
- # binary_code is a sequence of column by column
- cols = [list(binary_code)[x:x + height] for x in
- range(0, len(list(binary_code)), height)]
-
- for x in range(height):
- row = []
- for y in range(width):
- bit = int(cols[y][x])
- if invert:
- if bit:
- bit = 0
- else:
- bit = 1
- row.append(bit)
- grid.append(row)
-
- # handle padding
- if pad:
- if width < self.width:
- n = int((self.width - width) / 2)
- if invert:
- padding = [1] * n
- else:
- padding = [0] * n
- for idx, row in enumerate(grid):
- grid[idx] = padding + row + padding
- if height < self.height:
- pass # TODO vertical padding
- self.grid = grid
- return self
-
- def from_string(self, str_grid):
- rows = [r for r in str_grid.split("\n") if len(r)]
- grid = []
- for r in rows:
- row = []
- for char in list(r):
- if char == " ":
- row.append(1)
- elif char == FaceplateGrid.pad_char:
- row.append(None)
- else:
- row.append(0)
- while len(row) < self.width:
- row.append(None)
- grid.append(row)
- self.grid = grid
- return self
-
- def to_string(self, draw_padding=False, invert=False):
- str_grid = ""
- for row in self.grid:
- line = ""
- for col in row:
- if col is None and draw_padding:
- line += self.pad_char
- elif col == 1:
- if invert:
- line += "X"
- else:
- line += " "
- elif col == 0:
- if invert:
- line += " "
- else:
- line += "X"
- str_grid += line + "\n"
- return str_grid
-
- def invert(self):
- for x in range(self.height):
- for y in range(self.width):
- if self.grid[x][y] == 0:
- self.grid[x][y] = 1
- elif self.grid[x][y] == 1:
- self.grid[x][y] = 0
- return self
-
- def clear(self):
- for x in range(self.height):
- for y in range(self.width):
- self.grid[x][y] = 0
- return self
-
- @property
- def is_empty(self):
- for x in range(self.height):
- for y in range(self.width):
- if self.grid[x][y] == 1:
- return False
- return True
-
- def randomize(self, n=200):
- for i in range(n):
- x = random.randint(0, self.height-1)
- y = random.randint(0, self.width-1)
- self.grid[x][y] = int(random.randint(0, 1))
- return self
-
- def __len__(self):
- # number of pixels
- return self.width * self.height
-
- def __delitem__(self, index):
- self.grid.__delitem__(index)
-
- def insert(self, index, value):
- self.grid.insert(index - 1, value)
-
- def __setitem__(self, index, value):
- self.grid.__setitem__(index, value)
-
- def __getitem__(self, index):
- return self.grid.__getitem__(index)
-
-
- class FacePlateAnimation(FaceplateGrid):
-
- def __init__(self, grid=None, bus=None):
- super().__init__(grid, bus)
- self.finished = False
-
- def animate(self):
- pass
-
- def __iter__(self):
- while not self.finished:
- self.animate()
- yield self
-
- def start(self):
- self.finished = False
-
- def stop(self):
- self.finished = True
-
- def run(self, delay=0.5, callback=None, daemonic=False):
- self.start()
-
- if delay < 0.4:
- # writer bugs out if sending messages too rapidly
- delay = 0.4
-
- def step(callback=callback):
- try:
- if not self.finished:
- self.animate()
- if callback:
- callback(self)
- except Exception as e:
- LOG.error(e)
-
- if daemonic:
- create_loop(step, delay)
- else:
- while not self.finished:
- step()
- sleep(delay)
- self.stop()
-
- def scroll_down(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- self.grid[y][x] = old[y - 1][x]
-
- def scroll_up(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- if y == self.height - 1:
- self.grid[y][x] = old[0][x]
- else:
- self.grid[y][x] = old[y + 1][x]
-
- def scroll_right(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- self.grid[y][x] = old[y][x - 1]
-
- def scroll_left(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- if x == self.width -1:
- self.grid[y][x] = old[y][0]
- else:
- self.grid[y][x] = old[y][x + 1]
-
- def move_down(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- if y - 1 < 0:
- self.grid[y][x] = 0
- else:
- self.grid[y][x] = old[y - 1][x]
-
- def move_up(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- if y == self.height - 1:
- self.grid[y][x] = 0
- else:
- self.grid[y][x] = old[y + 1][x]
-
- def move_right(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- self.grid[y][x] = old[y][x - 1]
-
- def move_left(self):
- old = copy.deepcopy(self.grid)
- for x in range(self.width):
- for y in range(self.height):
- if x == self.width - 1:
- self.grid[y][x] = 0
- else:
- self.grid[y][x] = old[y][x + 1]
-
-
- class BlackScreen(FaceplateGrid):
- # Basically a util class to handle
- # inverting on __init__
- str_grid = """
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.invert()
diff --git a/ovos_utils/enclosure/mark1/faceplate/animations.py b/ovos_utils/enclosure/mark1/faceplate/animations.py
deleted file mode 100644
index cc50e40f..00000000
--- a/ovos_utils/enclosure/mark1/faceplate/animations.py
+++ /dev/null
@@ -1,570 +0,0 @@
-import copy
-import random
-
-from ovos_utils.log import log_deprecation
-
-log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0")
-
-
-try:
- from ovos_mark1.faceplate.animations import *
-except ImportError:
- from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation, BlackScreen
-
- # Base animations
- # These are mostly meant to be subclassed (empty animations)
- class HorizontalScroll(FacePlateAnimation):
- def __init__(self, direction="right", grid=None, bus=None):
- super().__init__(grid, bus)
- assert direction.startswith("r") or direction.startswith("l")
- self.direction = direction[0]
-
- def animate(self):
- if self.direction == "r":
- self.scroll_right()
- else:
- self.scroll_left()
- if self.is_empty:
- self.stop()
-
-
- class VerticalScroll(FacePlateAnimation):
- def __init__(self, direction="up",
- grid=None, bus=None):
- super().__init__(grid, bus)
- assert direction.startswith("u") or direction.startswith("d")
- self.direction = direction[0]
-
- def animate(self):
- if self.direction == "u":
- self.scroll_up()
- else:
- self.scroll_down()
- if self.is_empty:
- self.stop()
-
-
- class LeftRight(FacePlateAnimation):
- def __init__(self, direction="right", start="left", grid=None, bus=None):
- super().__init__(grid, bus)
- assert direction.startswith("r") or direction.startswith("l")
- self.direction = direction[0]
-
- # start at right/left side/center
- inverted = not isinstance(self, BlackScreen)
- if start[0] == "l":
- # left side
- for y in range(self.height):
- for x in range(self.width):
- if not inverted and self.grid[y][x] == 1:
- pass
- elif inverted and self.grid[y][x] == 0:
- pass
- elif start[0] == "r":
- pass # right side
- else:
- pass # center
- print(self.grid[1])
-
- def animate(self):
- left_collision = False
- right_collision = False
- inverted = not isinstance(self, BlackScreen)
- for y in range(self.height):
- if inverted:
- if self.grid[y][self.width - 1] == 0:
- right_collision = True
- if self.grid[y][0] == 0:
- left_collision = True
- else:
- if self.grid[y][self.width - 1] == 1:
- right_collision = True
- if self.grid[y][0] == 1:
- left_collision = True
- if left_collision and right_collision:
- return # No space left to animate
- elif right_collision:
- self.direction = "l"
- elif left_collision:
- self.direction = "r"
- if self.direction == "r":
- self.scroll_right()
- else:
- self.scroll_left()
- if self.is_empty:
- self.stop()
-
-
- class UpDown(FacePlateAnimation):
- def __init__(self, direction="up", grid=None, bus=None):
- super().__init__(grid, bus)
- assert direction.startswith("u") or direction.startswith("d")
- self.direction = direction[0]
-
- def animate(self):
- top_collision = False
- bottom_collision = False
- for x in range(self.width):
- if self.grid[0][x] == 1:
- top_collision = True
- if self.grid[self.height - 1][x] == 1:
- bottom_collision = True
-
- if top_collision and bottom_collision:
- return # No space left to animate
- elif top_collision:
- self.direction = "d"
- elif bottom_collision:
- self.direction = "u"
- if self.direction == "u":
- self.scroll_up()
- else:
- self.scroll_down()
- if self.is_empty:
- self.stop()
-
-
- class CollisionBox(FacePlateAnimation):
- def __init__(self,
- horizontal_direction=None,
- vertical_direction=None,
- grid=None, bus=None):
- super().__init__(grid, bus)
- assert horizontal_direction is None or \
- horizontal_direction.startswith("r") or \
- horizontal_direction.startswith("l")
- assert vertical_direction is None or \
- vertical_direction.startswith("u") or \
- vertical_direction.startswith("d")
- self.vertical_direction = vertical_direction[0] if \
- vertical_direction else None
- self.horizontal_direction = horizontal_direction[0] if \
- horizontal_direction else None
-
- def animate(self):
- left_collision = False
- right_collision = False
- top_collision = False
- bottom_collision = False
- for y in range(self.height):
- if self.grid[y][self.width - 1] == 1:
- right_collision = True
- if self.grid[y][0] == 1:
- left_collision = True
- for x in range(self.width):
- if self.grid[0][x] == 1:
- top_collision = True
- if self.grid[self.height - 1][x] == 1:
- bottom_collision = True
-
- if top_collision and bottom_collision:
- self.vertical_direction = None
- elif top_collision:
- self.vertical_direction = "d"
- elif bottom_collision:
- self.vertical_direction = "u"
-
- if left_collision and right_collision:
- self.horizontal_direction = None
- elif right_collision:
- self.horizontal_direction = "l"
- elif left_collision:
- self.horizontal_direction = "r"
-
- if self.vertical_direction is None:
- pass
- elif self.vertical_direction == "u":
- self.scroll_up()
- elif self.vertical_direction == "d":
- self.scroll_down()
-
- if self.horizontal_direction is None:
- pass
- elif self.horizontal_direction == "r":
- self.scroll_right()
- elif self.horizontal_direction == "l":
- self.scroll_left()
-
- if self.is_empty:
- self.stop()
-
-
- # Ready to use animations
- class SquareWave(HorizontalScroll):
- def __init__(self, direction="r", frequency=3,
- amplitude=4, grid=None, bus=None):
- super().__init__(direction, grid, bus)
- # frequency must be > 1
- # frequency is in number of pixels
- assert 0 < frequency
- # amplitude must be 2, 4 or 6. else it renders badly
- # amplitude is in number of pixels
- assert 0 < amplitude < self.height
- assert divmod(amplitude, 2)[1] == 0
-
- self.freq = frequency
- self.amplitude = self.height - amplitude
-
- self._initial_grid()
- self.invert()
-
- def _initial_grid(self):
- # draws the initial state
- count = 0
- top = True
- a = self.amplitude // 2 - 1
- for x in range(self.width):
- if top:
- self.grid[a + 1][x] = 1
- else:
- self.grid[-a - 1][x] = 1
-
- for y in range(self.height):
- if count == 0:
- self.grid[y][x] = 1
- if y <= a:
- self.grid[y][x] = 0
- elif y >= self.height - a:
- self.grid[y][x] = 0
- count += 1
- if count == self.freq + 1:
- count = 0
- top = not top
-
-
- class StrayDot(CollisionBox):
- def __init__(self,
- start_x=None,
- start_y=None,
- horizontal_direction=None,
- vertical_direction=None,
- grid=None, bus=None):
- horizontal_direction = horizontal_direction or \
- random.choice(["l", "r"])
- vertical_direction = vertical_direction or \
- random.choice(["u", "d"])
- super().__init__(horizontal_direction, vertical_direction,
- grid, bus)
- start_x = start_x or random.randint(0, self.width - 1)
- start_y = start_y or random.randint(0, self.height - 1)
- self.grid[start_y][start_x] = 1
-
-
- class ParticleBox(FacePlateAnimation):
- def __init__(self, n_particles=5, bus=None):
- super().__init__(bus=bus)
- assert 0 < n_particles < 11
- self.n_particles = n_particles
- self.particles = []
-
- class Dot:
- def __init__(self, idx, x, y, vx, vy):
- self.x = x
- self.y = y
- self.vx = vx
- self.vy = vy
- self.idx = idx
-
- for i in range(n_particles):
- vx = random.choice(["l", "r"])
- vy = random.choice(["u", "d"])
- x = random.randint(0, self.width - 1)
- y = random.randint(0, self.height - 1)
- while self.grid[y][x] == 1:
- # 2 particles can't occupy same space
- x = random.randint(0, self.width - 1)
- y = random.randint(0, self.height - 1)
- self.grid[y][x] = 1
- self.particles.append(Dot(i, x, y, vx, vy))
-
- def render_particles(self):
- self.clear()
- for p in self.particles:
- self.grid[p.y][p.x] = 1
-
- def get_particle(self, x, y):
- for p in self.particles:
- if p.x == x and p.y == y:
- return p
-
- def process_collisions(self):
- # new particles after this turn
- new_particles = copy.deepcopy(self.particles)
-
- # NOTE this is not a physics simulation!
- # while it is behaving like an elastic collision
- # if there is a 3+ particle collision results will be incorrect
- # as long as only 2 particles collide it looks accurate
- # max number of particles limited to 10 to minimize chance of this
- # happening
- for p in self.particles:
- idx = p.idx
-
- # horizontal movement
- if p.vx is None:
- # not moving horizontally
- if p.x != 0:
- # check for collisions from left p2 -> p1
- p2 = self.get_particle(p.x - 1, p.y)
- if p2 and p2.vx == "r":
- # collision p2 -> p1
- if p.x == self.width - 1:
- new_particles[idx].vx = None
- else:
- new_particles[idx].vx = "r"
- new_particles[idx].x += 1
- if p.x != self.width - 1:
- # check for collisions from right p1 <- p2
- p2 = self.get_particle(p.x + 1, p.y)
- if p2 and p2.vx == "l":
- # collision p1 <- p2
- if p.x == 0:
- new_particles[idx].vx = None
- else:
- new_particles[idx].vx = "l"
- new_particles[idx].x -= 1
- # moving right
- elif p.vx == "r":
- p2 = self.get_particle(p.x + 1, p.y)
- if p.x == self.width - 1:
- # border collision p1 -> |
- new_particles[idx].vx = "l"
- new_particles[idx].x -= 1
- elif p2:
- # particle collision p1 -> p2
- if p2.vx is None:
- # p2 moves, p1 stops
- new_particles[idx].vx = None
- elif p2.vx == "r":
- # moving together
- new_particles[idx].x += 1
- elif p2 and p2.vx == "l":
- if p.x == 0:
- new_particles[idx].vx = None
- else:
- # both change direction
- new_particles[idx].vx = "l"
- new_particles[idx].x -= 1
- else:
- # move right
- new_particles[idx].x += 1
- # moving left
- elif p.vx == "l":
- p2 = self.get_particle(p.x - 1, p.y)
- if p.x == 0:
- # border collision | <- p1
- new_particles[idx].vx = "r"
- new_particles[idx].x += 1
- elif p2:
- # particle collision p2 <- p1
-
- if p2.vx is None:
- # p2 moves, p1 stops
- new_particles[idx].vx = None
- elif p2.vx == "l":
- # moving together, no collision
- new_particles[idx].x -= 1
- elif p2.vx == "r":
- if p.x == self.width - 1:
- new_particles[idx].vx = None
- else:
- # both change direction
- new_particles[idx].vx = "r"
- new_particles[idx].x += 1
- else:
- # move left
- new_particles[idx].x -= 1
-
- # vertical movement
- if p.vy is None:
- # not moving vertically
- if p.y != 0:
- # check for collisions from top p2 -> p1
- p2 = self.get_particle(p.x, p.y - 1)
- if p2 and p2.vy == "d":
- if p.y == self.height - 1:
- new_particles[idx].vy = None
- else:
- # collision p2 -> p1
- new_particles[idx].vy = "d"
- new_particles[idx].y += 1
- if p.y != self.height - 1:
- # check for collisions from bottom p1 <- p2
- p2 = self.get_particle(p.x, p.y + 1)
- if p2 and p2.vy == "u":
- # collision p1 <- p2
- if p.y == 0: # on top
- new_particles[idx].vy = None
- else:
- new_particles[idx].vy = "u"
- new_particles[idx].y -= 1
- # moving down
- elif p.vy == "d":
- p2 = self.get_particle(p.x, p.y + 1)
- if p.y == self.height - 1:
- # border collision p1 -> |
- new_particles[idx].vy = "u"
- new_particles[idx].y -= 1
- elif p2:
- # particle collision p1 -> p2
-
- if p2.vy is None:
- # p2 moves, p1 stops
- new_particles[idx].vy = None
- elif p2.vy == "d":
- # moving together
- new_particles[idx].y += 1
- elif p2.vy == "u":
- if p.y == 0:
- new_particles[
- idx].vy = None # wall absorbed momentum
- else:
- # both change direction
- new_particles[idx].vy = "u"
- new_particles[idx].y -= 1
- else:
- # move down
- new_particles[idx].y += 1
- # moving up
- elif p.vy == "u":
-
- p2 = self.get_particle(p.x, p.y - 1)
- if p.y == 0:
- # border collision | <- p1
- new_particles[idx].vy = "d"
- new_particles[idx].y += 1
- elif p2:
- # particle collision p2 <- p1
- if p2.vy is None:
- # p2 moves, p1 stops
- new_particles[idx].vy = None
- elif p2.vy == "u":
- # moving together, no collision
- new_particles[idx].y -= 1
- elif p2.vy == "d":
- # both change direction
- if p.y == self.height - 1:
- new_particles[idx].vy = None
- else:
- new_particles[idx].vy = "d"
- new_particles[idx].y += 1
- else:
- # move left
- new_particles[idx].y -= 1
-
- # update processed particles
- self.particles = new_particles
-
- def animate(self):
- self.process_collisions()
- self.render_particles()
-
-
- class FallingDots(FacePlateAnimation):
- def __init__(self, n=10, bus=None):
- super().__init__(bus=bus)
- self._create = True
- assert 0 < n < 32
- self.n = n
-
- @property
- def n_dots(self):
- n = 0
- for y in range(self.height):
- for x in range(self.width):
- if self.grid[y][x]:
- n += 1
- return n
-
- def animate(self):
- self.move_down()
- if self._create:
- if random.choice([True, False]):
- self._create = False
- x = random.randint(0, self.width - 1)
- self.grid[0][x] = 1
- if self.n_dots < self.n:
- self._create = True
-
-
- class StraightParticleShooter(FacePlateAnimation):
- def __init__(self, period=None, bus=None):
- super().__init__(bus=bus)
- self.direction = "d"
- self.period = period
- self.counter = 0
- # draw shooter
- self.grid[0][0] = 1
- self.grid[1][0] = 1
- self.grid[1][1] = 1
- self.grid[2][0] = 1
-
- def line_down(self):
- old = copy.deepcopy(self.grid)
- for y in range(self.height):
- self.grid[y][0] = old[y - 1][0]
- self.grid[y][1] = old[y - 1][1]
-
- def line_up(self):
- old = copy.deepcopy(self.grid)
- for y in range(self.height):
- if y == self.height - 1:
- self.grid[y][0] = old[0][0]
- self.grid[y][1] = old[0][1]
- else:
- self.grid[y][0] = old[y + 1][0]
- self.grid[y][1] = old[y + 1][1]
-
- def scroll_particles(self):
- old = copy.deepcopy(self.grid)
- for x in range(2, self.width):
- for y in range(self.height):
- if old[y][x] == 1:
- self.grid[y][x] = 0
- if x < self.width - 1:
- self.grid[y][x + 1] = 1
-
- @property
- def num_particles(self):
- n = 0
- for x in range(2, self.width):
- for y in range(self.height):
- if self.grid[y][x] == 1:
- n += 1
- return n
-
- @property
- def line(self):
- for y in range(self.height):
- if self.grid[y][0] == 1:
- return y
- return 0
-
- def animate(self):
- # collision detection
- top_collision = False
- bottom_collision = False
- if self.grid[0][0] == 1:
- top_collision = True
- elif self.grid[self.height - 1][0] == 1:
- bottom_collision = True
- if top_collision:
- self.direction = "d"
- elif bottom_collision:
- self.direction = "u"
-
- # bounce the "emitter" up and down
- if self.direction == "u":
- self.line_up()
- else:
- self.line_down()
-
- # create particles
- period = self.period or random.randint(0, 20)
- if self.num_particles < 1 or self.counter >= period:
- self.grid[self.line + 1][2] = 1
- self.counter = 0
-
- # animate particles
- self.scroll_particles()
- self.counter += 1
diff --git a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py b/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py
deleted file mode 100644
index 23198546..00000000
--- a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py
+++ /dev/null
@@ -1,485 +0,0 @@
-import copy
-import random
-
-from ovos_utils.log import log_deprecation
-
-log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0")
-
-
-try:
- from ovos_mark1.faceplate.cellular_automaton import *
-except:
- from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation
-
-
- # Game of Life Base
- class GoL(FacePlateAnimation):
-
- def __init__(self, entropy=0, grid=None, bus=None):
- super().__init__(grid, bus)
- self.entropy = entropy
- if self.is_empty:
- self.randomize()
-
- def _live_neighbours(self, y, x):
- """Returns the number of live neighbours."""
- count = 0
- if y > 0:
- if self.grid[y - 1][x]:
- count = count + 1
- if x > 0:
- if self.grid[y - 1][x - 1]:
- count = count + 1
- if self.width > (x + 1):
- if self.grid[y - 1][x + 1]:
- count = count + 1
-
- if x > 0:
- if self.grid[y][x - 1]:
- count = count + 1
- if self.width > (x + 1):
- if self.grid[y][x + 1]:
- count = count + 1
-
- if self.height > (y + 1):
- if self.grid[y + 1][x]:
- count = count + 1
- if x > 0:
- if self.grid[y + 1][x - 1]:
- count = count + 1
- if self.width > (x + 1):
- if self.grid[y + 1][x + 1]:
- count = count + 1
-
- return count
-
- def animate(self):
- """Game of Life turn"""
- nt = copy.deepcopy(self.grid)
- for y in range(0, self.height):
- for x in range(0, self.width):
- neighbours = self._live_neighbours(y, x)
- if self.grid[y][x] == 0:
- if neighbours == 3:
- nt[y][x] = 1
- else:
- if (neighbours < 2) or (neighbours > 3):
- nt[y][x] = 0
- if nt == self.grid and self.entropy <= 0:
- self.stop()
- self.grid = nt
- self.randomize(self.entropy)
- if self.is_empty:
- self.stop()
-
-
- # Langtons Ant base
- class _Ant:
- def __init__(self, x, y, direction, height=8, width=32):
- self.x = x
- self.y = y
- assert direction[0] in ["r", "l", "u", "d"]
- self.direction = direction[0]
- self.grid_height = height
- self.grid_width = width
- self.dead = False
-
- def move_forward(self):
- if self.direction == "r":
- self.x += 1
- if self.x == self.grid_width:
- self.dead = True
- elif self.direction == "d":
- self.y += 1
- if self.y == self.grid_height:
- self.dead = True
- elif self.direction == "l":
- self.x -= 1
- if self.x == -1:
- self.dead = True
- elif self.direction == "u":
- self.y -= 1
- if self.y == -1:
- self.dead = True
-
- def turn_right(self):
- if self.direction == "r":
- self.direction = "d"
- elif self.direction == "d":
- self.direction = "l"
- elif self.direction == "l":
- self.direction = "u"
- elif self.direction == "u":
- self.direction = "r"
-
- def turn_left(self):
- if self.direction == "r":
- self.direction = "u"
- elif self.direction == "d":
- self.direction = "r"
- elif self.direction == "l":
- self.direction = "d"
- elif self.direction == "u":
- self.direction = "l"
-
-
- class _InfiniteAnt(_Ant):
- def move_forward(self):
- if self.direction == "r":
- self.x += 1
- if self.x == self.grid_width:
- self.x = 0
- elif self.direction == "d":
- self.y += 1
- if self.y == self.grid_height:
- self.y = 0
- elif self.direction == "l":
- self.x -= 1
- if self.x == -1:
- self.x = self.grid_width - 1
- elif self.direction == "u":
- self.y -= 1
- if self.y == -1:
- self.y = self.grid_height - 1
-
-
- class _ReverseAnt(_Ant):
- def turn_left(self):
- super().turn_right()
-
- def turn_right(self):
- super().turn_left()
-
-
- class _ReverseInfiniteAnt(_InfiniteAnt):
- def turn_left(self):
- super().turn_right()
-
- def turn_right(self):
- super().turn_left()
-
-
- class LangtonsAnt(FacePlateAnimation):
- def __init__(self, ants=1, continuous=True, gen_reverse=False,
- grid=None, bus=None):
- super().__init__(grid=grid, bus=bus)
- self.ants = []
- # if continuous loops around the board
- # height + 1 -> 0
- # width + 1 -> 0
- # else ant is removed
- self.continuous = continuous
- if isinstance(ants, int):
- # spawn N ants
- assert 0 <= ants < 256
- for i in range(ants):
- x = random.randint(0, self.width - 1)
- y = random.randint(0, self.height - 1)
- direction = random.choice(["u", "d", "l", "r"])
- reverse = False
- if gen_reverse:
- reverse = random.choice([True, False])
- ant = self.ant_factory(x, y, direction, reverse)
- self.ants.append(ant)
- elif isinstance(ants, list):
- # Ant objects
- self.ants = ants
- for ant in self.ants:
- assert isinstance(ant, _Ant)
- else:
- raise ValueError
-
- def ant_factory(self, x, y, direction, reverse=False):
- # reverse ants exit black squares to the opposite direction
- if self.continuous:
- # loops around the board instead of dying
- if reverse:
- return _ReverseInfiniteAnt(x, y, direction)
- return _InfiniteAnt(x, y, direction)
- if reverse:
- return _ReverseAnt(x, y, direction)
- return _Ant(x, y, direction)
-
- def move_ants(self):
- # copy grid, multiple ants might want to flip same square
- # end result is the same so this is not a problem as long as it does
- # not change during iteration
- old_grid = copy.deepcopy(self.grid)
-
- for idx, ant in enumerate(self.ants):
- if self.ants[idx].dead:
- continue
- if old_grid[ant.y][ant.x] == 1:
- # black
- self.ants[idx].turn_left()
- else:
- # white
- self.ants[idx].turn_right()
- # flip color
- self.grid[ant.y][ant.x] = not old_grid[ant.y][ant.x]
- # update ant position
- self.ants[idx].move_forward()
-
- def animate(self):
- self.move_ants()
- # Stop condition -> all ants moved out of the board
- dead_ants = [ant for ant in self.ants if ant.dead]
- if len(dead_ants) == len(self.ants):
- self.stop()
-
-
- # Game of Life Animations
- class SpaceInvader(GoL):
- # This basically half "pulsar"
- str_grid = """
- XXXXXXXXXXXX XXX XXXXXXXXXXXXX
- XXXXXXXXXX X X X X X XXXXXXXXXXX
- XXXXXXXX XX X XX XXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXX XXX XXXXXXXXXXXXX
- XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX
- XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- """
-
-
- # Langton's Ant animations
-
- # Single Ant
- class LangtonsLineDisplacer(LangtonsAnt):
- # see pattern here
- # https://youtu.be/w6XQQhCgq5c?t=84
-
- def __init__(self, x=None, y=None, continuous=True, bus=None):
- super().__init__(0, continuous, bus=bus)
- x = x if x is not None else random.randint(0, self.width - 1)
- y = y if y is not None else random.randint(0, self.height - 1)
- ant = self.ant_factory(x, y - 1, "u")
- self.ants.append(ant)
- # create initial line
- for i in range(0, self.width):
- self.grid[y][i] = 1
-
-
- # 2 Ants
- class LangtonsAntsOscillator(LangtonsAnt):
- # see pattern here
- # https://youtu.be/w6XQQhCgq5c?t=103
-
- def __init__(self, x=None, y=None, bus=None):
- super().__init__(0, bus=bus)
- x1 = x2 = x if x is not None else self.width // 2
- y1 = y if y is not None else self.height // 2
- y2 = y1 - 1
- dir1 = "d"
- dir2 = "u"
- ant1 = self.ant_factory(x1, y1, dir1)
- ant2 = self.ant_factory(x2, y2, dir2)
- self.ants += [ant1, ant2]
-
-
- class LangtonsAntsOscillator2(LangtonsAnt):
- def __init__(self, x=None, y=None, bus=None):
- super().__init__(0, bus=bus)
- x1 = x2 = x if x is not None else self.width // 2
- y1 = y if y is not None else self.height // 2
- y2 = y1 - 1
- dir1 = dir2 = "u"
- ant1 = self.ant_factory(x1, y1, dir1)
- ant2 = self.ant_factory(x2, y2, dir2)
- self.ants += [ant1, ant2]
-
-
- class LangtonsAntsOscillator3(LangtonsAnt):
- def __init__(self, x=None, y=None, bus=None):
- super().__init__(0, bus=bus)
- x1 = x2 = x if x is not None else self.width // 2
- y1 = y if y is not None else self.height // 2
- y2 = y1 - 1
- dir1 = "l"
- dir2 = "r"
- ant1 = self.ant_factory(x1, y1, dir1)
- ant2 = self.ant_factory(x2, y2, dir2)
- self.ants += [ant1, ant2]
-
-
- class LangtonsAntsOscillator4(LangtonsAnt):
- def __init__(self, x=None, y=None, bus=None):
- super().__init__(0, bus=bus)
- x1 = x2 = x if x is not None else self.width // 2
- y1 = y if y is not None else self.height // 2
- y2 = y1 - 1
- dir1 = dir2 = "l"
- ant1 = self.ant_factory(x1, y1, dir1)
- ant2 = self.ant_factory(x2, y2, dir2)
- self.ants += [ant1, ant2]
-
-
- class LangtonsAntsOscillator5(LangtonsAnt):
- def __init__(self, x=None, y=None, bus=None):
- super().__init__(0, bus=bus)
- x1 = x2 = x if x is not None else self.width // 2
- y1 = y if y is not None else self.height // 2
- y2 = y1 - 1
- dir1 = "u"
- dir2 = "d"
- ant1 = self.ant_factory(x1, y1, dir1)
- ant2 = self.ant_factory(x2, y2, dir2)
- self.ants += [ant1, ant2]
-
-
- class LangtonsAntTrail(LangtonsAnt):
- # see pattern here
- # https://youtu.be/w6XQQhCgq5c?t=159
-
- def __init__(self, x=None, y=None, bus=None):
- super().__init__(0, bus=bus)
- x = x if x is not None else random.randint(0, self.width - 1)
- y = y if y is not None else random.randint(0, self.height - 1)
- dir1 = "u"
- dir2 = "d"
- ant1 = self.ant_factory(x, y - 1, dir1)
- ant2 = self.ant_factory(x, y, dir2, reverse=True)
- self.ants += [ant1, ant2]
-
-
- class LangtonsAntDotTransporter(LangtonsAnt):
- # https://youtu.be/w6XQQhCgq5c?t=171
-
- def __init__(self, x=None, y=None, bus=None):
- super().__init__(0, bus=bus)
- x = x if x is not None else random.randint(0, self.width - 1)
- y = y if y is not None else random.randint(0, self.height - 1)
- dir1 = dir2 = "u"
- ant1 = self.ant_factory(x, y, dir1)
- ant2 = self.ant_factory(x + 1, y, dir2, reverse=True)
- self.ants += [ant1, ant2]
- # initial grid
- if y + 1 == self.height:
- self.grid[0][x + 1] = 1 # bellow anti-ant
- else:
- self.grid[y + 1][x + 1] = 1 # bellow anti-ant
- self.grid[y - 1][x] = 1 # above ant
-
-
- # 1D / elementar automata
-
- class ElementarAutomata(FacePlateAnimation):
- def __init__(self, direction="u", idx=0, seed=None, grid=None, bus=None):
- super().__init__(grid, bus)
- assert direction[0] in ["u", "d", "l", "r"]
- self.direction = direction[0]
- self.row = idx
- self.initial_state(seed)
-
- def initial_state(self, seed=None):
- if seed is not None:
- line = seed
- else:
- line = [0 for i in range(self.width)]
- self.grid[self.row] = line
-
- def rule(self):
- # process the line
- raise NotImplementedError
-
- def animate(self):
- old = copy.deepcopy(self.grid)
- new_line = self.rule()
- if self.direction == "u":
- self.move_up()
- elif self.direction == "d":
- self.move_down()
- elif self.direction == "l":
- self.move_left()
- elif self.direction == "r":
- self.move_right()
- self.grid[self.row] = new_line
- if old == self.grid:
- self.stop()
-
-
- class SierpinskiTriangle(ElementarAutomata):
- def __init__(self, direction="u", seed=None, bus=None):
- assert direction[0] in ["u", "d"]
- if direction[0] == "u":
- idx = -1
- else:
- idx = 0
- super().__init__(direction, idx, seed=seed, bus=bus)
-
- def initial_state(self, seed=None):
- if seed is not None:
- line = seed
- else:
- line = [0 for i in range(self.width)]
- idx = self.width // 2
- line[idx] = 1
- self.grid[self.row] = line
-
- def rule(self):
- new_line = copy.deepcopy(self.grid[self.row])
- # handle middle
- for i in range(1, self.width - 1):
- left = self.grid[self.row][i - 1]
- right = self.grid[self.row][i + 1]
- if left and right:
- new_line[i] = 0
- elif left or right:
- new_line[i] = 1
- else:
- new_line[i] = 0
-
- # handle edges
- # if self.grid[0][1] == 1:
- # new_line[0] = 1
- # else:
- # new_line[0] = 0
- # if self.grid[0][-2] == 1:
- # new_line[-1] = 1
- # else:
- # new_line[-1] = 0
- return new_line
-
-
- class Rule110(ElementarAutomata):
- def __init__(self, direction="u", seed=None, bus=None):
- assert direction[0] in ["u", "d"]
- if direction[0] == "u":
- idx = -1
- else:
- idx = 0
- super().__init__(direction, idx, seed=seed, bus=bus)
-
- def initial_state(self, seed=None):
- if seed is not None:
- line = seed
- else:
- line = [0 for i in range(self.width)]
- idx = self.width - 1
- line[idx] = 1
- self.grid[self.row] = line
-
- def rule(self):
- new_line = copy.deepcopy(self.grid[self.row])
- # handle middle
- for i in range(1, self.width - 1):
- left = self.grid[self.row][i - 1]
- mid = self.grid[self.row][i]
- right = self.grid[self.row][i + 1]
- if str(left) + str(mid) + str(right) in \
- ["110", "101", "011", "010", "001"]:
- new_line[i] = 1
- else:
- new_line[i] = 0
-
- # handle edges
- if self.grid[0][1] == 1:
- new_line[0] = 1
- else:
- new_line[0] = 0
- if self.grid[0][-2] == 1:
- new_line[-1] = 1
- else:
- new_line[-1] = 0
- return new_line
diff --git a/ovos_utils/enclosure/mark1/faceplate/icons.py b/ovos_utils/enclosure/mark1/faceplate/icons.py
deleted file mode 100644
index f49c1e02..00000000
--- a/ovos_utils/enclosure/mark1/faceplate/icons.py
+++ /dev/null
@@ -1,239 +0,0 @@
-from ovos_utils.log import log_deprecation
-
-log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0")
-
-
-try:
- from ovos_mark1.faceplate.icons import *
-except:
- from ovos_utils.enclosure.mark1.faceplate import FaceplateGrid, BlackScreen
-
- class MusicIcon(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXX XXXXXXXXXXXXX
- XXXXXXXXXXXXXX XXXXXXXXXXXXX
- XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX
- XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX
- XXXXXXXXXXXXX XX XXXXXXXXXXXXX
- XXXXXXXXXXXX X XXXXXXXXXXXXX
- XXXXXXXXXXXXX XXX XXXXXXXXXXXXXX
- """
-
-
- class PlusIcon(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- """
-
-
- class HeartIcon(FaceplateGrid):
- str_grid = """
-
- xx xx
- xxxx xxxx
- xxxxxxxxx
- xxxxxxx
- xxxxx
- xxx
- x
- """
-
-
- class HollowHeartIcon(FaceplateGrid):
- str_grid = """
-
- xx xx
- x x x x
- x x x
- x x
- x x
- x x
- x
- """
-
-
- class SkullIcon(FaceplateGrid):
- str_grid = """
-
- xxxxxxx
- x xxx x
- xxxxxxxxx
- xxx xxx
- xxxxx
- x x x
- x x x
- """
-
-
- class DeadFishIcon(FaceplateGrid):
- str_grid = """
-
- x xxxx
- x x x x xx xxx
- xxxxxxxxxxxxxxx
- x x x x xxxxxx
- x xxxx
- """
-
-
- class InfoIcon(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXX
- """
-
-
- class ArrowLeftIcon(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXXXXXXX
- XXXXXXXXXXXX XXXXXXXXXXXXXXXXX
- XXXXXXXXXXX X XXXXXXXXXX
- XXXXXXXXXX X XXXXXXXXXX
- XXXXXXXXXXX X XXXXXXXXXX
- XXXXXXXXXXXX XXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXXXXXXX
- """
-
-
- class WarningIcon(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXX XXXXXXXXXXXXXXX
- XXXXXXXXXXXXX X XXXXXXXXXXXXXX
- XXXXXXXXXXXX XXX XXXXXXXXXXXXX
- XXXXXXXXXXX XXX XXXXXXXXXXXX
- XXXXXXXXXX XXXXXXXXXXX
- XXXXXXXXX XXX XXXXXXXXXX
- XXXXXXXX X XXXXXXXXX
- """
-
-
- class CrossIcon(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX
- XXXXXXXXXXXX XXX XXXXXXXXXXX
- XXXXXXXXXXXXX X XXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXX X XXXXXXXXXXXX
- XXXXXXXXXXXX XXX XXXXXXXXXXX
- XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX
- """
-
-
- class JarbasAI(BlackScreen):
- str_grid = """
- X XXXXXXXXXXXXXXXXXXXXXXX X
- XX XXXXXXXXXXXXXXXXXXXXXXXX X XX
- XX XXXXXXXXXX XXXXXXXXXXXXX X X
- XX XXXXXXXXXX XXXXXXXXX X X
- XX XX X X X XX X XXX X X
- X X XX X XX XX X XX X X X X
- X X XX X XXX XX X XX XXX X X X
- X X XXX X X X X X
- """
-
-
- class SpaceInvader1(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXXX XXXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXX x x XXXXXXXXX
- XXXXXXXXXX XX XX XXXXXXXXX
- XXXXXXXXXXXXXX XXXXXXXXXXXXX
- XXXXXXXXXX XXXXXXXXX
- XXXXXXXXXX XXXXXXXXXXX XXXXXXXXX
- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- """
-
-
- class SpaceInvader2(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXXXX x x XXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXX X X XXXXXXXXX
- XXXXXXXXXX XXXXXXXXX
- XXXXXXXXXX XXX XXX XXXXXXXXX
- XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX
- XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX
- """
-
-
- class SpaceInvader3(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXX
- XXXXXXXXXXXX X X XXXXXXXXXXX
- XXXXXXXXXX XXXXXXXXX
- XXXXXXXXXX XX XXX XX XXXXXXXXX
- XXXXXXXXXXXXXX XXXXXXXXXXXXX
- XXXXXXXXXXXXX XXX XXXXXXXXXXXX
- """
-
-
- class SpaceInvader4(BlackScreen):
- str_grid = """
- XXXXXXXXXXXXX XXXXXXXXXXXX
- XXXXXXXXXXXXXXX XXXXXXXXXXXXXX
- XXXXXXXXXXXXX XXXXXXXXXXX
- XXXXXXXXXXXX X XXXXXXXXXXX
- XXXXXXXXXX XXXXXXXXX
- XXXXXXXXXX XX XX XXXXXXXXX
- XXXXXXXXXXXXXX X XXXXXXXXXXXXX
- XXXXXXXXXXXXX XXX XXXXXXXXXXXX
- """
-
-
- # Encoded icons
- class Boat(BlackScreen):
- encoded = "QIAAABACAGIEMEOEPHAEAGACABABAAAAAA"
-
-
- # Default weather icons for mark1
- class SunnyIcon(FaceplateGrid):
- encoded = "IICEIBMDNLMDIBCEAA"
-
-
- class PartlyCloudyIcon(FaceplateGrid):
- encoded = "IIEEGBGDHLHDHBGEEA"
-
-
- class CloudyIcon(FaceplateGrid):
- encoded = "IIIBMDMDODODODMDIB"
-
-
- class LightRainIcon(FaceplateGrid):
- encoded = "IIMAOJOFPBPJPFOBMA"
-
-
- class RainIcon(FaceplateGrid):
- encoded = "IIMIOFOBPFPDPJOFMA"
-
-
- class StormIcon(FaceplateGrid):
- encoded = "IIAAIIMEODLBJAAAAA"
-
-
- class SnowIcon(FaceplateGrid):
- encoded = "IIJEKCMBPHMBKCJEAA"
-
-
- class WindIcon(FaceplateGrid):
- encoded = "IIABIBIBIJIJJGJAGA"
diff --git a/ovos_utils/events.py b/ovos_utils/events.py
index 8c603bf7..cef7c169 100644
--- a/ovos_utils/events.py
+++ b/ovos_utils/events.py
@@ -5,7 +5,7 @@
from ovos_utils.fakebus import Message, FakeBus, dig_for_message
from ovos_utils.file_utils import to_alnum
-from ovos_utils.log import LOG, log_deprecation, deprecated
+from ovos_utils.log import LOG
def unmunge_message(message, skill_id: str):
@@ -206,17 +206,8 @@ def clear(self):
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__
+ def __init__(self, bus=None, skill_id=None):
+ self.skill_id = skill_id or self.__class__.__name__.lower()
self.bus = bus
self.events = EventContainer(bus)
self.scheduled_repeats = []
@@ -230,15 +221,14 @@ def set_bus(self, bus):
self.bus = bus
self.events.set_bus(bus)
- def set_id(self, sched_id: str):
+ def set_id(self, skill_id: str):
"""
Attach the skill_id of the parent skill
Args:
- sched_id (str): skill_id of the parent skill
+ skill_id (str): skill_id of the parent skill
"""
- # NOTE: can not rename sched_id kwarg to keep api compatibility
- self.skill_id = sched_id
+ self.skill_id = skill_id
def _get_source_message(self):
message = dig_for_message() or Message("")
@@ -427,39 +417,3 @@ def shutdown(self):
"""
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
diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py
index 1ce41efd..0b02f6b1 100644
--- a/ovos_utils/file_utils.py
+++ b/ovos_utils/file_utils.py
@@ -3,19 +3,47 @@
import os
import re
import tempfile
+from os import walk
+from os.path import dirname, splitext, join
+from sys import platform
from threading import RLock
from typing import Optional, List
-from os import walk
-from os.path import dirname
-from os.path import splitext, join
-
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from ovos_utils.bracket_expansion import expand_options
from ovos_utils.log import LOG, log_deprecation
-from ovos_utils.system import search_mycroft_core_location
+
+
+def ensure_directory_exists(directory, domain=None):
+ """ Create a directory and give access rights to all
+
+ Args:
+ domain (str): The IPC domain. Basically a subdirectory to prevent
+ overlapping signal filenames.
+
+ Returns:
+ str: a path to the directory
+ """
+ if domain:
+ directory = os.path.join(directory, domain)
+
+ # Expand and normalize the path
+ directory = os.path.normpath(directory)
+ directory = os.path.expanduser(directory)
+
+ if not os.path.isdir(directory):
+ try:
+ save = os.umask(0)
+ os.makedirs(directory, 0o777) # give everyone rights to r/w here
+ except OSError:
+ LOG.warning("Failed to create: " + directory)
+ pass
+ finally:
+ os.umask(save)
+
+ return directory
def to_alnum(skill_id: str) -> str:
@@ -59,12 +87,12 @@ def get_temp_path(*args) -> str:
def get_cache_directory(folder: str) -> str:
"""
Get a temporary cache directory, preferably in RAM.
- Note that Windows will not use RAM.
+ Note that only Linux use RAM.
@param folder: base path to use for cache
@return: valid cache path
"""
path = get_temp_path(folder)
- if os.name != 'nt':
+ if platform == 'linux':
try:
from memory_tempfile import MemoryTempfile
path = join(MemoryTempfile(fallback=True).gettempdir(), folder)
diff --git a/ovos_utils/fingerprinting.py b/ovos_utils/fingerprinting.py
deleted file mode 100644
index 073e81b3..00000000
--- a/ovos_utils/fingerprinting.py
+++ /dev/null
@@ -1,471 +0,0 @@
-import platform
-import socket
-from enum import Enum
-from os.path import join, isfile
-from ovos_utils.log import LOG, deprecated
-from ovos_utils.system import is_installed, is_running_from_module, has_screen, \
- get_desktop_environment, search_mycroft_core_location, is_process_running
-
-
-class MycroftPlatform(str, Enum):
- PICROFT = "picroft"
- BIGSCREEN = "kde"
- OVOS = "OpenVoiceOS"
- HIVEMIND = "HiveMind"
- MARK1 = "mycroft_mark_1"
- MARK2 = "mycroft_mark_2"
- HOLMESV = "HolmesV"
- OLD_HOLMES = "mycroft-lib"
- NEON = "neon_core"
- CHATTERBOX = "chatterbox"
- OTHER = "unknown"
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def detect_platform():
- return max(((k, v) for k, v in classify_fingerprint().items()),
- key=lambda k: k[1])[0]
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def get_config_fingerprint(config=None):
- if not config:
- try:
- from ovos_config.config import read_mycroft_config
- config = read_mycroft_config()
- except ImportError:
- LOG.warning("Config not provided and ovos_config not available")
- config = dict()
- conf = config
- listener_conf = conf.get("listener", {})
- skills_conf = conf.get("skills", {})
- return {
- "enclosure": conf.get("enclosure", {}).get("platform"),
- "data_dir": conf.get("data_dir"),
- "msm_skills_dir": skills_conf.get("msm", {}).get("directory"),
- "ipc_path": conf.get("ipc_path"),
- "input_device_name": listener_conf.get("device_name"),
- "input_device_index": listener_conf.get("device_index"),
- "default_audio_backend": conf.get("Audio", {}).get("default-backend"),
- "priority_skills": skills_conf.get("priority_skills"),
- "backend_url": conf.get("server", {}).get("url")
- }
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def get_platform_fingerprint():
- return {
- "hostname": socket.gethostname(),
- "platform": platform.platform(),
- "python_version": platform.python_version(),
- "system": platform.system(),
- "version": platform.version(),
- "arch": platform.machine(),
- "release": platform.release(),
- "desktop_env": get_desktop_environment(),
- "mycroft_core_location": search_mycroft_core_location(),
- "can_display": has_screen(),
- "is_gui_installed": is_installed("mycroft-gui-app"),
- "is_vlc_installed": is_installed("vlc"),
- "pulseaudio_running": is_process_running("pulseaudio"),
- "core_supports_xdg": core_supports_xdg(),
- "core_version": {
- "version_str": get_mycroft_version(),
- "is_chatterbox_core": is_chatterbox_core(),
- "is_neon_core": is_neon_core(),
- "is_holmes": is_holmes(),
- "is_ovos": is_ovos(),
- "is_mycroft_core": is_mycroft_core()
- }
- }
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def get_fingerprint():
- finger = get_platform_fingerprint()
- finger["configuration"] = get_config_fingerprint()
- return finger
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def core_supports_xdg():
- return True # no longer optional
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def get_mycroft_version():
- try: # ovos
- from mycroft.version import OVOS_VERSION_STR
- return OVOS_VERSION_STR
- except ImportError:
- pass
- try: # mycroft
- from mycroft.version import CORE_VERSION_STR
- return CORE_VERSION_STR
- except ImportError:
- pass
-
- root = search_mycroft_core_location()
- if root:
- version_file = join(root, "version", "__init__.py")
- if not isfile(version_file):
- version_file = join(root, "mycroft", "version", "__init__.py")
- if isfile(version_file):
- version = []
- with open(version_file) as f:
- text = f.read()
- version.append(
- text.split("CORE_VERSION_MAJOR =")[-1].split("\n")[
- 0].strip())
- version.append(
- text.split("CORE_VERSION_MINOR =")[-1].split("\n")[
- 0].strip())
- version.append(
- text.split("CORE_VERSION_BUILD =")[-1].split("\n")[
- 0].strip())
- version = ".".join(version)
- if "CORE_VERSION_STR = '.'.join(map(str, " \
- "CORE_VERSION_TUPLE)) + " in text:
- version += text.split(
- "CORE_VERSION_STR = '.'.join(map(str, "
- "CORE_VERSION_TUPLE)) + ")[-1].split("\n")[0][1:-1]
- return version
- return None
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def is_chatterbox_core():
- try:
- import chatterbox
- return True
- except ImportError:
- return False
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def is_neon_core():
- try:
- import neon_core
- return True
- except ImportError:
- return False
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def is_mycroft_core():
- try:
- import mycroft
- return True
- except ImportError:
- return False
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def is_vanilla_mycroft_core():
- return is_mycroft_core() and not is_ovos()
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def is_holmes():
- return "HolmesV" in (get_mycroft_version() or "") or is_mycroft_lib()
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def is_mycroft_lib():
- return "mycroft-lib" in (get_mycroft_version() or "")
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def is_ovos():
- return is_running_from_module("ovos-core")
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def classify_platform_print(fingerprint=None):
- fingerprint = fingerprint or get_platform_fingerprint()
- # key, val pairs that indicate a certain platform
- fingerprints = {
- MycroftPlatform.PICROFT: {
- "core_supports_xdg": False,
- "core_version": {'is_chatterbox_core': False,
- 'is_neon_core': False,
- 'is_holmes': False,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.BIGSCREEN: {
- "core_supports_xdg": False,
- "core_version": {'is_chatterbox_core': False,
- 'is_neon_core': False,
- 'is_holmes': False,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.OVOS: {
- "core_supports_xdg": True,
- "core_version": {'is_chatterbox_core': False,
- 'is_neon_core': False,
- 'is_holmes': False,
- 'is_ovos': True,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.MARK1: {
- "core_supports_xdg": False,
- "core_version": {'is_chatterbox_core': False,
- 'is_neon_core': False,
- 'is_holmes': False,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.MARK2: {
- "core_supports_xdg": False,
- "core_version": {'is_chatterbox_core': False,
- 'is_neon_core': False,
- 'is_holmes': False,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.HOLMESV: {
- "core_supports_xdg": True,
- "core_version": {'version_str': '20.8.1(HolmesV)',
- 'is_chatterbox_core': False,
- 'is_neon_core': False,
- 'is_holmes': True,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.OLD_HOLMES: {
- "core_supports_xdg": False,
- "core_version": {'version_str': '20.8.1(mycroft-lib)',
- 'is_chatterbox_core': False,
- 'is_neon_core': False,
- 'is_holmes': True,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.CHATTERBOX: {
- "core_supports_xdg": True,
- "core_version": {'is_chatterbox_core': True,
- 'is_neon_core': False,
- 'is_holmes': True,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.NEON: {
- "core_supports_xdg": True,
- "core_version": {'is_chatterbox_core': False,
- 'is_neon_core': True,
- 'is_holmes': True,
- 'is_ovos': False,
- 'is_mycroft_core': True}
- },
- MycroftPlatform.OTHER: {
- "core_supports_xdg": True
- }
- }
-
- # score += score * weight (if key matches)
- weights = {
- MycroftPlatform.PICROFT: {},
- MycroftPlatform.BIGSCREEN: {},
- MycroftPlatform.OVOS: {
- "core_supports_xdg": 1.0,
- "core_version": 3.0
- },
- MycroftPlatform.CHATTERBOX: {
- "core_supports_xdg": 1.0,
- "core_version": 3.0
- },
- MycroftPlatform.NEON: {
- "core_supports_xdg": 1.0,
- "core_version": 3.0
- },
- MycroftPlatform.MARK1: {},
- MycroftPlatform.MARK2: {},
- MycroftPlatform.HOLMESV: {
- "core_supports_xdg": 1.0,
- "core_version": 3.0
- },
- MycroftPlatform.OLD_HOLMES: {
- "core_supports_xdg": 1.0,
- "core_version": 3.0
- },
- MycroftPlatform.OTHER: {"core_supports_xdg": 0.01}
- }
-
- # score -= score * weight (if key does not match)
- negative_weights = {
- MycroftPlatform.PICROFT: {
- "core_supports_xdg": 0.1 # likely to change soon once
- # mycroft-core merges the open PR
- },
- MycroftPlatform.BIGSCREEN: {},
- MycroftPlatform.OVOS: {},
- MycroftPlatform.MARK1: {
- "core_supports_xdg": 0.1 # likely to change soon once
- # mycroft-core merges the open PR
- },
- MycroftPlatform.MARK2: {
- "core_supports_xdg": 0.1 # likely to change soon once
- # mycroft-core merges the open PR
- },
- MycroftPlatform.HOLMESV: {},
- MycroftPlatform.OLD_HOLMES: {},
- MycroftPlatform.OTHER: {}
- }
-
- key_counts = {e: 0 for e in MycroftPlatform}
-
- for k, v in fingerprint.items():
- # compare this fingerprint value to known platform features
- for enclosure in MycroftPlatform:
- if enclosure not in fingerprints:
- continue
- count = key_counts[enclosure] or 1
- # key is in fingerprint
- if fingerprints[enclosure].get(k):
- # key matches
- if fingerprints[enclosure].get(k) == v:
- if k in weights.get(enclosure, []):
- key_counts[enclosure] += count * weights[enclosure][k]
- # key does not match
- elif k in negative_weights.get(enclosure, []):
- key_counts[enclosure] -= abs(count) * \
- negative_weights[enclosure][k]
-
- # score platforms
- m = max(v for v in key_counts.values())
- if not m:
- return {k: 0 for k in key_counts}
- return {k: v / m for k, v in key_counts.items()}
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def classify_config_print(fingerprint=None):
- fingerprint = fingerprint or get_config_fingerprint()
-
- # key, val pairs that indicate a certain platform
- fingerprints = {
- MycroftPlatform.PICROFT: {
- 'backend_url': 'https://api.mycroft.ai',
- 'enclosure': 'picroft'
- },
- MycroftPlatform.BIGSCREEN: {
- 'backend_url': 'https://api.mycroft.ai'
- },
- MycroftPlatform.OVOS: {
- 'enclosure': 'OpenVoiceOS',
- 'data_dir': '/opt/ovos'
- },
- MycroftPlatform.MARK1: {
- 'backend_url': 'https://api.mycroft.ai',
- 'enclosure': 'mycroft_mark_1',
- "data_dir": "/opt/mycroft"
- },
- MycroftPlatform.MARK2: {
- 'backend_url': 'https://api.mycroft.ai',
- 'enclosure': 'mycroft_mark_2',
- "data_dir": "/opt/mycroft"
- },
- MycroftPlatform.HOLMESV: {
- "enclosure": "HolmesV"
- },
- MycroftPlatform.OLD_HOLMES: {
- "enclosure": "mycroft-lib"
- },
- MycroftPlatform.CHATTERBOX: {
- 'enclosure': 'chatterhat', # TODO list comparison
- "data_dir": "~/chatterbox",
- },
- MycroftPlatform.NEON: {},
- MycroftPlatform.OTHER: {}
- }
-
- # score += score * weight (if key matches)
- weights = {
- MycroftPlatform.PICROFT: {
- "enclosure": 1.0,
- 'backend_url': 0.5
- },
- MycroftPlatform.BIGSCREEN: {},
- MycroftPlatform.OVOS: {
- "enclosure": 1.0
- },
- MycroftPlatform.CHATTERBOX: {
- "enclosure": 1.0,
- 'backend_url': 1.0,
- "data_dir": 1.0
- },
- MycroftPlatform.NEON: {
- "enclosure": 1.0
- },
- MycroftPlatform.MARK1: {
- "enclosure": 1.0,
- 'backend_url': 1.0,
- "data_dir": 1.0
- },
- MycroftPlatform.MARK2: {
- "enclosure": 1.0,
- 'backend_url': 1.0,
- "data_dir": 1.0
- },
- MycroftPlatform.HOLMESV: {},
- MycroftPlatform.OTHER: {}
- }
-
- # score -= score * weight (if key does not match)
- negative_weights = {
- MycroftPlatform.PICROFT: {
- "enclosure": 0.2,
- 'backend_url': 0.2
- },
- MycroftPlatform.BIGSCREEN: {},
- MycroftPlatform.OVOS: {},
- MycroftPlatform.MARK1: {
- "enclosure": 3.0,
- 'backend_url': 0.5,
- 'data_dir': 1.0
- },
- MycroftPlatform.MARK2: {
- "enclosure": 3.0,
- 'backend_url': 0.5,
- 'data_dir': 1.5 # pantacor really needs /opt, users cant change it
- },
- MycroftPlatform.HOLMESV: {},
- MycroftPlatform.OLD_HOLMES: {"enclosure": 0.5},
- MycroftPlatform.OTHER: {}
- }
-
- key_counts = {e: 0 for e in MycroftPlatform}
-
- for k, v in fingerprint.items():
- # compare this fingerprint value to known platform features
- for enclosure in MycroftPlatform:
- count = key_counts[enclosure] or 1
- # key is in fingerprint
- if fingerprints[enclosure].get(k):
-
- # key matches
- if fingerprints[enclosure][k] == v:
- if k in weights.get(enclosure, []):
- key_counts[enclosure] += count * weights[enclosure][k]
- # key does not match
- elif k in negative_weights.get(enclosure, []):
- key_counts[enclosure] -= abs(count) * \
- negative_weights[enclosure][k]
-
- # score platforms
- m = max(v for v in key_counts.values())
- if not m:
- return {k: 0 for k in key_counts}
- return {k: v / m for k, v in key_counts.items()}
-
-
-@deprecated("fingerprinting utils are deprecated.", "0.1.0")
-def classify_fingerprint():
- plat = classify_platform_print()
- conf = classify_config_print()
- for k, v in conf.items():
- # high bias for platform fingerprint
- plat[k] = (v * 0.5 + plat[k] * 1.5) / 2
- return plat
diff --git a/ovos_utils/gui.py b/ovos_utils/gui.py
index 766780d7..f6a9d2ae 100644
--- a/ovos_utils/gui.py
+++ b/ovos_utils/gui.py
@@ -1,20 +1,15 @@
-import time
-from collections import namedtuple
-from enum import IntEnum
-from os import walk
-from os.path import join, splitext, isfile, isdir
-from typing import List, Union, Optional, Callable
+from os.path import join, isdir
+from typing import List
-from ovos_utils import resolve_ovos_resource_file, resolve_resource_file
-from ovos_utils.fakebus import Message
-from ovos_utils.log import LOG, log_deprecation, deprecated
+from ovos_utils.log import LOG
+from ovos_bus_client.util import wait_for_reply
from ovos_utils.system import is_installed, has_screen, is_process_running
_default_gui_apps = (
- "mycroft-gui-app",
+ "ovos-gui-app",
"ovos-shell",
- "mycroft-embedded-shell",
- "plasmashell"
+ "mycroft-gui-app",
+ "mycroft-embedded-shell"
)
@@ -38,7 +33,13 @@ def is_gui_running(applications: List[str] = _default_gui_apps) -> bool:
Return true if a GUI application is running
@param applications: list of applications to check for
"""
- return any((is_process_running(app) for app in applications))
+ deprecated = any((is_process_running(app) for app in applications
+ if app.startswith("mycroft-")))
+ if deprecated:
+ LOG.warning("you are running a deprecated mycroft-gui version, "
+ "please move to a OVOS maintained version")
+ return True
+ return deprecated or any((is_process_running(app) for app in applications))
def is_gui_connected(bus=None) -> bool:
@@ -48,10 +49,6 @@ def is_gui_connected(bus=None) -> bool:
@param bus: MessageBusClient to use for query
@return: True if GUI is connected
"""
- try:
- from ovos_bus_client.util import wait_for_reply
- except:
- from ovos_utils.messagebus import wait_for_reply
response = wait_for_reply("gui.status.request",
"gui.status.request.response", bus=bus)
if response:
@@ -94,1141 +91,8 @@ def get_ui_directories(root_dir: str) -> dict:
LOG.debug("Skill implements resources in `gui` directory")
ui_directories["all"] = join(base_directory, "gui")
return ui_directories
- LOG.info("Checking for legacy UI directories")
+
if isdir(join(base_directory, "ui")):
- LOG.debug("Handling `ui` directory as `qt5`")
+ LOG.debug("legacy UI directory found - Handling `ui` directory as `qt5`")
ui_directories["qt5"] = join(base_directory, "ui")
return ui_directories
-
-
-class GUIPlaybackStatus(IntEnum):
- STOPPED = 0
- PLAYING = 1
- PAUSED = 2
- UNDEFINED = 3
-
-
-class GUITracker:
- """ Replicates GUI API from mycroft-core,
- does not interact with GUI but exactly mimics status"""
- Namespace = namedtuple('Namespace', ['name', 'pages'])
- RESERVED_KEYS = ['__from', '__idle']
- IDLE_MESSAGE = "mycroft.mark2.collect_idle" # TODO this will change
-
- @deprecated("GUITracker has been deprecated without replacement", "0.1.0")
- def __init__(self, bus=None,
- host='0.0.0.0', port=8181, route='/core', ssl=False):
- if not bus:
- from ovos_bus_client.util import get_mycroft_bus
- bus = get_mycroft_bus(host, port, route, ssl)
- self.bus = bus
- self._active_skill = None
- self._is_idle = False
- self.idle_ts = 0
- # This datastore holds the data associated with the GUI provider. Data
- # is stored in Namespaces, so you can have:
- # self.datastore["namespace"]["name"] = value
- # Typically the namespace is a meaningless identifier, but there is a
- # special "SYSTEM" namespace.
- self._datastore = {}
-
- # self.loaded is a list, each element consists of a namespace named
- # tuple.
- # The namespace namedtuple has the properties "name" and "pages"
- # The name contains the namespace name as a string and pages is a
- # mutable list of loaded pages.
- #
- # [Namespace name, [List of loaded qml pages]]
- # [
- # ["SKILL_NAME", ["page1.qml, "page2.qml", ... , "pageN.qml"]
- # [...]
- # ]
- self._loaded = [] # list of lists in order.
-
- # Listen for new GUI clients to announce themselves on the main bus
- self._active_namespaces = []
-
- # GUI handlers
- self.bus.on("gui.value.set", self._on_gui_set_value)
- self.bus.on("gui.page.show", self._on_gui_show_page)
- self.bus.on("gui.page.delete", self._on_gui_delete_page)
- self.bus.on("gui.clear.namespace", self._on_gui_delete_namespace)
-
- # Idle screen handlers TODO message cleanup...
- self._idle_screens = {}
- self.bus.on("mycroft.device.show.idle", self._on_show_idle) # legacy
- self.bus.on(self.IDLE_MESSAGE, self._on_show_idle)
- self.bus.on("mycroft.mark2.register_idle", self._on_register_idle)
-
- self.bus.emit(Message("mycroft.mark2.collect_idle"))
-
- @staticmethod
- def is_gui_installed():
- return is_gui_installed()
-
- @staticmethod
- def is_gui_running():
- return is_gui_running()
-
- def is_gui_connected(self):
- return is_gui_connected(self.bus)
-
- @staticmethod
- def can_display():
- return can_display()
-
- def is_displaying(self):
- return self.active_skill is not None
-
- def is_idle(self):
- return self._is_idle
-
- @property
- def active_skill(self):
- return self._active_skill
-
- @property
- def gui_values(self):
- return self._datastore
-
- @property
- def idle_screens(self):
- return self._idle_screens
-
- @property
- def active_namespaces(self):
- return self._active_namespaces
-
- @property
- def gui_pages(self):
- return self._loaded
-
- # GUI event handlers
- # user can/should subclass this
- def on_idle(self, namespace):
- pass
-
- def on_active(self, namespace):
- pass
-
- def on_new_page(self, namespace, page, index):
- pass
-
- def on_delete_page(self, namespace, index):
- pass
-
- def on_gui_value(self, namespace, key, value):
- pass
-
- def on_new_namespace(self, namespace):
- pass
-
- def on_move_namespace(self, namespace, from_index, to_index):
- pass
-
- def on_remove_namespace(self, namespace, index):
- pass
-
- ######################################################################
- # GUI client API
- # TODO see how much of this can be removed
- @staticmethod
- def _get_page_data(message):
- """ Extract page related data from a message.
-
- Args:
- message: messagebus message object
- Returns:
- tuple (page, namespace, index)
- Raises:
- ValueError if value is missing.
- """
- data = message.data
- # Note: 'page' can be either a string or a list of strings
- if 'page' not in data:
- raise ValueError("Page missing in data")
- if 'index' in data:
- index = data['index']
- else:
- index = 0
- page = data.get("page", "")
- namespace = data.get("__from", "")
- return page, namespace, index
-
- def _set(self, namespace, name, value):
- """ Perform the send of the values to the connected GUIs. """
- if namespace not in self._datastore:
- self._datastore[namespace] = {}
- if self._datastore[namespace].get(name) != value:
- self._datastore[namespace][name] = value
-
- def __find_namespace(self, namespace):
- for i, skill in enumerate(self._loaded):
- if skill[0] == namespace:
- return i
- return None
-
- def __insert_pages(self, namespace: str, pages: List[str]):
- """ Insert pages into the namespace
-
- Args:
- namespace (str): Namespace to add to
- pages (list): Pages (str) to insert
- """
- LOG.debug("Inserting new pages")
- # Insert the pages into local reprensentation as well.
- updated = self.Namespace(self._loaded[0].name,
- self._loaded[0].pages + pages)
- self._loaded[0] = updated
-
- def __remove_page(self, namespace, pos):
- """ Delete page.
-
- Args:
- namespace (str): Namespace to remove from
- pos (int): Page position to remove
- """
- LOG.debug("Deleting {} from {}".format(pos, namespace))
- self.on_delete_page(namespace, pos)
- # Remove the page from the local reprensentation as well.
- self._loaded[0].pages.pop(pos)
-
- def __insert_new_namespace(self, namespace: str, pages: List[str]):
- """ Insert new namespace and pages.
-
- This first sends a message adding a new namespace at the
- highest priority (position 0 in the namespace stack)
-
- Args:
- namespace (str): The skill namespace to create
- pages (str): Pages to insert (name matches QML)
- """
- LOG.debug("Inserting new namespace")
- self.on_new_namespace(namespace)
- # Make sure the local copy is updated
- self._loaded.insert(0, self.Namespace(namespace, pages))
- if time.time() - self.idle_ts > 1:
- # we cant know if this page is idle or not, but when it is we
- # received a idle event within the same second
- self._is_idle = False
- self.on_active(namespace)
- else:
- self.on_idle(namespace)
-
- def __move_namespace(self, from_pos, to_pos):
- """ Move an existing namespace to a new position in the stack.
-
- Args:
- from_pos (int): Position in the stack to move from
- to_pos (int): Position to move to
- """
- LOG.debug("Activating existing namespace")
- # Move the local representation of the skill from current
- # position to position 0.
- namespace = self._loaded[from_pos].name
- self.on_move_namespace(namespace, from_pos, to_pos)
- self._loaded.insert(to_pos, self._loaded.pop(from_pos))
-
- def _show(self, namespace, page, index):
- """ Show a page and load it as needed.
-
- Args:
- page (str or list): page(s) to show
- namespace (str): skill namespace
- index (int): ??? TODO: Unused in code ???
-
- TODO: - Update sync to match.
- - Separate into multiple functions/methods
- """
-
- LOG.debug("GUIConnection activating: " + namespace)
- self._active_skill = namespace
- pages = page if isinstance(page, list) else [page]
-
- # find namespace among loaded namespaces
- try:
- index = self.__find_namespace(namespace)
- if index is None:
- # This namespace doesn't exist, insert them first so they're
- # shown.
- self.__insert_new_namespace(namespace, pages)
- return
- else: # Namespace exists
- if index > 0:
- # Namespace is inactive, activate it by moving it to
- # position 0
- self.__move_namespace(index, 0)
-
- # Find if any new pages needs to be inserted
- new_pages = [p for p in pages if
- p not in self._loaded[0].pages]
- if new_pages:
- self.__insert_pages(namespace, new_pages)
- except Exception as e:
- LOG.exception(repr(e))
-
- ######################################################################
- # Internal GUI events
- def _on_gui_set_value(self, message):
- data = message.data
- namespace = data.get("__from", "")
-
- # Pass these values on to the GUI renderers
- for key in data:
- if key not in self.RESERVED_KEYS:
- try:
- self._set(namespace, key, data[key])
- self.on_gui_value(namespace, key, data[key])
- except Exception as e:
- LOG.exception(repr(e))
-
- def _on_gui_delete_page(self, message):
- """ Bus handler for removing pages. """
- page, namespace, _ = self._get_page_data(message)
- try:
- self._remove_pages(namespace, page)
- except Exception as e:
- LOG.exception(repr(e))
-
- def _on_gui_delete_namespace(self, message):
- """ Bus handler for removing namespace. """
- try:
- namespace = message.data['__from']
- self._remove_namespace(namespace)
- except Exception as e:
- LOG.exception(repr(e))
-
- def _on_gui_show_page(self, message):
- try:
- page, namespace, index = self._get_page_data(message)
- # Pass the request to the GUI(s) to pull up a page template
- self._show(namespace, page, index)
- self.on_new_page(namespace, page, index)
- except Exception as e:
- LOG.exception(repr(e))
-
- def _remove_namespace(self, namespace):
- """ Remove namespace.
-
- Args:
- namespace (str): namespace to remove
- """
- index = self.__find_namespace(namespace)
- if index is None:
- return
- else:
- LOG.debug("Removing namespace {} at {}".format(namespace, index))
- self.on_remove_namespace(namespace, index)
- # Remove namespace from loaded namespaces
- self._loaded.pop(index)
-
- def _remove_pages(self, namespace, pages):
- """ Remove the listed pages from the provided namespace.
-
- Args:
- namespace (str): The namespace to modify
- pages (list): List of page names (str) to delete
- """
- try:
- index = self.__find_namespace(namespace)
- if index is None:
- return
- else:
- # Remove any pages that doesn't exist in the namespace
- pages = [p for p in pages if p in self._loaded[index].pages]
- # Make sure to remove pages from the back
- indexes = [self._loaded[index].pages.index(p) for p in pages]
- indexes = sorted(indexes)
- indexes.reverse()
- for page_index in indexes:
- self.__remove_page(namespace, page_index)
- except Exception as e:
- LOG.exception(repr(e))
-
- def _on_register_idle(self, message):
- """Handler for catching incoming idle screens."""
- if "name" in message.data and "id" in message.data:
- screen = message.data["name"]
- if screen not in self._idle_screens:
- self.bus.on("{}.idle".format(message.data["id"]),
- self._on_show_idle)
- self._idle_screens[screen] = message.data["id"]
- LOG.info("Registered {}".format(message.data["name"]))
- else:
- LOG.error("Malformed idle screen registration received")
-
- def _on_show_idle(self, message):
- self.idle_ts = time.time()
- self._is_idle = True
-
-
-try:
- from ovos_bus_client.apis.gui import extend_about_data as _ead, GUIInterface as _GI, GUIWidgets as _GW
-
-
- def extend_about_data(about_data: Union[list, dict],
- bus=None):
- return _ead(about_data, bus)
-
-
- class GUIWidgets(_GW):
- def __new__(cls, *args, **kwargs):
- log_deprecation("GUIWidgets moved to ovos_bus_client.apis.gui", "0.1.0")
- return _GW(*args, **kwargs)
-
-
- class GUIInterface(_GI):
- def __new__(cls, *args, **kwargs):
- log_deprecation("GUIInterface moved to ovos_bus_client.apis.gui", "0.1.0")
- return _GI(*args, **kwargs)
-
-except ImportError:
-
- @deprecated("extend_about_data moved to ovos_bus_client.apis.gui", "0.1.0")
- 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
- """
- if not bus:
- from ovos_bus_client.util import get_mycroft_bus
- bus = 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:
- @deprecated("GUIWidgets moved to ovos_bus_client.apis.gui", "0.1.0")
- def __init__(self, bus=None):
- if not bus:
- from ovos_bus_client.util import get_mycroft_bus
- bus = get_mycroft_bus()
- self.bus = 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
- """
-
- @deprecated("GUIInterface moved to ovos_bus_client.apis.gui", "0.1.0")
- 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):
- if not bus:
- from ovos_bus_client.util import get_mycroft_bus
- bus = get_mycroft_bus()
- self._bus = 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)
diff --git a/ovos_utils/intents/__init__.py b/ovos_utils/intents/__init__.py
deleted file mode 100644
index 2d329971..00000000
--- a/ovos_utils/intents/__init__.py
+++ /dev/null
@@ -1,143 +0,0 @@
-from ovos_utils.intents.intent_service_interface import IntentQueryApi, \
- IntentServiceInterface
-from ovos_utils.intents.converse import ConverseTracker
-from ovos_utils.intents.layers import IntentLayers
-from ovos_utils.log import log_deprecation
-
-log_deprecation("ovos_utils.intents moved to ovos_workshop.intents", "0.1.0")
-
-try:
- from ovos_workshop.intents import *
-
-except ImportError:
-
- class Intent:
- def __init__(self, name, requires, at_least_one, optional):
- """Create Intent object
- Args:
- name(str): Name for Intent
- requires(list): Entities that are required
- at_least_one(list): One of these Entities are required
- optional(list): Optional Entities used by the intent
- """
- self.name = name
- self.requires = requires
- self.at_least_one = at_least_one
- self.optional = optional
-
- def validate(self, tags, confidence):
- """Using this method removes tags from the result of validate_with_tags
- Returns:
- intent(intent): Results from validate_with_tags
- """
- raise NotImplementedError("please install adapt-parser")
-
- def validate_with_tags(self, tags, confidence):
- """Validate whether tags has required entites for this intent to fire
- Args:
- tags(list): Tags and Entities used for validation
- confidence(float): The weight associate to the parse result,
- as indicated by the parser. This is influenced by a parser
- that uses edit distance or context.
- Returns:
- intent, tags: Returns intent and tags used by the intent on
- failure to meat required entities then returns intent with
- confidence
- of 0.0 and an empty list for tags.
- """
- raise NotImplementedError("please install adapt-parser")
-
-
- class IntentBuilder:
- """
- IntentBuilder, used to construct intent parsers.
- Attributes:
- at_least_one(list): A list of Entities where one is required.
- These are separated into lists so you can have one of (A or B) and
- then require one of (D or F).
- requires(list): A list of Required Entities
- optional(list): A list of optional Entities
- name(str): Name of intent
- Notes:
- This is designed to allow construction of intents in one line.
- Example:
- IntentBuilder("Intent")\
- .requires("A")\
- .one_of("C","D")\
- .optional("G").build()
- """
-
- def __init__(self, intent_name):
- """
- Constructor
- Args:
- intent_name(str): the name of the intents that this parser
- parses/validates
- """
- self.at_least_one = []
- self.requires = []
- self.optional = []
- self.name = intent_name
-
- def one_of(self, *args):
- """
- The intent parser should require one of the provided entity types to
- validate this clause.
- Args:
- args(args): *args notation list of entity names
- Returns:
- self: to continue modifications.
- """
- self.at_least_one.append(args)
- return self
-
- def require(self, entity_type, attribute_name=None):
- """
- The intent parser should require an entity of the provided type.
- Args:
- entity_type(str): an entity type
- attribute_name(str): the name of the attribute on the parsed intent.
- Defaults to match entity_type.
- Returns:
- self: to continue modifications.
- """
- if not attribute_name:
- attribute_name = entity_type
- self.requires += [(entity_type, attribute_name)]
- return self
-
- def optionally(self, entity_type, attribute_name=None):
- """
- Parsed intents from this parser can optionally include an entity of the
- provided type.
- Args:
- entity_type(str): an entity type
- attribute_name(str): the name of the attribute on the parsed intent.
- Defaults to match entity_type.
- Returns:
- self: to continue modifications.
- """
- if not attribute_name:
- attribute_name = entity_type
- self.optional += [(entity_type, attribute_name)]
- return self
-
- def build(self):
- """
- Constructs an intent from the builder's specifications.
- :return: an Intent instance.
- """
- return Intent(self.name, self.requires,
- self.at_least_one, self.optional)
-
-
-class AdaptIntent(IntentBuilder):
- """Wrapper for IntentBuilder setting a blank name.
-
- Args:
- name (str): Optional name of intent
- """
-
- def __init__(self, name=''):
- super().__init__(name)
-
diff --git a/ovos_utils/intents/converse.py b/ovos_utils/intents/converse.py
deleted file mode 100644
index 8f8c6e6e..00000000
--- a/ovos_utils/intents/converse.py
+++ /dev/null
@@ -1,205 +0,0 @@
-import time
-
-import ovos_utils.messagebus
-from ovos_utils.intents.intent_service_interface import IntentQueryApi
-from ovos_utils.log import LOG, deprecated
-
-
-class ConverseTracker:
- """ Using the messagebus this class recreates/keeps track of the state
- of the converse system, it uses both passive listening and active
- queries to sync it's state, it also emits 2 new bus events
-
- Implements https://github.com/MycroftAI/mycroft-core/pull/1468
- """
- bus = None
- active_skills = []
- converse_timeout = 5 # MAGIC NUMBER hard coded in mycroft-core
- last_conversed = None
- intent_api = None
-
- @deprecated("ConverseTracker has been deprecated without replacement", "0.1.0")
- def __init__(self):
- pass
-
- @classmethod
- def connect_bus(cls, mycroft_bus):
- """Registers the bus object to use."""
- # PATCH - in mycroft-core this would be handled in intent_service
- # in here it is done in MycroftSkill.bind so i added this
- # conditional check
- if cls.bus is None and mycroft_bus is not None:
- cls.bus = mycroft_bus
- cls.intent_api = IntentQueryApi(cls.bus)
- cls.register_bus_events()
-
- @classmethod
- def register_bus_events(cls):
- cls.bus.on('active_skill_request', cls.handle_activate_request)
- cls.bus.on('skill.converse.response', cls.handle_converse_response)
- cls.bus.on("mycroft.skill.handler.start", cls.handle_intent_start)
- cls.bus.on("recognizer_loop:utterance", cls.handle_utterance)
-
- # public methods
- @classmethod
- def check_skill(cls, skill_id):
- """ Check if a skill is active """
- cls.filter_active_skills()
- for skill in list(cls.active_skills):
- if skill[0] == skill_id:
- return True
- return False
-
- @classmethod
- def filter_active_skills(cls):
- """ Removes expired skills from active skill list """
- # filter timestamps
- for skill in list(cls.active_skills):
- if time.time() - skill[1] <= cls.converse_timeout * 60:
- cls.remove_active_skill(skill[0])
-
- @classmethod
- def sync_with_intent_service(cls):
- """sync active skill list using intent api
-
- WARNING
- we don't have the timestamps so order might be messed up!!
- avoid calling this until
- """
- skill_ids = cls.intent_api.get_active_skills(include_timestamps=True)
- if skill_ids:
- if len(skill_ids[0]) == 2:
- # PR was merged! hurray!
- cls.active_skills = skill_ids
- else:
- # hoping they come sorted by timestamp....
- # older to newer (most recently used)
- for skill_id in reversed(skill_ids):
- # are we tracking this skill ?
- if not cls.check_skill(skill_id):
- # we missed adding this skill in our tracking
- cls.add_active_skill(skill_id)
- for skill in cls.active_skills:
- if skill[0] not in skill_ids:
- # we missed removing this skill in our tracking
- cls.remove_active_skill(skill[0])
-
- # https://github.com/MycroftAI/mycroft-core/pull/1468
- @classmethod
- def remove_active_skill(cls, skill_id, silent=False):
- """
- Emits "converse.skill.deactivated" event, improvement of #1468
- """
- for skill in list(cls.active_skills):
- if skill[0] == skill_id:
- cls.active_skills.remove(skill)
- if not silent:
- cls.bus.emit(ovos_utils.messagebus.Message("converse.skill.deactivated",
- {"skill_id": skill[0]}))
-
- @classmethod
- def add_active_skill(cls, skill_id):
- """
- Emits "converse.skill.activated" event, improvement of #1468
- """
- # search the list for an existing entry that already contains it
- # and remove that reference
- if skill_id != '':
- cls.remove_active_skill(skill_id, silent=True)
- # add skill with timestamp to start of skill_list
- cls.active_skills.insert(0, [skill_id, time.time()])
- # this might be sent more than once and it's perfectly fine
- # it's just a new info message not consumed anywhere by default
- cls.bus.emit(ovos_utils.messagebus.Message("converse.skill.activated",
- {"skill_id": skill_id}))
- else:
- LOG.warning('Skill ID was empty, won\'t add to list of '
- 'active skills.')
-
- # status tracking
- @classmethod
- def handle_activate_request(cls, message):
- """
- a skill bumped itself to the top of active skills list
- duplicate functionality from mycroft-core, keeping list in sync
- """
- skill_id = message.data["skill_id"]
- cls.add_active_skill(skill_id)
-
- @classmethod
- def handle_converse_error(cls, message):
- """
- a skill was removed from active skill list due to converse error
- duplicate functionality from mycroft-core, keeping list in sync
- """
- skill_id = message.data["skill_id"]
- if message.data["error"] == "skill id does not exist":
- cls.remove_active_skill(skill_id)
-
- @classmethod
- def handle_intent_start(cls, message):
- """
- duplicate functionality from mycroft-core, keeping list in sync
-
- TODO skill_id from message, core is not passing it along... it used
- to be possible to retrieve it from munged message but that changed.
- send a PR (if those got merged this code wouldn't exist)
-
- handle_utterance will take over this functionality for now
- handle_converse_response will take corrective action
- """
- # skill_id = message.data["skill_id"]
- # bump skill to top of active list
- # cls.add_active_skill(skill_id)
-
- @classmethod
- def handle_utterance(cls, message):
- """
- duplicate functionality from mycroft-core, keeping list in sync
-
- WORKAROUND - skill_id missing in handle_intent_start, will keep list
- in sync by using the IntentAPI to check what skill the utterance
- should trigger
-
- handle_converse_response will take corrective action
- """
- # NOTE borked in mycroft-core
- # needs https://github.com/MycroftAI/mycroft-core/pull/2786
- skill_id = cls.intent_api.get_skill(message.data["utterances"][0])
- if skill_id:
- # this skill will trigger and therefore is the last active skill
- cls.add_active_skill(skill_id)
- # will remove expired intents from list
- cls.filter_active_skills()
-
- @classmethod
- def handle_converse_response(cls, message):
- """
- tracks last_conversed skill
-
- FAILSAFE - additional checks to correct active skills list,
- but that should never happen, accounts for mistakes in
- handle_utterance / intent_api
-
- accounts for https://github.com/MycroftAI/mycroft-core/pull/2786
- not yet being merged
- """
- skill_id = message.data["skill_id"]
- if 'error' in message.data:
- cls.handle_converse_error(message)
- elif message.data.get('result') is True:
- cls.last_conversed = skill_id
- if not cls.check_skill(skill_id):
- # seems like we missed adding this skill to active list
- # NOTE this is a failsafe and should never trigger
- # since this answered True we have the real timestamp
- cls.add_active_skill(skill_id)
- elif not cls.check_skill(skill_id):
- # seems like we missed adding this skill to active list
- # NOTE this is a failsafe and should never trigger
- # since this answered false and we don't have the real timestamp
- # let's add it to the end of the active_skills list
- ts = time.time()
- if len(cls.active_skills):
- ts = cls.active_skills[-1][1]
- cls.active_skills.append([skill_id, ts])
diff --git a/ovos_utils/intents/intent_service_interface.py b/ovos_utils/intents/intent_service_interface.py
deleted file mode 100644
index cddadff5..00000000
--- a/ovos_utils/intents/intent_service_interface.py
+++ /dev/null
@@ -1,585 +0,0 @@
-from os.path import exists, isfile
-from threading import RLock
-from typing import List, Tuple, Optional
-
-import ovos_utils.messagebus
-from ovos_utils.log import LOG, log_deprecation, deprecated
-
-from ovos_utils.file_utils import to_alnum # backwards compat import
-
-log_deprecation("ovos_utils.intents moved to ovos_workshop.intents", "0.1.0")
-
-
-try:
- from ovos_workshop.intents import *
-except:
-
- def munge_regex(regex: str, skill_id: str) -> str:
- """
- Insert skill id as letters into match groups.
-
- Args:
- regex (str): regex string
- skill_id (str): skill identifier
- Returns:
- (str) munged regex
- """
- base = '(?P<' + to_alnum(skill_id)
- return base.join(regex.split('(?P<'))
-
-
- def munge_intent_parser(intent_parser, name, skill_id):
- """
- Rename intent keywords to make them skill exclusive
- This gives the intent parser an exclusive name in the
- format :. The keywords are given unique
- names in the format .
-
- The function will not munge instances that's already been
- munged
-
- Args:
- intent_parser: (IntentParser) object to update
- name: (str) Skill name
- skill_id: (int) skill identifier
- """
- # Munge parser name
- if not name.startswith(str(skill_id) + ':'):
- intent_parser.name = str(skill_id) + ':' + name
- else:
- intent_parser.name = name
-
- # Munge keywords
- skill_id = to_alnum(skill_id)
- # Munge required keyword
- reqs = []
- for i in intent_parser.requires:
- if not i[0].startswith(skill_id):
- kw = (skill_id + i[0], skill_id + i[0])
- reqs.append(kw)
- else:
- reqs.append(i)
- intent_parser.requires = reqs
-
- # Munge optional keywords
- opts = []
- for i in intent_parser.optional:
- if not i[0].startswith(skill_id):
- kw = (skill_id + i[0], skill_id + i[0])
- opts.append(kw)
- else:
- opts.append(i)
- intent_parser.optional = opts
-
- # Munge at_least_one keywords
- at_least_one = []
- for i in intent_parser.at_least_one:
- element = [skill_id + e.replace(skill_id, '') for e in i]
- at_least_one.append(tuple(element))
- intent_parser.at_least_one = at_least_one
-
-
- class IntentServiceInterface:
- """
- Interface to communicate with the Mycroft intent service.
-
- This class wraps the messagebus interface of the intent service allowing
- for easier interaction with the service. It wraps both the Adapt and
- Padatious parts of the intent services.
- """
-
- def __init__(self, bus=None):
- self._bus = bus
- self.skill_id = self.__class__.__name__
- # TODO: Consider using properties with setters to prevent duplicates
- self.registered_intents: List[Tuple[str, object]] = []
- self.detached_intents: List[Tuple[str, object]] = []
- self._iterator_lock = RLock()
-
- @property
- def intent_names(self) -> List[str]:
- """
- Get a list of intent names (both registered and disabled).
- """
- return [a[0] for a in self.registered_intents + self.detached_intents]
-
- @property
- def bus(self):
- if not self._bus:
- raise RuntimeError("bus not set. call `set_bus()` before trying to"
- "interact with the Messagebus")
- return self._bus
-
- @bus.setter
- def bus(self, val):
- self.set_bus(val)
-
- def set_bus(self, bus=None):
- self._bus = bus or ovos_utils.messagebus.get_mycroft_bus()
-
- def set_id(self, skill_id: str):
- self.skill_id = skill_id
-
- def register_adapt_keyword(self, vocab_type: str, entity: str,
- aliases: Optional[List[str]] = None,
- lang: str = None):
- """
- Send a message to the intent service to add an Adapt keyword.
- @param vocab_type: Keyword reference (file basename)
- @param entity: Primary keyword value
- @param aliases: List of alternative keyword values
- @param lang: BCP-47 language code of entity and aliases
- """
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
-
- # TODO 22.02: Remove compatibility data
- aliases = aliases or []
- entity_data = {'entity_value': entity,
- 'entity_type': vocab_type,
- 'lang': lang}
- compatibility_data = {'start': entity, 'end': vocab_type}
-
- self.bus.emit(msg.forward("register_vocab",
- {**entity_data, **compatibility_data}))
- for alias in aliases:
- alias_data = {
- 'entity_value': alias,
- 'entity_type': vocab_type,
- 'alias_of': entity,
- 'lang': lang}
- compatibility_data = {'start': alias, 'end': vocab_type}
- self.bus.emit(msg.forward("register_vocab",
- {**alias_data, **compatibility_data}))
-
- def register_adapt_regex(self, regex: str, lang: str = None):
- """
- Register a regex string with the intent service.
- @param regex: Regex to be registered; Adapt extracts keyword references
- from named match group.
- @param lang: BCP-47 language code of regex
- """
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
- self.bus.emit(msg.forward("register_vocab",
- {'regex': regex, 'lang': lang}))
-
- def register_adapt_intent(self, name: str, intent_parser: object):
- """
- Register an Adapt intent parser object. Serializes the intent_parser
- and sends it over the messagebus to registered.
- @param name: string intent name (without skill_id prefix)
- @param intent_parser: Adapt Intent object
- """
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
- self.bus.emit(msg.forward("register_intent", intent_parser.__dict__))
- self.registered_intents.append((name, intent_parser))
- self.detached_intents = [detached for detached in self.detached_intents
- if detached[0] != name]
-
- def detach_intent(self, intent_name: str):
- """
- DEPRECATED: Use `remove_intent` instead, all other methods from this
- class expect intent_name; this was the weird one expecting the internal
- munged intent_name with skill_id.
- """
- name = intent_name.split(':')[1]
- log_deprecation(f"Update to `self.remove_intent({name})",
- "0.1.0")
- self.remove_intent(name)
-
- def remove_intent(self, intent_name: str):
- """
- Remove an intent from the intent service. The intent is saved in the
- list of detached intents for use when re-enabling an intent. A
- `detach_intent` Message is emitted for the intent service to handle.
- @param intent_name: Registered intent to remove/detach (no skill_id)
- """
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
- if intent_name in self.intent_names:
- # TODO: This will create duplicates of already detached intents
- LOG.info(f"Detaching intent: {intent_name}")
- self.detached_intents.append((intent_name,
- self.get_intent(intent_name)))
- self.registered_intents = [pair for pair in self.registered_intents
- if pair[0] != intent_name]
- self.bus.emit(msg.forward("detach_intent",
- {"intent_name":
- f"{self.skill_id}:{intent_name}"}))
-
- def intent_is_detached(self, intent_name: str) -> bool:
- """
- Determine if an intent is detached.
- @param intent_name: String intent reference to check (without skill_id)
- @return: True if intent is in detached_intents, else False.
- """
- is_detached = False
- with self._iterator_lock:
- for (name, _) in self.detached_intents:
- if name == intent_name:
- is_detached = True
- break
- return is_detached
-
- def set_adapt_context(self, context: str, word: str, origin: str):
- """
- Set an Adapt context.
- @param context: context keyword name to add/update
- @param word: word to register (context keyword value)
- @param origin: original origin of the context (for cross context)
- """
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
- self.bus.emit(msg.forward('add_context',
- {'context': context, 'word': word,
- 'origin': origin}))
-
- def remove_adapt_context(self, context: str):
- """
- Remove an Adapt context.
- @param context: context keyword name to remove
- """
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
- self.bus.emit(msg.forward('remove_context', {'context': context}))
-
- def register_padatious_intent(self, intent_name: str, filename: str,
- lang: str):
- """
- Register a Padatious intent file with the intent service.
- @param intent_name: Unique intent identifier
- (usually `skill_id`:`filename`)
- @param filename: Absolute file path to entity file
- @param lang: BCP-47 language code of registered intent
- """
- if not isinstance(filename, str):
- raise ValueError('Filename path must be a string')
- if not exists(filename):
- raise FileNotFoundError(f'Unable to find "{filename}"')
- with open(filename) as f:
- samples = [_ for _ in f.read().split("\n") if _
- and not _.startswith("#")]
- data = {'file_name': filename,
- "samples": samples,
- 'name': intent_name,
- 'lang': lang}
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
- self.bus.emit(msg.forward("padatious:register_intent", data))
- self.registered_intents.append((intent_name.split(':')[-1], data))
-
- def register_padatious_entity(self, entity_name: str, filename: str,
- lang: str):
- """
- Register a Padatious entity file with the intent service.
- @param entity_name: Unique entity identifier
- (usually `skill_id`:`filename`)
- @param filename: Absolute file path to entity file
- @param lang: BCP-47 language code of registered intent
- """
- if not isinstance(filename, str):
- raise ValueError('Filename path must be a string')
- if not exists(filename):
- raise FileNotFoundError('Unable to find "{}"'.format(filename))
- with open(filename) as f:
- samples = [_ for _ in f.read().split("\n") if _
- and not _.startswith("#")]
- msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("")
- if "skill_id" not in msg.context:
- msg.context["skill_id"] = self.skill_id
- self.bus.emit(msg.forward('padatious:register_entity',
- {'file_name': filename,
- "samples": samples,
- 'name': entity_name,
- 'lang': lang}))
-
- def get_intent_names(self):
- log_deprecation("Reference `intent_names` directly", "0.1.0")
- return self.intent_names
-
- def detach_all(self):
- """
- Detach all intents associated with this interface and remove all
- internal references to intents and handlers.
- """
- for name in self.intent_names:
- self.remove_intent(name)
- if self.registered_intents:
- LOG.error(f"Expected an empty list; got: {self.registered_intents}")
- self.registered_intents = []
- self.detached_intents = [] # Explicitly remove all intent references
-
- def get_intent(self, intent_name: str) -> Optional[object]:
- """
- Get an intent object by name. This will find both enabled and disabled
- intents.
- @param intent_name: name of intent to find (without skill_id)
- @return: intent object if found, else None
- """
- to_return = None
- with self._iterator_lock:
- for name, intent in self.registered_intents:
- if name == intent_name:
- to_return = intent
- break
- if to_return is None:
- with self._iterator_lock:
- for name, intent in self.detached_intents:
- if name == intent_name:
- to_return = intent
- break
- return to_return
-
- def __iter__(self):
- """Iterator over the registered intents.
-
- Returns an iterator returning name-handler pairs of the registered
- intent handlers.
- """
- return iter(self.registered_intents)
-
- def __contains__(self, val):
- """
- Checks if an intent name has been registered.
- """
- return val in [i[0] for i in self.registered_intents]
-
-
- def open_intent_envelope(message):
- """
- Convert dictionary received over messagebus to Intent.
- """
- # TODO can this method be fully removed from ovos_utils ?
- from adapt.intent import Intent
-
- intent_dict = message.data
- return Intent(intent_dict.get('name'),
- intent_dict.get('requires'),
- intent_dict.get('at_least_one'),
- intent_dict.get('optional'))
-
-
-class IntentQueryApi:
- """
- Query Intent Service at runtime
- """
-
- @deprecated("IntentQueryApi has been deprecated without replacement", "0.1.0")
- def __init__(self, bus=None, timeout=5):
- if bus is None:
- bus = ovos_utils.messagebus.get_mycroft_bus()
- self.bus = bus
- self.timeout = timeout
-
- def get_adapt_intent(self, utterance, lang="en-us"):
- """ get best adapt intent for utterance """
- msg = ovos_utils.messagebus.Message("intent.service.adapt.get",
- {"utterance": utterance, "lang": lang},
- context={"destination": "intent_service",
- "source": "intent_api"})
-
- resp = self.bus.wait_for_response(msg,
- 'intent.service.adapt.reply',
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
- return data["intent"]
-
- def get_padatious_intent(self, utterance, lang="en-us"):
- """ get best padatious intent for utterance """
- msg = ovos_utils.messagebus.Message("intent.service.padatious.get",
- {"utterance": utterance, "lang": lang},
- context={"destination": "intent_service",
- "source": "intent_api"})
- resp = self.bus.wait_for_response(msg,
- 'intent.service.padatious.reply',
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
- return data["intent"]
-
- def get_intent(self, utterance, lang="en-us"):
- """ get best intent for utterance """
- msg = ovos_utils.messagebus.Message("intent.service.intent.get",
- {"utterance": utterance, "lang": lang},
- context={"destination": "intent_service",
- "source": "intent_api"})
- resp = self.bus.wait_for_response(msg,
- 'intent.service.intent.reply',
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
- return data["intent"]
-
- def get_skill(self, utterance, lang="en-us"):
- """ get skill that utterance will trigger """
- intent = self.get_intent(utterance, lang)
- if not intent:
- return None
- # theoretically skill_id might be missing
- if intent.get("skill_id"):
- return intent["skill_id"]
- # retrieve skill from munged intent name
- if intent.get("intent_name"): # padatious + adapt
- return intent["name"].split(":")[0]
- if intent.get("intent_type"): # adapt
- return intent["intent_type"].split(":")[0]
- return None # raise some error here maybe? this should never happen
-
- def get_skills_manifest(self):
- msg = ovos_utils.messagebus.Message("intent.service.skills.get",
- context={"destination": "intent_service",
- "source": "intent_api"})
- resp = self.bus.wait_for_response(msg,
- 'intent.service.skills.reply',
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
- return data["skills"]
-
- def get_active_skills(self, include_timestamps=False):
- msg = ovos_utils.messagebus.Message("intent.service.active_skills.get",
- context={"destination": "intent_service",
- "source": "intent_api"})
- resp = self.bus.wait_for_response(msg,
- 'intent.service.active_skills.reply',
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
- if include_timestamps:
- return data["skills"]
- return [s[0] for s in data["skills"]]
-
- def get_adapt_manifest(self):
- msg = ovos_utils.messagebus.Message("intent.service.adapt.manifest.get",
- context={"destination": "intent_service",
- "source": "intent_api"})
- resp = self.bus.wait_for_response(msg,
- 'intent.service.adapt.manifest',
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
- return data["intents"]
-
- def get_padatious_manifest(self):
- msg = ovos_utils.messagebus.Message("intent.service.padatious.manifest.get",
- context={"destination": "intent_service",
- "source": "intent_api"})
- resp = self.bus.wait_for_response(msg,
- 'intent.service.padatious.manifest',
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
- return data["intents"]
-
- def get_intent_manifest(self):
- padatious = self.get_padatious_manifest()
- adapt = self.get_adapt_manifest()
- return {"adapt": adapt,
- "padatious": padatious}
-
- def get_vocab_manifest(self):
- msg = ovos_utils.messagebus.Message("intent.service.adapt.vocab.manifest.get",
- context={"destination": "intent_service",
- "source": "intent_api"})
- reply_msg_type = 'intent.service.adapt.vocab.manifest'
- resp = self.bus.wait_for_response(msg,
- reply_msg_type,
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
-
- vocab = {}
- for voc in data["vocab"]:
- if voc.get("regex"):
- continue
- if voc["end"] not in vocab:
- vocab[voc["end"]] = {"samples": []}
- vocab[voc["end"]]["samples"].append(voc["start"])
- return [{"name": voc, "samples": vocab[voc]["samples"]}
- for voc in vocab]
-
- def get_regex_manifest(self):
- msg = ovos_utils.messagebus.Message("intent.service.adapt.vocab.manifest.get",
- context={"destination": "intent_service",
- "source": "intent_api"})
- reply_msg_type = 'intent.service.adapt.vocab.manifest'
- resp = self.bus.wait_for_response(msg,
- reply_msg_type,
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
-
- vocab = {}
- for voc in data["vocab"]:
- if not voc.get("regex"):
- continue
- name = voc["regex"].split("(?P<")[-1].split(">")[0]
- if name not in vocab:
- vocab[name] = {"samples": []}
- vocab[name]["samples"].append(voc["regex"])
- return [{"name": voc, "regexes": vocab[voc]["samples"]}
- for voc in vocab]
-
- def get_entities_manifest(self):
- msg = ovos_utils.messagebus.Message("intent.service.padatious.entities.manifest.get",
- context={"destination": "intent_service",
- "source": "intent_api"})
- reply_msg_type = 'intent.service.padatious.entities.manifest'
- resp = self.bus.wait_for_response(msg,
- reply_msg_type,
- timeout=self.timeout)
- data = resp.data if resp is not None else {}
- if not data:
- LOG.error("Intent Service timed out!")
- return None
-
- entities = []
- # read files
- for ent in data["entities"]:
- if isfile(ent["file_name"]):
- with open(ent["file_name"]) as f:
- lines = f.read().replace("(", "").replace(")", "").split(
- "\n")
- samples = []
- for l in lines:
- samples += [a.strip() for a in l.split("|") if a.strip()]
- entities.append({"name": ent["name"], "samples": samples})
- return entities
-
- def get_keywords_manifest(self):
- padatious = self.get_entities_manifest()
- adapt = self.get_vocab_manifest()
- regex = self.get_regex_manifest()
- return {"adapt": adapt,
- "padatious": padatious,
- "regex": regex}
-
-
diff --git a/ovos_utils/intents/layers.py b/ovos_utils/intents/layers.py
deleted file mode 100644
index e1c62ede..00000000
--- a/ovos_utils/intents/layers.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from ovos_utils.log import LOG, deprecated
-
-
-try:
- from ovos_workshop.decorators.layers import IntentLayers
-except ImportError:
- import ovos_utils.messagebus
-
- from time import sleep
-
- class IntentLayers:
-
- @deprecated("IntentLayers moved to ovos_workshop.decorators.layers", "0.1.0")
- def __init__(self, bus=None, layers=None):
- layers = layers or []
- self.bus = bus or ovos_utils.messagebus.get_mycroft_bus()
- # make intent levels for N layers
- self.layers = layers
- self.current_layer = 0
- self.activate_layer(0)
- self.named_layers = {}
-
- def disable_intent(self, intent_name):
- """Disable a registered intent"""
- self.bus.emit(ovos_utils.messagebus.Message("mycroft.skill.disable_intent",
- {"intent_name": intent_name}))
-
- def enable_intent(self, intent_name):
- """Reenable a registered self intent"""
- self.bus.emit(ovos_utils.messagebus.Message("mycroft.skill.enable_intent",
- {"intent_name": intent_name}))
-
- def reset(self):
- LOG.info("Reseting Intent Layers")
- self.activate_layer(0)
-
- def next(self):
- LOG.info("Going to next Intent Layer")
- self.current_layer += 1
- if self.current_layer > len(self.layers):
- LOG.info("Already in last layer, going to layer 0")
- self.current_layer = 0
- self.activate_layer(self.current_layer)
-
- def previous(self):
- LOG.info("Going to previous Intent Layer")
- self.current_layer -= 1
- if self.current_layer < 0:
- self.current_layer = 0
- LOG.error("Already in layer 0")
- else:
- self.activate_layer(self.current_layer)
-
- def add_layer(self, intent_list=None):
- intent_list = intent_list or []
- self.layers.append(intent_list)
- LOG.info("Adding intent layer: " + str(intent_list))
-
- def add_named_layer(self, name, intent_list=None):
- intent_list = intent_list or []
- self.named_layers[name] = len(self.layers)
- self.layers.append(intent_list)
- LOG.info("Setting layer " + name + " to: " + str(intent_list))
-
- def activate_named_layer(self, name):
- if name in self.named_layers:
- i = self.named_layers[name]
- LOG.info("activating layer named: " + name)
- self.activate_layer(i)
- else:
- LOG.error("no layer named: " + name)
-
- def deactivate_named_layer(self, name):
- if name in self.named_layers:
- i = self.named_layers[name]
- LOG.info("deactivating layer named: " + name)
- self.deactivate_layer(i)
- else:
- LOG.error("no layer named: " + name)
-
- def remove_named_layer(self, name):
- if name in self.named_layers:
- i = self.named_layers[name]
- LOG.info("removing layer named: " + name)
- self.remove_layer(i)
- else:
- LOG.error("no layer named: " + name)
-
- def replace_named_layer(self, name, intent_list=None):
- if name in self.named_layers:
- i = self.named_layers[name]
- LOG.info("replacing layer named: " + name)
- self.replace_layer(i, intent_list)
- else:
- LOG.error("no layer named: " + name)
-
- def replace_layer(self, layer_num, intent_list=None):
- intent_list = intent_list or []
- if self.current_layer == layer_num:
- self.deactivate_layer(layer_num)
- LOG.info("Adding layer" + str(intent_list) + " in position " + str(
- layer_num))
- self.layers[layer_num] = intent_list
- if self.current_layer == layer_num:
- self.activate_layer(layer_num)
-
- def remove_layer(self, layer_num):
- if layer_num >= len(self.layers):
- return False
- if self.current_layer == layer_num:
- self.deactivate_layer(layer_num)
- self.layers.pop(layer_num)
- LOG.info("Removing layer number " + str(layer_num))
- return True
-
- def find_layer(self, intent_name):
- layer_list = []
- for i in range(0, len(self.layers)):
- if intent_name in self.layers[i]:
- layer_list.append(i)
- return layer_list
-
- def disable(self):
- LOG.info("Disabling layers")
- # disable all layers
- for i in range(0, len(self.layers)):
- self.deactivate_layer(i)
-
- def activate_layer(self, layer_num):
- # error check
- if layer_num < 0 or layer_num > len(self.layers):
- LOG.error("invalid layer number")
- return False
-
- self.current_layer = layer_num
-
- # disable other layers
- self.disable()
-
- # TODO in here we should wait for all intents to be detached
- # sometimes detach intent from this step comes after register from next
- # is there some bus signal we can track?
- sleep(0.3)
-
- # enable layer
- LOG.info("Activating Layer " + str(layer_num))
- if layer_num < len(self.layers) and len(self.layers):
- for intent_name in self.layers[layer_num]:
- self.enable_intent(intent_name)
- return True
- return False
-
- def deactivate_layer(self, layer_num):
- # error check
- if layer_num < 0 or layer_num > len(self.layers):
- LOG.error("invalid layer number")
- return False
- LOG.info("Deactivating Layer " + str(layer_num))
- for intent_name in self.layers[layer_num]:
- self.disable_intent(intent_name)
- return True
diff --git a/ovos_utils/json_helper.py b/ovos_utils/json_helper.py
index c54ba9b5..ed356450 100644
--- a/ovos_utils/json_helper.py
+++ b/ovos_utils/json_helper.py
@@ -1,9 +1,5 @@
import json
from copy import copy
-# TODO: Deprecate unused imports
-from json_database.utils import is_jsonifiable, get_key_recursively, \
- get_key_recursively_fuzzy, get_value_recursively_fuzzy, \
- get_value_recursively, jsonify_recursively
def nested_get(base, key_list):
diff --git a/ovos_utils/log.py b/ovos_utils/log.py
index d054053b..6f4c69a0 100644
--- a/ovos_utils/log.py
+++ b/ovos_utils/log.py
@@ -12,12 +12,15 @@
#
import functools
import inspect
+import json
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from os.path import join
-from typing import List
+from pathlib import Path
+from typing import Optional, List, Set
+
class LOG:
"""
@@ -77,21 +80,21 @@ def __init__(cls, name='OVOS'):
@classmethod
def init(cls, config=None):
-
+ from ovos_utils.xdg_utils import xdg_state_home
try:
from ovos_config.meta import get_xdg_base
- default_base = get_xdg_base()
+ xdg_base = get_xdg_base()
except ImportError:
- default_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER") or \
- "mycroft"
- from ovos_utils.xdg_utils import xdg_state_home
+ xdg_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER") or "mycroft"
+
+ xdg_path = os.path.join(xdg_state_home(), xdg_base)
config = config or {}
- cls.base_path = config.get("path") or \
- f"{xdg_state_home()}/{default_base}"
+ cls.base_path = config.get("path") or xdg_path
cls.max_bytes = config.get("max_bytes", 50000000)
cls.backup_count = config.get("backup_count", 3)
- cls.level = config.get("level") or LOG.level
+ level = config.get("level") or LOG.level
+ cls.set_level(level)
cls.diagnostic_mode = config.get("diagnostic", False)
@classmethod
@@ -179,38 +182,73 @@ def exception(cls, *args, **kwargs):
cls._get_real_logger().exception(*args, **kwargs)
-def init_service_logger(service_name):
- # this is makes all logs from this service be configured to write to service_name.log file
- # if this is not called in every __main__.py entrypoint logs will be written
- # to a generic OVOS.log file shared across all services
+def _monitor_log_level():
+ _logs_conf = get_logs_config(LOG.name)
+ hax = hash(json.dumps(_logs_conf, sort_keys=True, indent=2))
+ if hax != _monitor_log_level.config_hash:
+ _monitor_log_level.config_hash = hax
+ LOG.init(_logs_conf)
+ LOG.info("updated LOG level")
+
+
+_monitor_log_level.config_hash = None
+
+
+def init_service_logger(service_name: str):
+ """
+ Initialize `LOG` for the specified service
+ @param service_name: Name of service to configure `LOG` for
+ """
+ _logs_conf = get_logs_config(service_name)
+ _monitor_log_level.config_hash = hash(json.dumps(_logs_conf, sort_keys=True,
+ indent=2))
+ LOG.name = service_name
+ LOG.init(_logs_conf) # set up the LOG instance
try:
- from ovos_config.config import read_mycroft_config
- _cfg = read_mycroft_config()
+ from ovos_config import Configuration
+ Configuration.set_config_watcher(_monitor_log_level)
except ImportError:
- LOG.warning("ovos_config not available. Falling back to defaults")
- _cfg = dict()
+ LOG.warning("Can not monitor config LOG level changes")
+def get_logs_config(service_name: Optional[str] = None,
+ _cfg: Optional[dict] = None) -> dict:
+ """
+ Get logging configuration for the specified service
+ @param service_name: Name of service to get logging configuration for
+ @param _cfg: Configuration to parse
+ @return: dict logging configuration for the specified service
+ """
+ if _cfg is None:
+ try:
+ from ovos_config import Configuration
+ _cfg = Configuration()
+ except ImportError:
+ LOG.warning("ovos_config not available. Falling back to defaults")
+ _cfg = {}
+
# First try and get the "logging" section
- log_config = _cfg.get("logging")
+ logging_conf = _cfg.get("logging")
# For compatibility we try to get the "logs" from the root level
# and default to empty which is used in case there is no logging
# section
_logs_conf = _cfg.get("logs") or {}
- if log_config: # We found a logging section
+ if logging_conf: # We found a logging section
# if "logs" is defined in "logging" use that as the default
# where per-service "logs" are not defined
- _logs_conf = log_config.get("logs") or _logs_conf
+ _logs_conf = logging_conf.get("logs") or _logs_conf
# Now get our config by service name
- _cfg = log_config.get(service_name) or log_config
+ if service_name:
+ _cfg = logging_conf.get(service_name) or logging_conf
+ else:
+ # No service name specified, use `logging` configuration
+ _cfg = logging_conf
# and if "logs" is redefined in "logging." use that
_logs_conf = _cfg.get("logs") or _logs_conf
# Grab the log level from whatever section we found, defaulting to INFO
_log_level = _cfg.get("log_level", "INFO")
- # and write it into the "logs" config
_logs_conf["level"] = _log_level
- LOG.name = service_name
- LOG.init(_logs_conf) # setup the LOG instance
+ return _logs_conf
def log_deprecation(log_message: str = "DEPRECATED",
@@ -229,7 +267,6 @@ def log_deprecation(log_message: str = "DEPRECATED",
determination. i.e. an internal exception handling method should log the
first call external to that package
"""
- import inspect
stack = inspect.stack()[1:] # [0] is this method
call_info = "Unknown Origin"
origin_module = func_module
@@ -265,6 +302,7 @@ def deprecated(log_message: str, deprecation_version: str):
@param log_message: Deprecation log message
@param deprecation_version: package version in which deprecation will occur
"""
+
def wrapped(func):
@functools.wraps(func)
def log_wrapper(*args, **kwargs):
@@ -273,6 +311,88 @@ def log_wrapper(*args, **kwargs):
func_module=func.__module__,
deprecation_version=deprecation_version)
return func(*args, **kwargs)
+
return log_wrapper
return wrapped
+
+
+def get_log_path(service: str, directories: Optional[List[str]] = None) \
+ -> Optional[str]:
+ """
+ Get the path to the log directory for a given service.
+ Default behaviour is to check the configured paths for the service.
+ If a list of directories is provided, check that list for the service log
+
+ Args:
+ service: service name
+ directories: (optional) list of directories to check for service
+
+ Returns:
+ path to log directory for service
+ (returned path may be `None` if `directories` is specified)
+ """
+ if directories:
+ for directory in directories:
+ file = os.path.join(directory, f"{service}.log")
+ if os.path.exists(file):
+ return directory
+ return None
+
+ from ovos_utils.xdg_utils import xdg_state_home
+ try:
+ from ovos_config import Configuration
+ from ovos_config.meta import get_xdg_base
+ except ImportError:
+ xdg_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER", "mycroft")
+ return os.path.join(xdg_state_home(), xdg_base)
+
+ config = get_logs_config(service_name=service)
+ # service specific config or default config location
+ path = config.get("path")
+ # default xdg location
+ if not path:
+ path = os.path.join(xdg_state_home(), get_xdg_base())
+
+ return path
+
+
+def get_log_paths(config: Optional[dict] = None) -> Set[str]:
+ """
+ Get all log paths for all service logs
+ Different services may have different log paths
+
+ Returns:
+ set of paths to log directories
+ """
+ paths = set()
+ if not config:
+ try:
+ from ovos_config import Configuration
+ config = Configuration()
+ except ImportError:
+ LOG.warning("ovos_config not available. Falling back to defaults")
+ config = dict()
+
+ for name, service_config in config.get("logging", {}).items():
+ if not isinstance(service_config, dict) or name == "logs":
+ continue
+ if service_config.get("path"):
+ paths.add(service_config.get("path"))
+ paths.add(get_log_path(""))
+
+ return paths
+
+
+def get_available_logs(directories: Optional[List[str]] = None) -> List[str]:
+ """
+ Get a list of all available log files
+ Args:
+ directories: (optional) list of directories to check for service
+
+ Returns:
+ list of log file basenames (i.e. "audio", "skills")
+ """
+ directories = directories or get_log_paths()
+ return [Path(f).stem for path in directories
+ for f in os.listdir(path) if Path(f).suffix == ".log"]
diff --git a/ovos_utils/log_parser.py b/ovos_utils/log_parser.py
new file mode 100644
index 00000000..f7de2f5d
--- /dev/null
+++ b/ovos_utils/log_parser.py
@@ -0,0 +1,669 @@
+import re
+import os
+from datetime import datetime
+from traceback import FrameSummary
+from dataclasses import dataclass
+from typing import Any, Tuple, List, Generator, Dict, Union, Optional
+
+from dateutil.parser import parse
+import rich_click as click
+from rich.console import Console
+from rich.style import Style
+from rich.table import Table
+import pydoc
+from combo_lock import ComboLock
+
+try:
+ from ovos_config import Configuration
+ use24h = Configuration().get("time_format", "full") == "full"
+ date_format = Configuration().get("date_format", "DMY")
+except ImportError:
+ use24h = True
+ date_format = "DMY"
+
+from ovos_utils.log import get_log_path, get_log_paths, get_available_logs
+
+
+TIME_FORMAT = '%Y-%m-%d %H:%M:%S.%f'
+LOGLOCK = ComboLock("ovos_logs_console_script")
+
+
+@dataclass
+class LogLine:
+ timestamp: datetime = None
+ source: str = ""
+ location: str = ""
+ level: str = ""
+ message: str = ""
+
+ def __str__(self):
+ # sytsem messages etc.
+ if not all([self.source, self.location, self.level]):
+ return self.message
+ return f"{self.format_timestamp()} - {self.source} - {self.location} - {self.level} - {self.message}"
+
+ def format_timestamp(self):
+ if self.timestamp:
+ return self.timestamp.strftime(TIME_FORMAT)[:-3]
+ return ""
+
+
+# Traceback frame
+class Frame(FrameSummary):
+ def __init__(self, filename, lineno, name, line):
+ super().__init__(filename, lineno, name, line=line)
+
+ def as_dict(self):
+ return {
+ "location": self.format_location(),
+ "level": "TRACEBACK",
+ "message": self.line
+ }
+
+ def as_logline(self):
+ return LogLine(**self.as_dict())
+
+ def format_location(self):
+ if "/bin/" in self.filename:
+ package = self.filename.split("/bin/")[-1].replace(".py", "")\
+ .replace("-", "_").replace("/", ".")
+ elif "site-packages" not in self.filename and \
+ (pyver := re.search(r"python\d\.\d+[\\/]", self.filename)):
+ package = self.filename.split(pyver.group())[-1].replace(".py", "")\
+ .replace("-", "_").replace("/", ".")
+ else:
+ package = self.filename.split("site-packages/")[-1].replace(".py", "")\
+ .replace("-", "_").replace("/", ".")
+ method = self.name.replace(".py", "").replace("-", "_")
+ return f"{package}:{method}:{self.lineno}"
+
+ def __str__(self):
+ return f' File "{self.filename}", line {self.lineno}, in {self.name}\n {self.line}\n'
+
+
+class Traceback:
+ PATTERN = r'File "(?P[^"]+)", line (?P\d+), in (?P\S+)\n\s*(?P.+)'
+
+ def __init__(self, frames: List[Frame], exception: str, timestamp: datetime = None):
+ self.frames = frames
+ self.exception = exception
+ self._timestamp = timestamp
+
+ @property
+ def exception_location(self):
+ return self.frames[-1].format_location()
+
+ @property
+ def timestamp(self):
+ return self._timestamp
+
+ @timestamp.setter
+ def timestamp(self, value):
+ self._timestamp = value
+
+ def to_loglines(self) -> List[LogLine]:
+
+ lines = [LogLine(timestamp=self.timestamp,
+ location=self.exception_location,
+ level="EXCEPTION",
+ message=self.exception)]
+
+ for frame in self.frames:
+ lines.append(frame.as_logline())
+
+ return lines
+
+ @classmethod
+ def from_list(cls, lines):
+ lines = [line if line.endswith("\n") else line + "\n" for line in lines]
+ multiline = "".join(lines)
+ return cls.from_string(multiline)
+
+ @classmethod
+ def from_string(cls, s):
+ matches = re.findall(cls.PATTERN, s, re.MULTILINE)
+ frames = []
+ for match in matches:
+ data = dict(zip(["filename", "lineno", "name", "line"], match))
+ frames.append(Frame(**data))
+ exception = next(line for line in s.split("\n")[::-1] if line)
+ return cls(frames, exception)
+
+ def __str__(self):
+ multiline = "Traceback (most recent call last):\n"
+ for frame in self.frames:
+ multiline += str(frame)
+ multiline += f"{self.exception}\n"
+ return multiline
+
+
+class OVOSLogParser:
+ LOG_PATTERN = r'(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{1,6}) - (?P