From 73cbafc6436b094c1d509f897fd3430a9e76c02b Mon Sep 17 00:00:00 2001 From: s-martin Date: Sun, 17 Dec 2023 22:27:11 +0000 Subject: [PATCH 01/41] adding pydoc-markdown yml --- pydoc-markdown.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 pydoc-markdown.yml diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml new file mode 100644 index 000000000..8e7911377 --- /dev/null +++ b/pydoc-markdown.yml @@ -0,0 +1,7 @@ +loaders: +- type: python + search_path: [./src] +renderer: + type: markdown + render_toc: true + filename: docstring.md From 17e3cfdb6dff305c5ad8c0bb8f4dfc12c17f1547 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 21 Dec 2023 23:13:19 +0000 Subject: [PATCH 02/41] add lazydocs --- pydoc-markdown.yml | 1 + requirements.txt | 3 +++ run_docstring2markdown.sh | 17 +++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100755 run_docstring2markdown.sh diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 8e7911377..315933fbc 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -5,3 +5,4 @@ renderer: type: markdown render_toc: true filename: docstring.md + render_page_title: true diff --git a/requirements.txt b/requirements.txt index 0f8c8c86d..a276df63a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,3 +32,6 @@ gpiozero flake8>=4.0.0 pytest mock + +# API docs generation +lazydocs diff --git a/run_docstring2markdown.sh b/run_docstring2markdown.sh new file mode 100755 index 000000000..12f207d59 --- /dev/null +++ b/run_docstring2markdown.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Runner script for lazydocs to ensure +# - independent from working directory + +# Change working directory to location of script +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && exit 1) + +# Run lazydocs +lazydocs \ + --output-path="./documentation/developers/docstring" \ + --overview-file="docstring.md" \ + --src-base-url="https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/future3/develop/src/jukebox/" \ + --ignored-modules="ruamel,pulsectl" \ + ./src/jukebox \ No newline at end of file From df6771109dac4aa9c03c99706790c67e653afd2c Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 22 Dec 2023 08:08:15 +0100 Subject: [PATCH 03/41] run lazydocs and pydoc-markdown parallel for testing --- run_docstring2markdown.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/run_docstring2markdown.sh b/run_docstring2markdown.sh index 12f207d59..65dad42a6 100755 --- a/run_docstring2markdown.sh +++ b/run_docstring2markdown.sh @@ -10,8 +10,11 @@ cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && e # Run lazydocs lazydocs \ - --output-path="./documentation/developers/docstring" \ + --output-path="./documentation/developers/docstring-lazydocs" \ --overview-file="docstring.md" \ --src-base-url="https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/future3/develop/src/jukebox/" \ --ignored-modules="ruamel,pulsectl" \ - ./src/jukebox \ No newline at end of file + ./src/jukebox + +# expects pydoc-markdown.yml at working dir +pydoc-markdown \ No newline at end of file From 06e66e05bd2127d6edf258126a94e18f18b2762e Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 22 Dec 2023 08:10:09 +0100 Subject: [PATCH 04/41] fix output path --- pydoc-markdown.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 315933fbc..07c0eee20 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -1,8 +1,8 @@ loaders: - type: python - search_path: [./src] + search_path: [./src/jukebox] renderer: type: markdown render_toc: true - filename: docstring.md + filename: ./documentation/developers/docstring-pydoc-markdown/docstring.md render_page_title: true From 8c56cd6abd2e19cfca82f9ffc3434b67a3ec848a Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 22 Dec 2023 09:26:26 +0100 Subject: [PATCH 05/41] add created docs to source control --- .../developers/docstring-lazydocs/.pages | 4 + .../components.battery_monitor.md | 16 + .../docstring-lazydocs/components.controls.md | 16 + .../docstring-lazydocs/components.md | 16 + .../docstring-lazydocs/components.rfid.md | 19 + .../components.rfid.readerbase.md | 83 + .../components.rpc_command_alias.md | 19 + .../components.synchronisation.md | 16 + .../components.synchronisation.syncutils.md | 75 + .../docstring-lazydocs/docstring.md | 94 + .../docstring-lazydocs/jukebox.NvManager.md | 136 + .../docstring-lazydocs/jukebox.callingback.md | 75 + .../developers/docstring-lazydocs/jukebox.md | 16 + .../jukebox.playlistgenerator.md | 238 + .../docstring-lazydocs/jukebox.plugs.md | 545 ++ .../docstring-lazydocs/jukebox.rpc.md | 16 + .../docstring-lazydocs/jukebox.utils.md | 167 + .../docstring-lazydocs/jukebox.version.md | 49 + .../docstring-lazydocs/misc.inputminus.md | 69 + .../developers/docstring-lazydocs/misc.md | 58 + .../docstring-lazydocs/misc.simplecolors.md | 66 + .../docstring-pydoc-markdown/docstring.md | 5648 +++++++++++++++++ run_docstring2markdown.sh | 3 + 23 files changed, 7444 insertions(+) create mode 100644 documentation/developers/docstring-lazydocs/.pages create mode 100644 documentation/developers/docstring-lazydocs/components.battery_monitor.md create mode 100644 documentation/developers/docstring-lazydocs/components.controls.md create mode 100644 documentation/developers/docstring-lazydocs/components.md create mode 100644 documentation/developers/docstring-lazydocs/components.rfid.md create mode 100644 documentation/developers/docstring-lazydocs/components.rfid.readerbase.md create mode 100644 documentation/developers/docstring-lazydocs/components.rpc_command_alias.md create mode 100644 documentation/developers/docstring-lazydocs/components.synchronisation.md create mode 100644 documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md create mode 100644 documentation/developers/docstring-lazydocs/docstring.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.NvManager.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.callingback.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.plugs.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.rpc.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.utils.md create mode 100644 documentation/developers/docstring-lazydocs/jukebox.version.md create mode 100644 documentation/developers/docstring-lazydocs/misc.inputminus.md create mode 100644 documentation/developers/docstring-lazydocs/misc.md create mode 100644 documentation/developers/docstring-lazydocs/misc.simplecolors.md create mode 100644 documentation/developers/docstring-pydoc-markdown/docstring.md diff --git a/documentation/developers/docstring-lazydocs/.pages b/documentation/developers/docstring-lazydocs/.pages new file mode 100644 index 000000000..d09244c0d --- /dev/null +++ b/documentation/developers/docstring-lazydocs/.pages @@ -0,0 +1,4 @@ +title: API Reference +nav: + - Overview: docstring.md + - ... diff --git a/documentation/developers/docstring-lazydocs/components.battery_monitor.md b/documentation/developers/docstring-lazydocs/components.battery_monitor.md new file mode 100644 index 000000000..f14525810 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.battery_monitor.md @@ -0,0 +1,16 @@ + + + + +# module `components.battery_monitor` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.controls.md b/documentation/developers/docstring-lazydocs/components.controls.md new file mode 100644 index 000000000..36d1049da --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.controls.md @@ -0,0 +1,16 @@ + + + + +# module `components.controls` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.md b/documentation/developers/docstring-lazydocs/components.md new file mode 100644 index 000000000..feb18a2e0 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.md @@ -0,0 +1,16 @@ + + + + +# module `components` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.rfid.md b/documentation/developers/docstring-lazydocs/components.rfid.md new file mode 100644 index 000000000..bbf9477e4 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.rfid.md @@ -0,0 +1,19 @@ + + + + +# module `components.rfid` + + + + +**Global Variables** +--------------- +- **readerbase** + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md b/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md new file mode 100644 index 000000000..12be3db8a --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md @@ -0,0 +1,83 @@ + + + + +# module `components.rfid.readerbase` + + + + + + +--- + + + +## class `ReaderBaseClass` +Abstract Base Class for all Reader Classes to ensure common API + +Look at template_new_reader.py for documentation how to integrate a new RFID reader + + + +### method `__init__` + +```python +__init__(reader_cfg_key: str, description: str, logger: Logger) +``` + + + + + + + + +--- + + + +### method `cleanup` + +```python +cleanup() +``` + + + + + +--- + + + +### method `read_card` + +```python +read_card() +``` + + + + + +--- + + + +### method `stop` + +```python +stop() +``` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md b/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md new file mode 100644 index 000000000..cd5b4b889 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md @@ -0,0 +1,19 @@ + + + + +# module `components.rpc_command_alias` +This file provides definitions for RPC command aliases + +See :ref:`userguide/rpc_commands` + +**Global Variables** +--------------- +- **cmd_alias_definitions** + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.synchronisation.md b/documentation/developers/docstring-lazydocs/components.synchronisation.md new file mode 100644 index 000000000..b7fff4edc --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.synchronisation.md @@ -0,0 +1,16 @@ + + + + +# module `components.synchronisation` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md b/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md new file mode 100644 index 000000000..fd6f37303 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md @@ -0,0 +1,75 @@ + + + + +# module `components.synchronisation.syncutils` + + + + + +--- + + + +## function `clean_foldername` + +```python +clean_foldername(lib_path: str, folder: str) → str +``` + + + + + + +--- + + + +## function `ensure_trailing_slash` + +```python +ensure_trailing_slash(path: str) → str +``` + + + + + + +--- + + + +## function `remove_trailing_slash` + +```python +remove_trailing_slash(path: str) → str +``` + + + + + + +--- + + + +## function `remove_leading_slash` + +```python +remove_leading_slash(path: str) → str +``` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/docstring.md b/documentation/developers/docstring-lazydocs/docstring.md new file mode 100644 index 000000000..2f7d2355b --- /dev/null +++ b/documentation/developers/docstring-lazydocs/docstring.md @@ -0,0 +1,94 @@ + + +# API Overview + +## Modules + +- [`components`](./components.md#module-components) +- [`components.battery_monitor`](./components.battery_monitor.md#module-componentsbattery_monitor) +- [`components.controls`](./components.controls.md#module-componentscontrols) +- [`components.rfid`](./components.rfid.md#module-componentsrfid) +- [`components.rfid.readerbase`](./components.rfid.readerbase.md#module-componentsrfidreaderbase) +- [`components.rpc_command_alias`](./components.rpc_command_alias.md#module-componentsrpc_command_alias): This file provides definitions for RPC command aliases +- [`components.synchronisation`](./components.synchronisation.md#module-componentssynchronisation) +- [`components.synchronisation.syncutils`](./components.synchronisation.syncutils.md#module-componentssynchronisationsyncutils) +- [`jukebox`](./jukebox.md#module-jukebox) +- [`jukebox.NvManager`](./jukebox.NvManager.md#module-jukeboxnvmanager) +- [`jukebox.callingback`](./jukebox.callingback.md#module-jukeboxcallingback): Provides a generic callback handler +- [`jukebox.playlistgenerator`](./jukebox.playlistgenerator.md#module-jukeboxplaylistgenerator): Playlists are build from directory content in the following way: +- [`jukebox.plugs`](./jukebox.plugs.md#module-jukeboxplugs): A plugin package with some special functionality +- [`jukebox.rpc`](./jukebox.rpc.md#module-jukeboxrpc) +- [`jukebox.utils`](./jukebox.utils.md#module-jukeboxutils): Common utility functions +- [`jukebox.version`](./jukebox.version.md#module-jukeboxversion) +- [`misc`](./misc.md#module-misc) +- [`misc.inputminus`](./misc.inputminus.md#module-miscinputminus): Zero 3rd-party dependency module for user prompting +- [`misc.simplecolors`](./misc.simplecolors.md#module-miscsimplecolors): Zero 3rd-party dependency module to add colors to unix terminal output + +## Classes + +- [`readerbase.ReaderBaseClass`](./components.rfid.readerbase.md#class-readerbaseclass): Abstract Base Class for all Reader Classes to ensure common API +- [`NvManager.nv_dict`](./jukebox.NvManager.md#class-nv_dict) +- [`NvManager.nv_manager`](./jukebox.NvManager.md#class-nv_manager) +- [`callingback.CallbackHandler`](./jukebox.callingback.md#class-callbackhandler): Generic Callback Handler to collect callbacks functions through :func:`register` and execute them +- [`playlistgenerator.PlaylistCollector`](./jukebox.playlistgenerator.md#class-playlistcollector): Build a playlist from directory(s) +- [`playlistgenerator.PlaylistEntry`](./jukebox.playlistgenerator.md#class-playlistentry) +- [`plugs.PluginPackageClass`](./jukebox.plugs.md#class-pluginpackageclass): A local data class for holding all information about a loaded plugin package +- [`simplecolors.Colors`](./misc.simplecolors.md#class-colors): Container class for all the colors as constants + +## Functions + +- [`syncutils.clean_foldername`](./components.synchronisation.syncutils.md#function-clean_foldername) +- [`syncutils.ensure_trailing_slash`](./components.synchronisation.syncutils.md#function-ensure_trailing_slash) +- [`syncutils.remove_leading_slash`](./components.synchronisation.syncutils.md#function-remove_leading_slash) +- [`syncutils.remove_trailing_slash`](./components.synchronisation.syncutils.md#function-remove_trailing_slash) +- [`playlistgenerator.decode_livestream`](./jukebox.playlistgenerator.md#function-decode_livestream) +- [`playlistgenerator.decode_m3u`](./jukebox.playlistgenerator.md#function-decode_m3u) +- [`playlistgenerator.decode_musicfile`](./jukebox.playlistgenerator.md#function-decode_musicfile) +- [`playlistgenerator.decode_podcast`](./jukebox.playlistgenerator.md#function-decode_podcast) +- [`playlistgenerator.decode_podcast_core`](./jukebox.playlistgenerator.md#function-decode_podcast_core) +- [`plugs.atexit`](./jukebox.plugs.md#function-atexit): Decorator for functions that shall be called by the plugs package directly after at exit of program. +- [`plugs.call`](./jukebox.plugs.md#function-call): Call a function/method from the loaded plugins +- [`plugs.call_ignore_errors`](./jukebox.plugs.md#function-call_ignore_errors): Call a function/method from the loaded plugins ignoring all raised Exceptions. +- [`plugs.close_down`](./jukebox.plugs.md#function-close_down): Calls all functions registered with @atexit from all loaded modules in reverse order of module load order +- [`plugs.delete`](./jukebox.plugs.md#function-delete): Delete a plugin object from the registered plugs callables +- [`plugs.dereference`](./jukebox.plugs.md#function-dereference) +- [`plugs.dump_plugins`](./jukebox.plugs.md#function-dump_plugins): Write a human readable summary of all plugin callables to stream +- [`plugs.exists`](./jukebox.plugs.md#function-exists): Check if an object is registered within the plugs package +- [`plugs.finalize`](./jukebox.plugs.md#function-finalize): Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded +- [`plugs.generate_help_rst`](./jukebox.plugs.md#function-generate_help_rst): Write a reference of all plugin callables in Restructured Text format +- [`plugs.get`](./jukebox.plugs.md#function-get): Get a plugs-package registered object +- [`plugs.get_all_failed_packages`](./jukebox.plugs.md#function-get_all_failed_packages): Report those packages that did not load error free +- [`plugs.get_all_loaded_packages`](./jukebox.plugs.md#function-get_all_loaded_packages): Report a short summary of all loaded packages +- [`plugs.initialize`](./jukebox.plugs.md#function-initialize): Decorator for functions that shall be called by the plugs package directly after the module is loaded +- [`plugs.load`](./jukebox.plugs.md#function-load): Loads a python package as plugin package +- [`plugs.load_all_finalize`](./jukebox.plugs.md#function-load_all_finalize): Calls all functions registered with @finalize from all loaded modules in the order they were loaded +- [`plugs.load_all_named`](./jukebox.plugs.md#function-load_all_named): Load all packages in packages_named with mapped names +- [`plugs.load_all_unnamed`](./jukebox.plugs.md#function-load_all_unnamed): Load all packages in packages_unnamed with default names +- [`plugs.loaded_as`](./jukebox.plugs.md#function-loaded_as): Return the plugin name a python module is loaded as +- [`plugs.register`](./jukebox.plugs.md#function-register): A generic decorator / run-time function to register plugin module callables +- [`plugs.summarize`](./jukebox.plugs.md#function-summarize): Create a reference summary of all plugin callables in dictionary format +- [`plugs.tag`](./jukebox.plugs.md#function-tag): Method decorator for tagging a method as callable through the plugs interface +- [`utils.bind_rpc_command`](./jukebox.utils.md#function-bind_rpc_command): Decode an RPC command configuration entry and bind it to a function +- [`utils.decode_and_call_rpc_command`](./jukebox.utils.md#function-decode_and_call_rpc_command): Convenience function combining decode_rpc_command and plugs.call_ignore_errors +- [`utils.decode_rpc_call`](./jukebox.utils.md#function-decode_rpc_call): Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. +- [`utils.decode_rpc_command`](./jukebox.utils.md#function-decode_rpc_command): Decode an RPC Command from a config entry. +- [`utils.generate_cmd_alias_reference`](./jukebox.utils.md#function-generate_cmd_alias_reference): Write a reference of all rpc command aliases in text format +- [`utils.generate_cmd_alias_rst`](./jukebox.utils.md#function-generate_cmd_alias_rst): Write a reference of all rpc command aliases in Restructured Text format +- [`utils.get_git_state`](./jukebox.utils.md#function-get_git_state): Return git state information for the current branch +- [`utils.indent`](./jukebox.utils.md#function-indent) +- [`utils.rpc_call_to_str`](./jukebox.utils.md#function-rpc_call_to_str): Return a readable string of an RPC call config +- [`version.version`](./jukebox.version.md#function-version): Return the Jukebox version as a string +- [`version.version_info`](./jukebox.version.md#function-version_info): Return the Jukebox version as a tuple of three numbers +- [`misc.flatten`](./misc.md#function-flatten): Flatten all levels of hierarchy in nested iterables +- [`misc.getattr_hierarchical`](./misc.md#function-getattr_hierarchical): Like the builtin getattr, but descends though the hierarchy levels +- [`misc.recursive_chmod`](./misc.md#function-recursive_chmod): Recursively change folder and file permissions +- [`inputminus.input_int`](./misc.inputminus.md#function-input_int): Request an integer input from user +- [`inputminus.input_yesno`](./misc.inputminus.md#function-input_yesno): Request a yes / no choice from user +- [`inputminus.msg_highlight`](./misc.inputminus.md#function-msg_highlight) +- [`simplecolors.print`](./misc.simplecolors.md#function-print): Drop-in replacement for print with color choice and auto color reset for convenience +- [`simplecolors.resolve`](./misc.simplecolors.md#function-resolve): Resolve a color name into the respective color constant + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.NvManager.md b/documentation/developers/docstring-lazydocs/jukebox.NvManager.md new file mode 100644 index 000000000..e611fd91e --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.NvManager.md @@ -0,0 +1,136 @@ + + + + +# module `jukebox.NvManager` + + + + + + +--- + + + +## class `nv_manager` + + + + + + +### method `__init__` + +```python +__init__() +``` + + + + + + + + +--- + + + +### method `load` + +```python +load(default_filename, type=None) +``` + + + + + +--- + + + +### method `save_all` + +```python +save_all() +``` + + + + + + +--- + + + +## class `nv_dict` + + + + + + +### method `__init__` + +```python +__init__(default_filename=None) +``` + + + + + + + + +--- + + + +### method `hash` + +```python +hash() +``` + + + + + +--- + + + +### method `init_from_json` + +```python +init_from_json(path=None, merge=False) +``` + + + + + +--- + + + +### method `save_to_json` + +```python +save_to_json(filename=None) +``` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.callingback.md b/documentation/developers/docstring-lazydocs/jukebox.callingback.md new file mode 100644 index 000000000..63ca7f9c9 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.callingback.md @@ -0,0 +1,75 @@ + + + + +# module `jukebox.callingback` +Provides a generic callback handler + + + +--- + + + +## class `CallbackHandler` +Generic Callback Handler to collect callbacks functions through :func:`register` and execute them with :func:`run_callbacks` + +A lock is used to sequence registering of new functions and running callbacks. + +:param name: A name of this handler for usage in log messages :param logger: The logger instance to use for logging :param context: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created + + + +### method `__init__` + +```python +__init__(name: str, logger: Logger, context=None) +``` + + + + + + +--- + +#### property has_callbacks + +:data:`True` if there are any registered callbacks. Read-only property + + + +--- + + + +### method `register` + +```python +register(func: Optional[Callable[, NoneType]]) +``` + +Register a new function to be executed when the callback event happens + +:param func: The function to register. If set to :data:`None`, this register request is silently ignored. + +--- + + + +### method `run_callbacks` + +```python +run_callbacks(*args, **kwargs) +``` + +Run all registered callbacks. + +*ALL* exceptions from callback functions will be caught and logged only. Exceptions are not raised upwards! + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.md b/documentation/developers/docstring-lazydocs/jukebox.md new file mode 100644 index 000000000..d97777d88 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.md @@ -0,0 +1,16 @@ + + + + +# module `jukebox` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md b/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md new file mode 100644 index 000000000..d30b6bb2d --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md @@ -0,0 +1,238 @@ + + + + +# module `jukebox.playlistgenerator` +Playlists are build from directory content in the following way: a directory is parsed and files are added to the playlist in the following way + +1. files are added in alphabetic order 2. files ending with ``*livestream.txt`` are unpacked and the containing URL(s) are added verbatim to the playlist 3. files ending with ``*podcast.txt`` are unpacked and the containing Podcast URL(s) are expanded and added to the playlist 4. files ending with ``*.m3u`` are treated as folder playlist. Regular folder processing is suspended and the playlist is build solely from the ``*.m3u`` content. Only the alphabetically first ``*.m3u`` is processed. URLs are added verbatim to the playlist except for ``*.xml`` and ``*.podcast`` URLS, which are expanded first + +An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. + +.. code-block:: bash + + 01-livestream.txt 02-livestream.txt music.mp3 podcast.txt + +All files are treated as music files and are added to the playlist, except those: + +* starting with ``.``, * not having a file ending, i.e. do not contain a ``.``, * ending with ``.txt``, * ending with ``.m3u``, * ending with one of the excluded file endings in :attr:`PlaylistCollector._exclude_endings` + +In recursive mode, the playlist is generated by concatenating all sub-folder playlists. Sub-folders are parsed in alphabetic order. Symbolic links are being followed. The above rules are enforced on a per-folder bases. This means, one ``*.m3u`` file per sub-folder is processed (if present). + +In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. + +**Global Variables** +--------------- +- **TYPE_FILE** +- **TYPE_DIR** +- **TYPE_STREAM** +- **TYPE_PODCAST** +- **TYPE_DECODE** + +--- + + + +## function `decode_podcast_core` + +```python +decode_podcast_core(url, playlist) +``` + + + + + + +--- + + + +## function `decode_podcast` + +```python +decode_podcast(filename: str, path, playlist) +``` + + + + + + +--- + + + +## function `decode_livestream` + +```python +decode_livestream(filename: DirEntry, path, playlist) +``` + + + + + + +--- + + + +## function `decode_musicfile` + +```python +decode_musicfile(filename: DirEntry, path, playlist) +``` + + + + + + +--- + + + +## function `decode_m3u` + +```python +decode_m3u(filename: DirEntry, path, playlist: List[PlaylistEntry]) +``` + + + + + + +--- + + + +## class `PlaylistEntry` + + + + + + +### method `__init__` + +```python +__init__(filetype: int, name: str, path: str) +``` + + + + + + +--- + +#### property filetype + + + + + +--- + +#### property name + + + + + +--- + +#### property path + + + + + + + + +--- + + + +## class `PlaylistCollector` +Build a playlist from directory(s) + +This class is intended to be used with an absolute path to the music library: +``` + + plc = PlaylistCollector('/home/chris/music') plc.parse('Traumfaenger') print(f"res = {plc}") + +But it can also be used with relative paths from current working directory: +``` + + plc = PlaylistCollector('.') plc.parse('../../../../music/Traumfaenger') print(f"res = {plc}") + +``` +The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. + + + +### method `__init__` + +```python +__init__(music_library_base_path='/') +``` + +Initialize the playlist generator with music_library_base_path + +:param music_library_base_path: Base path the the music library. This is used to locate the file in the disk but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir + + + + +--- + + + +### method `get_directory_content` + +```python +get_directory_content(path='.') +``` + +Parse the folder ``path`` and create a content list. Depth is always the current level + +:param path: Path to folder **relative** to ``music_library_base_path`` :return: [ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] where type is one of :attr:`TYPE_DECODE` + +--- + + + +### method `parse` + +```python +parse(path='.', recursive=False) +``` + +Parse the folder ``path`` and create a playlist from it's content + +:param path: Path to folder **relative** to ``music_library_base_path`` :param recursive: Parse folder recursivley, or stay in top-level folder + +--- + + + +### classmethod `set_exclusion_endings` + +```python +set_exclusion_endings(endings: List[str]) +``` + +Set the class-wide file ending exclusion list + +See :attr:`PlaylistCollector._exclude_endings` + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.plugs.md b/documentation/developers/docstring-lazydocs/jukebox.plugs.md new file mode 100644 index 000000000..b7cf01966 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.plugs.md @@ -0,0 +1,545 @@ + + + + +# module `jukebox.plugs` +A plugin package with some special functionality + +Plugins packages are python packages that are dynamically loaded. From these packages only a subset of objects is exposed through the plugs.call interface. The python packages can use decorators or dynamic function call to register (callable) objects. + +The python package name may be different from the name the package is registered under in plugs. This allows to load different python packages for a specific feature based on a configuration file. Note: Python package are still loaded as regular python packages and can be accessed by normal means + +If you want to provide additional functionality to the same feature (probably even for run-time switching) you can implement a Factory Pattern using this package. Take a look at volume.py as an example. + +**Example:** Decorate a function for auto-registering under it's own name: +``` + + import jukebox.plugs as plugs @plugs.register def func1(param): pass + +**Example:** Decorate a function for auto-registering under a new name: +``` + + @plugs.register(name='better_name') def func2(param): pass + +**Example:** Register a function during run-time under it's own name: +``` + + def func3(param): pass plugs.register(func3) + +**Example:** Register a function during run-time under a new name: +``` + + def func4(param): pass plugs.register(func4, name='other_name', package='other_package') + +``` +**Example:** Decorate a class for auto registering during initialization, including all methods (see _register_class for more info): +``` + + @plugs.register(auto_tag=True) class MyClass1: pass + +**Example:** Register a class instance, from which only report is a callable method through the plugs interface: +``` + + class MyClass2: @plugs.tag def report(self): pass myinst2 = MyClass2() plugin.register(myinst2, name='myinst2') + +``` +Naming convention: + +package 1. Either a python package 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) + +plugin 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) 2. The string name to above object + +name The string name of the plugin object for registration + +method 1. In case the object is a class instance a bound method to call from the class instance 2. The string name to above object + +**Global Variables** +--------------- +- **ALLOW_DIRECT_IMPORTS** + +--- + + + +## function `register` + +```python +register( + plugin: Optional[Callable] = None, + name: Optional[str] = None, + package: Optional[str] = None, + replace: bool = False, + auto_tag: bool = False +) → Callable +``` + +A generic decorator / run-time function to register plugin module callables + +The functions comes in five distinct signatures for 5 use cases: + +1. ``@plugs.register``: decorator for a class w/o any arguments 2. ``@plugs.register``: decorator for a function w/o any arguments 3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments 4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments 5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of + + * function * bound method * class instance + +For more documentation see the functions + + * :func:`_register_obj` * :func:`_register_class` + +See the examples in Module :mod:`plugs` how to use this decorator / function + +:param plugin: :param name: :param package: :param replace: :param auto_tag: :return: + + +--- + + + +## function `tag` + +```python +tag(func: Callable) → Callable +``` + +Method decorator for tagging a method as callable through the plugs interface + +Note that the instantiated class must still be registered as plugin object (either with the class decorator or dynamically) + +:param func: function to decorate :return: the function + + +--- + + + +## function `initialize` + +```python +initialize(func: Callable) → Callable +``` + +Decorator for functions that shall be called by the plugs package directly after the module is loaded + +:param func: Function to decorate :return: The function itself + + +--- + + + +## function `finalize` + +```python +finalize(func: Callable) → Callable +``` + +Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded + +:param func: Function to decorate :return: The function itself + + +--- + + + +## function `atexit` + +```python +atexit(func: Callable[[int], Any]) → Callable[[int], Any] +``` + +Decorator for functions that shall be called by the plugs package directly after at exit of program. + +.. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your shutdown handler. + +The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) It is intended for passing down the signal number that initiated the program termination + +:param func: Function to decorate :return: The function itself + + +--- + + + +## function `load` + +```python +load(package: str, load_as: Optional[str] = None, prefix: Optional[str] = None) +``` + +Loads a python package as plugin package + +Executes a regular python package load. That means a potentially existing __init__.py is executed. Decorator @register can by used to register functions / classes / class istances as plugin callable Decorator @initializer can be used to tag functions that shall be called after package loading Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded Instead of using @initializer, you may of course use __init__.py + +Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under which they are loaded as plugin package also. + +:param package: Python package to load as plugin package :param load_as: Plugin package registration name. If None the name is the python's package simple name :param prefix: Prefix to python package to create fully qualified name. This is used only to locate the python package and ignored otherwise. Useful if all the plugin module are in a dedicated folder :return: + + +--- + + + +## function `load_all_named` + +```python +load_all_named( + packages_named: Mapping[str, str], + prefix: Optional[str] = None, + ignore_errors=False +) +``` + +Load all packages in packages_named with mapped names + +:param packages_named: Dict[load_as, package] + + +--- + + + +## function `load_all_unnamed` + +```python +load_all_unnamed( + packages_unnamed: Iterable[str], + prefix: Optional[str] = None, + ignore_errors=False +) +``` + +Load all packages in packages_unnamed with default names + + +--- + + + +## function `load_all_finalize` + +```python +load_all_finalize(ignore_errors=False) +``` + +Calls all functions registered with @finalize from all loaded modules in the order they were loaded + +This must be executed after the last plugin package is loaded + + +--- + + + +## function `close_down` + +```python +close_down(**kwargs) → Any +``` + +Calls all functions registered with @atexit from all loaded modules in reverse order of module load order + +Modules are processed in reverse order. Several at-exit tagged functions of a single module are processed in the order of registration. + +Errors raised in functions are suppressed to ensure all plugins are processed :return: + + +--- + + + +## function `dereference` + +```python +dereference( + package: str, + plugin: str, + method: Optional[str] = None, + args=None, + kwargs=None +) +``` + + + + + + +--- + + + +## function `call` + +```python +call( + package: str, + plugin: str, + method: Optional[str] = None, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None +) → Any +``` + +Call a function/method from the loaded plugins + +If a plugin is a function or a callable instance of a class, this is equivalent to + +``package.plugin(*args, **kwargs)`` + +If plugin is a class instance from which a method is called, this is equivalent to the followig. Also remember, that method must have the attribute ``plugin_callable = True`` + +``package.plugin.method(*args, **kwargs)`` + +Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. + +.. note: +``` There is no logger in this function as they all belong up-level where the exceptions are handled. If you want logger messages instead of exceptions, use :func:`call_ignore_errors` + +``` +:param package: Name of the plugin package in which to look for function/class instance :param plugin: Function name or instance name of a class :param method: Method name when accessing a class instance' method. Leave at *None* if unneeded. :param as_thread: Run the callable in separate daemon thread. There is no return value from the callable in this case! The return value is the thread object. Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. All threads are started as daemon threads with terminate upon main program termination. There is not stop-thread mechanism. This is intended for short lived threads. :param thread_name: Name of the thread :param args: Arguments passed to callable :param kwargs: Keyword arguments passed to callable :return: The return value from the called function, or, if started as thread the thread object + + +--- + + + +## function `call_ignore_errors` + +```python +call_ignore_errors( + package: str, + plugin: str, + method: Optional[str] = None, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None +) → Any +``` + +Call a function/method from the loaded plugins ignoring all raised Exceptions. + +Errors get logged. + +See :func:`call` for parameter documentation. + + +--- + + + +## function `exists` + +```python +exists( + package: str, + plugin: Optional[str] = None, + method: Optional[str] = None +) → bool +``` + +Check if an object is registered within the plugs package + + +--- + + + +## function `get` + +```python +get( + package: str, + plugin: Optional[str] = None, + method: Optional[str] = None +) → Any +``` + +Get a plugs-package registered object + +The return object depends on the number of parameters + +* 1 argument: Get the python module reference for the plugs *package* * 2 arguments: Get the plugin reference for the plugs *package.plugin* * 3 arguments: Get the plugin reference for the plugs *package.plugin.method* + + +--- + + + +## function `loaded_as` + +```python +loaded_as(module_name: str) → str +``` + +Return the plugin name a python module is loaded as + + +--- + + + +## function `delete` + +```python +delete(package: str, plugin: Optional[str] = None, ignore_errors=False) +``` + +Delete a plugin object from the registered plugs callables + +Note: This does not 'unload' the python module. It merely makes it un-callable via plugs! + + +--- + + + +## function `dump_plugins` + +```python +dump_plugins(stream) +``` + +Write a human readable summary of all plugin callables to stream + + +--- + + + +## function `summarize` + +```python +summarize() +``` + +Create a reference summary of all plugin callables in dictionary format + + +--- + + + +## function `generate_help_rst` + +```python +generate_help_rst(stream) +``` + +Write a reference of all plugin callables in Restructured Text format + + +--- + + + +## function `get_all_loaded_packages` + +```python +get_all_loaded_packages() → Dict[str, str] +``` + +Report a short summary of all loaded packages + +:return: Dictionary of the form `{loaded_as: loaded_from, ...}` + + +--- + + + +## function `get_all_failed_packages` + +```python +get_all_failed_packages() → Dict[str, str] +``` + +Report those packages that did not load error free + +.. note:: Package could fail to load + + 1. altogether: these package are not registered 2. partially: during initializer, finalizer functions: The package is loaded, but the function did not execute error-free + + Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED + +:return: Dictionary of the form `{loaded_as: loaded_from, ...}` + + +--- + + + +## class `PluginPackageClass` +A local data class for holding all information about a loaded plugin package + + + +### method `__init__` + +```python +__init__(loaded_from: str) +``` + + + + + + +--- + +#### property atexit + + + + + +--- + +#### property finalizer + + + + + +--- + +#### property initializer + + + + + +--- + +#### property loaded_from + + + + + +--- + +#### property module + + + + + +--- + +#### property plugins + + + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.rpc.md b/documentation/developers/docstring-lazydocs/jukebox.rpc.md new file mode 100644 index 000000000..90e0bca43 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.rpc.md @@ -0,0 +1,16 @@ + + + + +# module `jukebox.rpc` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.utils.md b/documentation/developers/docstring-lazydocs/jukebox.utils.md new file mode 100644 index 000000000..b46361e7e --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.utils.md @@ -0,0 +1,167 @@ + + + + +# module `jukebox.utils` +Common utility functions + +**Global Variables** +--------------- +- **cmd_alias_definitions** + +--- + + + +## function `decode_rpc_call` + +```python +decode_rpc_call(cfg_rpc_call: Dict) → Optional[Dict] +``` + +Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. + +.. important: Leaves all other parameters in cfg_action untouched or later downstream processing! + +:param cfg_rpc_call: RPC command as configuration entry :return: A fully populated deep copy of cfg_rpc_call + + +--- + + + +## function `decode_rpc_command` + +```python +decode_rpc_command( + cfg_rpc_cmd: Dict, + logger: Logger = +) → Optional[Dict] +``` + +Decode an RPC Command from a config entry. + +This means + + * Decode RPC command alias (if present) * Ensure all RPC call parameters have valid default values + +If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call which emits a misuse warning when called If an explicitly specified this is not done. However, it is ensured that the returned dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling for non-existing RPC commands and we get a clearer error message. + +:param cfg_rpc_cmd: RPC command as configuration entry :param logger: The logger to use :return: A decoded, fully populated deep copy of cfg_rpc_cmd + + +--- + + + +## function `decode_and_call_rpc_command` + +```python +decode_and_call_rpc_command( + rpc_cmd: Dict, + logger: Logger = +) +``` + +Convenience function combining decode_rpc_command and plugs.call_ignore_errors + + +--- + + + +## function `bind_rpc_command` + +```python +bind_rpc_command( + cfg_rpc_cmd: Dict, + dereference=False, + logger: Logger = +) +``` + +Decode an RPC command configuration entry and bind it to a function + +:param dereference: Dereference even the call to plugs.call(...) + + #. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with all checks applied at bind time #. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with all checks applied at bind time. + + Setting deference to True, circumvents the dynamic nature of the plugins: the function to call must exist at bind time and cannot change. If False, the function to call must only exist at call time. This can be important during the initialization where package ordering and initialization means that not all classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls is circumvented. Use with care! + +:return: Callable function w/o parameters which directly runs the RPC command using plugs.call_ignore_errors + + +--- + + + +## function `rpc_call_to_str` + +```python +rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) → str +``` + +Return a readable string of an RPC call config + +:param cfg_rpc_call: RPC call configuration entry :param with_args: Return string shall include the arguments of the function + + +--- + + + +## function `indent` + +```python +indent(doc, spaces=4) +``` + + + + + + +--- + + + +## function `generate_cmd_alias_rst` + +```python +generate_cmd_alias_rst(stream) +``` + +Write a reference of all rpc command aliases in Restructured Text format + + +--- + + + +## function `generate_cmd_alias_reference` + +```python +generate_cmd_alias_reference(stream) +``` + +Write a reference of all rpc command aliases in text format + + +--- + + + +## function `get_git_state` + +```python +get_git_state() +``` + +Return git state information for the current branch + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.version.md b/documentation/developers/docstring-lazydocs/jukebox.version.md new file mode 100644 index 000000000..7cf86940f --- /dev/null +++ b/documentation/developers/docstring-lazydocs/jukebox.version.md @@ -0,0 +1,49 @@ + + + + +# module `jukebox.version` + + + + +**Global Variables** +--------------- +- **VERSION_MAJOR** +- **VERSION_MINOR** +- **VERSION_PATCH** +- **VERSION_EXTRA** + +--- + + + +## function `version` + +```python +version() +``` + +Return the Jukebox version as a string + + +--- + + + +## function `version_info` + +```python +version_info() +``` + +Return the Jukebox version as a tuple of three numbers + +If this is a development version, an identifier string will be appended after the third integer. + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/misc.inputminus.md b/documentation/developers/docstring-lazydocs/misc.inputminus.md new file mode 100644 index 000000000..020082bb3 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/misc.inputminus.md @@ -0,0 +1,69 @@ + + + + +# module `misc.inputminus` +Zero 3rd-party dependency module for user prompting + +Yes, there are modules out there to do the same and they have more features. However, this is low-complexity and has zero dependencies + + +--- + + + +## function `input_int` + +```python +input_int( + prompt, + blank=None, + min=None, + max=None, + prompt_color=None, + prompt_hint=False +) → int +``` + +Request an integer input from user + +:param prompt: The prompt to display :param blank: Value to return when user just hits enter. Leave at None, if blank is invalid :param min: Minimum valid integer value (None disables this check) :param max: Maximum valid integer value (None disables this check) :param prompt_color: Color of the prompt. Color will be reset at end of prompt :param prompt_hint: Append a 'hint' with [min...max, default=xx] to end of prompt :return: integer value read from user input + + +--- + + + +## function `input_yesno` + +```python +input_yesno(prompt, blank=None, prompt_color=None, prompt_hint=False) → bool +``` + +Request a yes / no choice from user + +Accepts multiple input for true/false and is case insensitive + +:param prompt: The prompt to display :param blank: Value to return when user just hits enter. Leave at None, if blank is invalid :param prompt_color: Color of the prompt. Color will be reset at end of prompt :param prompt_hint: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized :return: boolean value read from user input + + +--- + + + +## function `msg_highlight` + +```python +msg_highlight(msg, color='\x1b[94m', deliminator_length=79) +``` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/misc.md b/documentation/developers/docstring-lazydocs/misc.md new file mode 100644 index 000000000..ea8406996 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/misc.md @@ -0,0 +1,58 @@ + + + + +# module `misc` + + + + + +--- + + + +## function `recursive_chmod` + +```python +recursive_chmod(path, mode_files, mode_dirs) +``` + +Recursively change folder and file permissions + +mode_files/mode dirs can be given in octal notation e.g. 0o777 flags from the stats module. + +Reference: https://docs.python.org/3/library/os.html#os.chmod + + +--- + + + +## function `flatten` + +```python +flatten(iterable) +``` + +Flatten all levels of hierarchy in nested iterables + + +--- + + + +## function `getattr_hierarchical` + +```python +getattr_hierarchical(obj: Any, name: str) → Any +``` + +Like the builtin getattr, but descends though the hierarchy levels + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/misc.simplecolors.md b/documentation/developers/docstring-lazydocs/misc.simplecolors.md new file mode 100644 index 000000000..2bc508fa4 --- /dev/null +++ b/documentation/developers/docstring-lazydocs/misc.simplecolors.md @@ -0,0 +1,66 @@ + + + + +# module `misc.simplecolors` +Zero 3rd-party dependency module to add colors to unix terminal output + +Yes, there are modules out there to do the same and they have more features. However, this is low-complexity and has zero dependencies + +**Global Variables** +--------------- +- **COLORS** + +--- + + + +## function `resolve` + +```python +resolve(color_name: str) +``` + +Resolve a color name into the respective color constant + +:param color_name: Name of the color :return: color constant + + +--- + + + +## function `print` + +```python +print( + color: Colors, + *values, + sep=' ', + end='\n', + file=<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>, + flush=False +) +``` + +Drop-in replacement for print with color choice and auto color reset for convenience + +Use just as a regular print function, but with first parameter as color + + +--- + + + +## class `Colors` +Container class for all the colors as constants + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-pydoc-markdown/docstring.md b/documentation/developers/docstring-pydoc-markdown/docstring.md new file mode 100644 index 000000000..b2184d060 --- /dev/null +++ b/documentation/developers/docstring-pydoc-markdown/docstring.md @@ -0,0 +1,5648 @@ +# None + +## Table of Contents + +* [run\_jukebox](#run_jukebox) +* [\_\_init\_\_](#__init__) +* [run\_register\_rfid\_reader](#run_register_rfid_reader) +* [run\_rpc\_tool](#run_rpc_tool) + * [get\_common\_beginning](#run_rpc_tool.get_common_beginning) +* [run\_configure\_audio](#run_configure_audio) +* [run\_publicity\_sniffer](#run_publicity_sniffer) +* [misc](#misc) + * [recursive\_chmod](#misc.recursive_chmod) + * [flatten](#misc.flatten) + * [getattr\_hierarchical](#misc.getattr_hierarchical) +* [misc.inputminus](#misc.inputminus) + * [input\_int](#misc.inputminus.input_int) + * [input\_yesno](#misc.inputminus.input_yesno) +* [misc.loggingext](#misc.loggingext) + * [ColorFilter](#misc.loggingext.ColorFilter) + * [\_\_init\_\_](#misc.loggingext.ColorFilter.__init__) + * [PubStream](#misc.loggingext.PubStream) + * [PubStreamHandler](#misc.loggingext.PubStreamHandler) +* [misc.simplecolors](#misc.simplecolors) + * [Colors](#misc.simplecolors.Colors) + * [resolve](#misc.simplecolors.resolve) + * [print](#misc.simplecolors.print) +* [components](#components) +* [components.playermpd.playcontentcallback](#components.playermpd.playcontentcallback) + * [PlayContentCallbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks) + * [register](#components.playermpd.playcontentcallback.PlayContentCallbacks.register) + * [run\_callbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks.run_callbacks) +* [components.playermpd](#components.playermpd) + * [PlayerMPD](#components.playermpd.PlayerMPD) + * [mpd\_retry\_with\_mutex](#components.playermpd.PlayerMPD.mpd_retry_with_mutex) + * [pause](#components.playermpd.PlayerMPD.pause) + * [next](#components.playermpd.PlayerMPD.next) + * [rewind](#components.playermpd.PlayerMPD.rewind) + * [replay](#components.playermpd.PlayerMPD.replay) + * [toggle](#components.playermpd.PlayerMPD.toggle) + * [replay\_if\_stopped](#components.playermpd.PlayerMPD.replay_if_stopped) + * [play\_card](#components.playermpd.PlayerMPD.play_card) + * [get\_folder\_content](#components.playermpd.PlayerMPD.get_folder_content) + * [play\_folder](#components.playermpd.PlayerMPD.play_folder) + * [play\_album](#components.playermpd.PlayerMPD.play_album) + * [get\_volume](#components.playermpd.PlayerMPD.get_volume) + * [set\_volume](#components.playermpd.PlayerMPD.set_volume) + * [play\_card\_callbacks](#components.playermpd.play_card_callbacks) +* [components.rpc\_command\_alias](#components.rpc_command_alias) +* [components.synchronisation.rfidcards](#components.synchronisation.rfidcards) + * [SyncRfidcards](#components.synchronisation.rfidcards.SyncRfidcards) + * [sync\_change\_on\_rfid\_scan](#components.synchronisation.rfidcards.SyncRfidcards.sync_change_on_rfid_scan) + * [sync\_all](#components.synchronisation.rfidcards.SyncRfidcards.sync_all) + * [sync\_card\_database](#components.synchronisation.rfidcards.SyncRfidcards.sync_card_database) + * [sync\_folder](#components.synchronisation.rfidcards.SyncRfidcards.sync_folder) +* [components.synchronisation](#components.synchronisation) +* [components.synchronisation.syncutils](#components.synchronisation.syncutils) +* [components.volume](#components.volume) + * [PulseMonitor](#components.volume.PulseMonitor) + * [SoundCardConnectCallbacks](#components.volume.PulseMonitor.SoundCardConnectCallbacks) + * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) + * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) + * [stop](#components.volume.PulseMonitor.stop) + * [run](#components.volume.PulseMonitor.run) + * [PulseVolumeControl](#components.volume.PulseVolumeControl) + * [OutputChangeCallbackHandler](#components.volume.PulseVolumeControl.OutputChangeCallbackHandler) + * [OutputVolumeCallbackHandler](#components.volume.PulseVolumeControl.OutputVolumeCallbackHandler) + * [toggle\_output](#components.volume.PulseVolumeControl.toggle_output) + * [get\_outputs](#components.volume.PulseVolumeControl.get_outputs) + * [publish\_volume](#components.volume.PulseVolumeControl.publish_volume) + * [publish\_outputs](#components.volume.PulseVolumeControl.publish_outputs) + * [set\_volume](#components.volume.PulseVolumeControl.set_volume) + * [get\_volume](#components.volume.PulseVolumeControl.get_volume) + * [change\_volume](#components.volume.PulseVolumeControl.change_volume) + * [get\_mute](#components.volume.PulseVolumeControl.get_mute) + * [mute](#components.volume.PulseVolumeControl.mute) + * [set\_output](#components.volume.PulseVolumeControl.set_output) + * [set\_soft\_max\_volume](#components.volume.PulseVolumeControl.set_soft_max_volume) + * [get\_soft\_max\_volume](#components.volume.PulseVolumeControl.get_soft_max_volume) + * [card\_list](#components.volume.PulseVolumeControl.card_list) +* [components.rfid](#components.rfid) +* [components.rfid.reader](#components.rfid.reader) + * [RfidCardDetectCallbacks](#components.rfid.reader.RfidCardDetectCallbacks) + * [register](#components.rfid.reader.RfidCardDetectCallbacks.register) + * [run\_callbacks](#components.rfid.reader.RfidCardDetectCallbacks.run_callbacks) + * [rfid\_card\_detect\_callbacks](#components.rfid.reader.rfid_card_detect_callbacks) + * [CardRemovalTimerClass](#components.rfid.reader.CardRemovalTimerClass) + * [\_\_init\_\_](#components.rfid.reader.CardRemovalTimerClass.__init__) +* [components.rfid.configure](#components.rfid.configure) + * [reader\_install\_dependencies](#components.rfid.configure.reader_install_dependencies) + * [reader\_load\_module](#components.rfid.configure.reader_load_module) + * [query\_user\_for\_reader](#components.rfid.configure.query_user_for_reader) + * [write\_config](#components.rfid.configure.write_config) +* [components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui](#components.rfid.hardware.fake_reader_gui.fake_reader_gui) +* [components.rfid.hardware.fake\_reader\_gui.description](#components.rfid.hardware.fake_reader_gui.description) +* [components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon) + * [create\_inputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_inputs) + * [set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.set_state) + * [que\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_state) + * [fix\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.fix_state) + * [pbox\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.pbox_set_state) + * [que\_set\_pbox](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_pbox) + * [create\_outputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_outputs) +* [components.rfid.hardware.generic\_usb.description](#components.rfid.hardware.generic_usb.description) +* [components.rfid.hardware.generic\_usb.generic\_usb](#components.rfid.hardware.generic_usb.generic_usb) +* [components.rfid.hardware.rc522\_spi.description](#components.rfid.hardware.rc522_spi.description) +* [components.rfid.hardware.rc522\_spi.rc522\_spi](#components.rfid.hardware.rc522_spi.rc522_spi) +* [components.rfid.hardware.pn532\_i2c\_py532.description](#components.rfid.hardware.pn532_i2c_py532.description) +* [components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532](#components.rfid.hardware.pn532_i2c_py532.pn532_i2c_py532) +* [components.rfid.hardware.rdm6300\_serial.rdm6300\_serial](#components.rfid.hardware.rdm6300_serial.rdm6300_serial) + * [decode](#components.rfid.hardware.rdm6300_serial.rdm6300_serial.decode) +* [components.rfid.hardware.rdm6300\_serial.description](#components.rfid.hardware.rdm6300_serial.description) +* [components.rfid.hardware.template\_new\_reader.description](#components.rfid.hardware.template_new_reader.description) +* [components.rfid.hardware.template\_new\_reader.template\_new\_reader](#components.rfid.hardware.template_new_reader.template_new_reader) + * [query\_customization](#components.rfid.hardware.template_new_reader.template_new_reader.query_customization) + * [ReaderClass](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass) + * [\_\_init\_\_](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.__init__) + * [cleanup](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.cleanup) + * [stop](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.stop) + * [read\_card](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.read_card) +* [components.rfid.readerbase](#components.rfid.readerbase) + * [ReaderBaseClass](#components.rfid.readerbase.ReaderBaseClass) +* [components.rfid.cards](#components.rfid.cards) + * [list\_cards](#components.rfid.cards.list_cards) + * [delete\_card](#components.rfid.cards.delete_card) + * [register\_card](#components.rfid.cards.register_card) + * [register\_card\_custom](#components.rfid.cards.register_card_custom) + * [save\_card\_database](#components.rfid.cards.save_card_database) +* [components.rfid.cardutils](#components.rfid.cardutils) + * [decode\_card\_command](#components.rfid.cardutils.decode_card_command) + * [card\_command\_to\_str](#components.rfid.cardutils.card_command_to_str) + * [card\_to\_str](#components.rfid.cardutils.card_to_str) +* [components.publishing](#components.publishing) + * [republish](#components.publishing.republish) +* [components.player](#components.player) + * [MusicLibPath](#components.player.MusicLibPath) + * [get\_music\_library\_path](#components.player.get_music_library_path) +* [components.jingle](#components.jingle) + * [JingleFactory](#components.jingle.JingleFactory) + * [list](#components.jingle.JingleFactory.list) + * [play](#components.jingle.play) + * [play\_startup](#components.jingle.play_startup) + * [play\_shutdown](#components.jingle.play_shutdown) +* [components.jingle.alsawave](#components.jingle.alsawave) + * [AlsaWave](#components.jingle.alsawave.AlsaWave) + * [play](#components.jingle.alsawave.AlsaWave.play) + * [AlsaWaveBuilder](#components.jingle.alsawave.AlsaWaveBuilder) + * [\_\_init\_\_](#components.jingle.alsawave.AlsaWaveBuilder.__init__) +* [components.jingle.jinglemp3](#components.jingle.jinglemp3) + * [JingleMp3Play](#components.jingle.jinglemp3.JingleMp3Play) + * [play](#components.jingle.jinglemp3.JingleMp3Play.play) + * [JingleMp3PlayBuilder](#components.jingle.jinglemp3.JingleMp3PlayBuilder) + * [\_\_init\_\_](#components.jingle.jinglemp3.JingleMp3PlayBuilder.__init__) +* [components.hostif.linux](#components.hostif.linux) + * [shutdown](#components.hostif.linux.shutdown) + * [reboot](#components.hostif.linux.reboot) + * [jukebox\_is\_service](#components.hostif.linux.jukebox_is_service) + * [is\_any\_jukebox\_service\_active](#components.hostif.linux.is_any_jukebox_service_active) + * [restart\_service](#components.hostif.linux.restart_service) + * [get\_disk\_usage](#components.hostif.linux.get_disk_usage) + * [get\_cpu\_temperature](#components.hostif.linux.get_cpu_temperature) + * [get\_ip\_address](#components.hostif.linux.get_ip_address) + * [wlan\_disable\_power\_down](#components.hostif.linux.wlan_disable_power_down) + * [get\_autohotspot\_status](#components.hostif.linux.get_autohotspot_status) + * [stop\_autohotspot](#components.hostif.linux.stop_autohotspot) + * [start\_autohotspot](#components.hostif.linux.start_autohotspot) +* [components.misc](#components.misc) + * [rpc\_cmd\_help](#components.misc.rpc_cmd_help) + * [get\_all\_loaded\_packages](#components.misc.get_all_loaded_packages) + * [get\_all\_failed\_packages](#components.misc.get_all_failed_packages) + * [get\_start\_time](#components.misc.get_start_time) + * [get\_log](#components.misc.get_log) + * [get\_log\_debug](#components.misc.get_log_debug) + * [get\_log\_error](#components.misc.get_log_error) + * [get\_git\_state](#components.misc.get_git_state) + * [empty\_rpc\_call](#components.misc.empty_rpc_call) +* [components.controls](#components.controls) +* [components.controls.bluetooth\_audio\_buttons](#components.controls.bluetooth_audio_buttons) +* [components.controls.common.evdev\_listener](#components.controls.common.evdev_listener) + * [find\_device](#components.controls.common.evdev_listener.find_device) + * [EvDevKeyListener](#components.controls.common.evdev_listener.EvDevKeyListener) + * [\_\_init\_\_](#components.controls.common.evdev_listener.EvDevKeyListener.__init__) + * [run](#components.controls.common.evdev_listener.EvDevKeyListener.run) + * [start](#components.controls.common.evdev_listener.EvDevKeyListener.start) +* [components.music\_cover\_art](#components.music_cover_art) + * [MusicCoverArt](#components.music_cover_art.MusicCoverArt) + * [get\_by\_filename\_as\_base64](#components.music_cover_art.MusicCoverArt.get_by_filename_as_base64) +* [components.battery\_monitor](#components.battery_monitor) +* [components.battery\_monitor.BatteryMonitorBase](#components.battery_monitor.BatteryMonitorBase) + * [pt1\_frac](#components.battery_monitor.BatteryMonitorBase.pt1_frac) + * [BattmonBase](#components.battery_monitor.BatteryMonitorBase.BattmonBase) +* [components.battery\_monitor.batt\_mon\_simulator](#components.battery_monitor.batt_mon_simulator) + * [battmon\_simulator](#components.battery_monitor.batt_mon_simulator.battmon_simulator) +* [components.battery\_monitor.batt\_mon\_i2c\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015) + * [battmon\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015.battmon_ads1015) +* [components.gpio.gpioz.plugin](#components.gpio.gpioz.plugin) + * [output\_devices](#components.gpio.gpioz.plugin.output_devices) + * [input\_devices](#components.gpio.gpioz.plugin.input_devices) + * [factory](#components.gpio.gpioz.plugin.factory) + * [IS\_ENABLED](#components.gpio.gpioz.plugin.IS_ENABLED) + * [IS\_MOCKED](#components.gpio.gpioz.plugin.IS_MOCKED) + * [CONFIG\_FILE](#components.gpio.gpioz.plugin.CONFIG_FILE) + * [ServiceIsRunningCallbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks) + * [register](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.register) + * [run\_callbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.run_callbacks) + * [service\_is\_running\_callbacks](#components.gpio.gpioz.plugin.service_is_running_callbacks) + * [build\_output\_device](#components.gpio.gpioz.plugin.build_output_device) + * [build\_input\_device](#components.gpio.gpioz.plugin.build_input_device) + * [get\_output](#components.gpio.gpioz.plugin.get_output) + * [on](#components.gpio.gpioz.plugin.on) + * [off](#components.gpio.gpioz.plugin.off) + * [set\_value](#components.gpio.gpioz.plugin.set_value) + * [flash](#components.gpio.gpioz.plugin.flash) +* [components.gpio.gpioz.plugin.connectivity](#components.gpio.gpioz.plugin.connectivity) + * [BUZZ\_TONE](#components.gpio.gpioz.plugin.connectivity.BUZZ_TONE) + * [register\_rfid\_callback](#components.gpio.gpioz.plugin.connectivity.register_rfid_callback) + * [register\_status\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_led_callback) + * [register\_status\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_buzzer_callback) + * [register\_status\_tonalbuzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_tonalbuzzer_callback) + * [register\_audio\_sink\_change\_callback](#components.gpio.gpioz.plugin.connectivity.register_audio_sink_change_callback) + * [register\_volume\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_led_callback) + * [register\_volume\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_buzzer_callback) + * [register\_volume\_rgbled\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_rgbled_callback) +* [components.gpio.gpioz.core.converter](#components.gpio.gpioz.core.converter) + * [ColorProperty](#components.gpio.gpioz.core.converter.ColorProperty) + * [VolumeToRGB](#components.gpio.gpioz.core.converter.VolumeToRGB) + * [\_\_call\_\_](#components.gpio.gpioz.core.converter.VolumeToRGB.__call__) + * [luminize](#components.gpio.gpioz.core.converter.VolumeToRGB.luminize) +* [components.gpio.gpioz.core.mock](#components.gpio.gpioz.core.mock) + * [patch\_mock\_outputs\_with\_callback](#components.gpio.gpioz.core.mock.patch_mock_outputs_with_callback) +* [components.gpio.gpioz.core.input\_devices](#components.gpio.gpioz.core.input_devices) + * [NameMixin](#components.gpio.gpioz.core.input_devices.NameMixin) + * [set\_rpc\_actions](#components.gpio.gpioz.core.input_devices.NameMixin.set_rpc_actions) + * [EventProperty](#components.gpio.gpioz.core.input_devices.EventProperty) + * [ButtonBase](#components.gpio.gpioz.core.input_devices.ButtonBase) + * [value](#components.gpio.gpioz.core.input_devices.ButtonBase.value) + * [pin](#components.gpio.gpioz.core.input_devices.ButtonBase.pin) + * [pull\_up](#components.gpio.gpioz.core.input_devices.ButtonBase.pull_up) + * [close](#components.gpio.gpioz.core.input_devices.ButtonBase.close) + * [Button](#components.gpio.gpioz.core.input_devices.Button) + * [on\_press](#components.gpio.gpioz.core.input_devices.Button.on_press) + * [LongPressButton](#components.gpio.gpioz.core.input_devices.LongPressButton) + * [on\_press](#components.gpio.gpioz.core.input_devices.LongPressButton.on_press) + * [ShortLongPressButton](#components.gpio.gpioz.core.input_devices.ShortLongPressButton) + * [RotaryEncoder](#components.gpio.gpioz.core.input_devices.RotaryEncoder) + * [pin\_a](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_a) + * [pin\_b](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_b) + * [on\_rotate\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_clockwise) + * [on\_rotate\_counter\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_counter_clockwise) + * [close](#components.gpio.gpioz.core.input_devices.RotaryEncoder.close) + * [TwinButton](#components.gpio.gpioz.core.input_devices.TwinButton) + * [StateVar](#components.gpio.gpioz.core.input_devices.TwinButton.StateVar) + * [close](#components.gpio.gpioz.core.input_devices.TwinButton.close) + * [value](#components.gpio.gpioz.core.input_devices.TwinButton.value) + * [is\_active](#components.gpio.gpioz.core.input_devices.TwinButton.is_active) +* [components.gpio.gpioz.core.output\_devices](#components.gpio.gpioz.core.output_devices) + * [LED](#components.gpio.gpioz.core.output_devices.LED) + * [flash](#components.gpio.gpioz.core.output_devices.LED.flash) + * [Buzzer](#components.gpio.gpioz.core.output_devices.Buzzer) + * [flash](#components.gpio.gpioz.core.output_devices.Buzzer.flash) + * [PWMLED](#components.gpio.gpioz.core.output_devices.PWMLED) + * [flash](#components.gpio.gpioz.core.output_devices.PWMLED.flash) + * [RGBLED](#components.gpio.gpioz.core.output_devices.RGBLED) + * [flash](#components.gpio.gpioz.core.output_devices.RGBLED.flash) + * [TonalBuzzer](#components.gpio.gpioz.core.output_devices.TonalBuzzer) + * [flash](#components.gpio.gpioz.core.output_devices.TonalBuzzer.flash) + * [melody](#components.gpio.gpioz.core.output_devices.TonalBuzzer.melody) +* [components.timers](#components.timers) +* [jukebox](#jukebox) +* [jukebox.callingback](#jukebox.callingback) + * [CallbackHandler](#jukebox.callingback.CallbackHandler) + * [register](#jukebox.callingback.CallbackHandler.register) + * [run\_callbacks](#jukebox.callingback.CallbackHandler.run_callbacks) + * [has\_callbacks](#jukebox.callingback.CallbackHandler.has_callbacks) +* [jukebox.version](#jukebox.version) + * [version](#jukebox.version.version) + * [version\_info](#jukebox.version.version_info) +* [jukebox.cfghandler](#jukebox.cfghandler) + * [ConfigHandler](#jukebox.cfghandler.ConfigHandler) + * [loaded\_from](#jukebox.cfghandler.ConfigHandler.loaded_from) + * [get](#jukebox.cfghandler.ConfigHandler.get) + * [setdefault](#jukebox.cfghandler.ConfigHandler.setdefault) + * [getn](#jukebox.cfghandler.ConfigHandler.getn) + * [setn](#jukebox.cfghandler.ConfigHandler.setn) + * [setndefault](#jukebox.cfghandler.ConfigHandler.setndefault) + * [config\_dict](#jukebox.cfghandler.ConfigHandler.config_dict) + * [is\_modified](#jukebox.cfghandler.ConfigHandler.is_modified) + * [clear\_modified](#jukebox.cfghandler.ConfigHandler.clear_modified) + * [save](#jukebox.cfghandler.ConfigHandler.save) + * [load](#jukebox.cfghandler.ConfigHandler.load) + * [get\_handler](#jukebox.cfghandler.get_handler) + * [load\_yaml](#jukebox.cfghandler.load_yaml) + * [write\_yaml](#jukebox.cfghandler.write_yaml) +* [jukebox.playlistgenerator](#jukebox.playlistgenerator) + * [TYPE\_DECODE](#jukebox.playlistgenerator.TYPE_DECODE) + * [PlaylistCollector](#jukebox.playlistgenerator.PlaylistCollector) + * [\_\_init\_\_](#jukebox.playlistgenerator.PlaylistCollector.__init__) + * [set\_exclusion\_endings](#jukebox.playlistgenerator.PlaylistCollector.set_exclusion_endings) + * [get\_directory\_content](#jukebox.playlistgenerator.PlaylistCollector.get_directory_content) + * [parse](#jukebox.playlistgenerator.PlaylistCollector.parse) +* [jukebox.NvManager](#jukebox.NvManager) +* [jukebox.publishing](#jukebox.publishing) + * [get\_publisher](#jukebox.publishing.get_publisher) +* [jukebox.publishing.subscriber](#jukebox.publishing.subscriber) +* [jukebox.publishing.server](#jukebox.publishing.server) + * [PublishServer](#jukebox.publishing.server.PublishServer) + * [run](#jukebox.publishing.server.PublishServer.run) + * [handle\_message](#jukebox.publishing.server.PublishServer.handle_message) + * [handle\_subscription](#jukebox.publishing.server.PublishServer.handle_subscription) + * [Publisher](#jukebox.publishing.server.Publisher) + * [\_\_init\_\_](#jukebox.publishing.server.Publisher.__init__) + * [send](#jukebox.publishing.server.Publisher.send) + * [revoke](#jukebox.publishing.server.Publisher.revoke) + * [resend](#jukebox.publishing.server.Publisher.resend) + * [close\_server](#jukebox.publishing.server.Publisher.close_server) +* [jukebox.daemon](#jukebox.daemon) + * [log\_active\_threads](#jukebox.daemon.log_active_threads) + * [JukeBox](#jukebox.daemon.JukeBox) + * [signal\_handler](#jukebox.daemon.JukeBox.signal_handler) +* [jukebox.plugs](#jukebox.plugs) + * [PluginPackageClass](#jukebox.plugs.PluginPackageClass) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [tag](#jukebox.plugs.tag) + * [initialize](#jukebox.plugs.initialize) + * [finalize](#jukebox.plugs.finalize) + * [atexit](#jukebox.plugs.atexit) + * [load](#jukebox.plugs.load) + * [load\_all\_named](#jukebox.plugs.load_all_named) + * [load\_all\_unnamed](#jukebox.plugs.load_all_unnamed) + * [load\_all\_finalize](#jukebox.plugs.load_all_finalize) + * [close\_down](#jukebox.plugs.close_down) + * [call](#jukebox.plugs.call) + * [call\_ignore\_errors](#jukebox.plugs.call_ignore_errors) + * [exists](#jukebox.plugs.exists) + * [get](#jukebox.plugs.get) + * [loaded\_as](#jukebox.plugs.loaded_as) + * [delete](#jukebox.plugs.delete) + * [dump\_plugins](#jukebox.plugs.dump_plugins) + * [summarize](#jukebox.plugs.summarize) + * [generate\_help\_rst](#jukebox.plugs.generate_help_rst) + * [get\_all\_loaded\_packages](#jukebox.plugs.get_all_loaded_packages) + * [get\_all\_failed\_packages](#jukebox.plugs.get_all_failed_packages) +* [jukebox.speaking\_text](#jukebox.speaking_text) +* [jukebox.multitimer](#jukebox.multitimer) + * [MultiTimer](#jukebox.multitimer.MultiTimer) + * [cancel](#jukebox.multitimer.MultiTimer.cancel) + * [GenericTimerClass](#jukebox.multitimer.GenericTimerClass) + * [\_\_init\_\_](#jukebox.multitimer.GenericTimerClass.__init__) + * [start](#jukebox.multitimer.GenericTimerClass.start) + * [cancel](#jukebox.multitimer.GenericTimerClass.cancel) + * [toggle](#jukebox.multitimer.GenericTimerClass.toggle) + * [trigger](#jukebox.multitimer.GenericTimerClass.trigger) + * [is\_alive](#jukebox.multitimer.GenericTimerClass.is_alive) + * [get\_timeout](#jukebox.multitimer.GenericTimerClass.get_timeout) + * [set\_timeout](#jukebox.multitimer.GenericTimerClass.set_timeout) + * [publish](#jukebox.multitimer.GenericTimerClass.publish) + * [get\_state](#jukebox.multitimer.GenericTimerClass.get_state) + * [GenericEndlessTimerClass](#jukebox.multitimer.GenericEndlessTimerClass) + * [GenericMultiTimerClass](#jukebox.multitimer.GenericMultiTimerClass) + * [\_\_init\_\_](#jukebox.multitimer.GenericMultiTimerClass.__init__) + * [start](#jukebox.multitimer.GenericMultiTimerClass.start) +* [jukebox.utils](#jukebox.utils) + * [decode\_rpc\_call](#jukebox.utils.decode_rpc_call) + * [decode\_rpc\_command](#jukebox.utils.decode_rpc_command) + * [decode\_and\_call\_rpc\_command](#jukebox.utils.decode_and_call_rpc_command) + * [bind\_rpc\_command](#jukebox.utils.bind_rpc_command) + * [rpc\_call\_to\_str](#jukebox.utils.rpc_call_to_str) + * [generate\_cmd\_alias\_rst](#jukebox.utils.generate_cmd_alias_rst) + * [generate\_cmd\_alias\_reference](#jukebox.utils.generate_cmd_alias_reference) + * [get\_git\_state](#jukebox.utils.get_git_state) +* [jukebox.rpc](#jukebox.rpc) +* [jukebox.rpc.client](#jukebox.rpc.client) +* [jukebox.rpc.server](#jukebox.rpc.server) + * [RpcServer](#jukebox.rpc.server.RpcServer) + * [\_\_init\_\_](#jukebox.rpc.server.RpcServer.__init__) + * [run](#jukebox.rpc.server.RpcServer.run) + + + +# run\_jukebox + +This is the main app and starts the Jukebox Core. + +Usually this runs as a service, which is started automatically after boot-up. At times, it may be necessary to restart +the service. +For example after a configuration change. Not all configuration changes can be applied on-the-fly. +See :ref:`userguide/configuration:Jukebox Configuration`. + +For debugging, it is usually desirable to run the Jukebox directly from the console rather than +as service. This gives direct logging info in the console and allows changing command line parameters. +See :ref:`userguide/troubleshooting:Troubleshooting`. + + + +# \_\_init\_\_ + + + +# run\_register\_rfid\_reader + +Setup tool to configure the RFID Readers. + +Run this once to register and configure the RFID readers with the Jukebox. Can be re-run at any time to change +the settings. For more information see :ref:`rfid/rfid:RFID Readers`. + +.. note:: This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). + Any manual modifications to the settings will have to be re-applied + + + +# run\_rpc\_tool + +Command Line Interface to the Jukebox RPC Server + +A command line tool for sending RPC commands to the running jukebox app. +This uses the same interface as the WebUI. Can be used for additional control +or for debugging. + +The tool features auto-completion and command history. + +The list of available commands is fetched from the running Jukebox service. + +.. todo: + - kwargs support + + + +#### get\_common\_beginning + +```python +def get_common_beginning(strings) +``` + +Return the strings that are common to the beginning of each string in the strings list. + + + +# run\_configure\_audio + +Setup tool to register the PulseAudio sinks as primary and secondary audio outputs. + +Will also setup equalizer and mono down mixer in the pulseaudio config file. + +Run this once after installation. Can be re-run at any time to change the settings. +For more information see :ref:`userguide/audio:Audio Configuration`. + + + +# run\_publicity\_sniffer + +A command line tool that monitors all messages being sent out from the +Jukebox via the publishing interface. Received messages are printed in the console. +Mainly used for debugging. + + + +# misc + + + +#### recursive\_chmod + +```python +def recursive_chmod(path, mode_files, mode_dirs) +``` + +Recursively change folder and file permissions + +mode_files/mode dirs can be given in octal notation e.g. 0o777 +flags from the stats module. + +Reference: https://docs.python.org/3/library/os.html#os.chmod + + + +#### flatten + +```python +def flatten(iterable) +``` + +Flatten all levels of hierarchy in nested iterables + + + +#### getattr\_hierarchical + +```python +def getattr_hierarchical(obj: Any, name: str) -> Any +``` + +Like the builtin getattr, but descends though the hierarchy levels + + + +# misc.inputminus + +Zero 3rd-party dependency module for user prompting + +Yes, there are modules out there to do the same and they have more features. +However, this is low-complexity and has zero dependencies + + + +#### input\_int + +```python +def input_int(prompt, + blank=None, + min=None, + max=None, + prompt_color=None, + prompt_hint=False) -> int +``` + +Request an integer input from user + +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `min`: Minimum valid integer value (None disables this check) +- `max`: Maximum valid integer value (None disables this check) +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [min...max, default=xx] to end of prompt + +**Returns**: + +integer value read from user input + + + +#### input\_yesno + +```python +def input_yesno(prompt, + blank=None, + prompt_color=None, + prompt_hint=False) -> bool +``` + +Request a yes / no choice from user + +Accepts multiple input for true/false and is case insensitive + +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized + +**Returns**: + +boolean value read from user input + + + +# misc.loggingext + +############## +Logger +############## +We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. + +The top-level logger is called 'jb' (to make it short). In any module you may simple create a child-logger at any hierarchy +level below 'jb'. It will inherit settings from it's parent logger unless otherwise configured in the yaml file. +Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be +created on the spot. + +:Example: How to get logger and log away at your heart's content: +>>> import logging +>>> logger = logging.getLogger('jb.awesome_module') +>>> logger.info('Started general awesomeness aura') + +Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: +`` +loggers: +jb: +level: WARNING +handlers: [console, debug_file_handler, error_file_handler] +propagate: no +jb.awesome_module: +level: DEBUG +`` + +.. note:: +The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense) +There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output + + + +## ColorFilter Objects + +```python +class ColorFilter(logging.Filter) +``` + +This filter adds colors to the logger + +It adds all colors from simplecolors by using the color name as new keyword, +i.e. use %(colorname)c or {colorname} in the formatter string + +It also adds the keyword {levelnameColored} which is an auto-colored drop-in replacement +for the levelname depending on severity. + +Don't forget to {reset} the color settings at the end of the string. + + + +#### \_\_init\_\_ + +```python +def __init__(enable=True, color_levelname=True) +``` + +**Arguments**: + +- `enable`: Enable the coloring +- `color_levelname`: Enable auto-coloring when using the levelname keyword + + + +## PubStream Objects + +```python +class PubStream() +``` + +" +Stream handler wrapper around the publisher for logging.StreamHandler + +Allows logging to send all log information (based on logging configuration) +to the Publisher. + +ATTENTION: This can lead to recursions! + +Recursions come up when +(a) Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, + which causes a send, ..... +(b) Publisher initialization emits logs, which need a Publisher instance to send logs + +IMPORTANT: To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the +functions in the send-function stack! + + + +## PubStreamHandler Objects + +```python +class PubStreamHandler(logging.StreamHandler) +``` + +Wrapper for logging.StreamHandler with stream = PubStream + +This serves one purpose: In logger.yaml custom handlers +can be configured (which are automatically instantiated). +Using this Handler, we can output to PubStream whithout +support code to instantiate PubStream keeping this file generic + + + +# misc.simplecolors + +Zero 3rd-party dependency module to add colors to unix terminal output + +Yes, there are modules out there to do the same and they have more features. +However, this is low-complexity and has zero dependencies + + + +## Colors Objects + +```python +class Colors() +``` + +Container class for all the colors as constants + + + +#### resolve + +```python +def resolve(color_name: str) +``` + +Resolve a color name into the respective color constant + +**Arguments**: + +- `color_name`: Name of the color + +**Returns**: + +color constant + + + +#### print + +```python +def print(color: Colors, + *values, + sep=' ', + end='\n', + file=sys.stdout, + flush=False) +``` + +Drop-in replacement for print with color choice and auto color reset for convenience + +Use just as a regular print function, but with first parameter as color + + + +# components + + + +# components.playermpd.playcontentcallback + + + +## PlayContentCallbacks Objects + +```python +class PlayContentCallbacks(Generic[STATE], CallbackHandler) +``` + +Callbacks are executed in various play functions + + + +#### register + +```python +def register(func: Callable[[str, STATE], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(folder: str, state: STATE) + :noindex: + + :param folder: relativ path to folder to play + :param state: indicator of the state inside the calling + + + + +#### run\_callbacks + +```python +def run_callbacks(folder: str, state: STATE) +``` + +:meta private: + + + +# components.playermpd + +Package for interfacing with the MPD Music Player Daemon + +Status information in three topics +1) Player Status: published only on change +This is a subset of the MPD status (and not the full MPD status) ?? +- folder +- song +- volume (volume is published only via player status, and not separatly to avoid too many Threads) +- ... +2) Elapsed time: published every 250 ms, unless constant +- elapsed +3) Folder Config: published only on change +This belongs to the folder being played +Publish: +- random, resume, single, loop +On save store this information: +Contains the information for resume functionality of each folder +- random, resume, single, loop +- if resume: +- current song, elapsed +- what is PLAYSTATUS for? +When to save +- on stop +Angstsave: +- on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) +- on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) +Load checks: +- if resume, but no song, elapsed -> log error and start from the beginning + +Status storing: +- Folder config for each folder (see above) +- Information to restart last folder playback, which is: +- last_folder -> folder_on_close +- song, elapsed +- random, resume, single, loop +- if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! +on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card + +Internal status +- last played folder: Needed to detect second swipe + + +Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, +'audio_folder_status': +{'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, +'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} + +**References**: + + https://github.com/Mic92/python-mpd2 + https://python-mpd2.readthedocs.io/en/latest/topics/commands.html + https://mpd.readthedocs.io/en/latest/protocol.html + + sudo -u mpd speaker-test -t wav -c 2 + + + +## PlayerMPD Objects + +```python +class PlayerMPD() +``` + +Interface to MPD Music Player Daemon + + + +#### mpd\_retry\_with\_mutex + +```python +def mpd_retry_with_mutex(mpd_cmd, *args) +``` + +This method adds thread saftey for acceses to mpd via a mutex lock, +it shall be used for each access to mpd to ensure thread safety +In case of a communication error the connection will be reestablished and the pending command will be repeated 2 times + +I think this should be refactored to a decorator + + + +#### pause + +```python +@plugs.tag +def pause(state: int = 1) +``` + +Enforce pause to state (1: pause, 0: resume) + +This is what you want as card removal action: pause the playback, so it can be resumed when card is placed +on the reader again. What happens on re-placement depends on configured second swipe option + + + +#### next + +```python +@plugs.tag +def next() +``` + +Play next track in current playlist + + + +#### rewind + +```python +@plugs.tag +def rewind() +``` + +Re-start current playlist from first track + +Note: Will not re-read folder config, but leave settings untouched + + + +#### replay + +```python +@plugs.tag +def replay() +``` + +Re-start playing the last-played folder + +Will reset settings to folder config + + + +#### toggle + +```python +@plugs.tag +def toggle() +``` + +Toggle pause state, i.e. do a pause / resume depending on current state + + + +#### replay\_if\_stopped + +```python +@plugs.tag +def replay_if_stopped() +``` + +Re-start playing the last-played folder unless playlist is still playing + +.. note:: To me this seems much like the behaviour of play, + but we keep it as it is specifically implemented in box 2.X + + + +#### play\_card + +```python +@plugs.tag +def play_card(folder: str, recursive: bool = False) +``` + +Main entry point for trigger music playing from RFID reader. Decodes second swipe options before playing folder content + +Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action +accordingly. + +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively + + + +#### get\_folder\_content + +```python +@plugs.tag +def get_folder_content(folder: str) +``` + +Get the folder content as content list with meta-information. Depth is always 1. + +Call repeatedly to descend in hierarchy + +**Arguments**: + +- `folder`: Folder path relative to music library path + + + +#### play\_folder + +```python +@plugs.tag +def play_folder(folder: str, recursive: bool = False) -> None +``` + +Playback a music folder. + +Folder content is added to the playlist as described by :mod:`jukebox.playlistgenerator`. +The playlist is cleared first. + +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively + + + +#### play\_album + +```python +@plugs.tag +def play_album(albumartist: str, album: str) +``` + +Playback a album found in MPD database. + +All album songs are added to the playlist +The playlist is cleared first. + +**Arguments**: + +- `albumartist`: Artist of the Album provided by MPD database +- `album`: Album name provided by MPD database + + + +#### get\_volume + +```python +def get_volume() +``` + +Get the current volume + +For volume control do not use directly, but use through the plugin 'volume', +as the user may have configured a volume control manager other than MPD + + + +#### set\_volume + +```python +def set_volume(volume) +``` + +Set the volume + +For volume control do not use directly, but use through the plugin 'volume', +as the user may have configured a volume control manager other than MPD + + + +#### play\_card\_callbacks + +Callback handler instance for play_card events. +- is executed when play_card function is called +States: +- See :class:`PlayCardState` +See :class:`PlayContentCallbacks` + + + +# components.rpc\_command\_alias + +This file provides definitions for RPC command aliases + +See :ref:`userguide/rpc_commands` + + + +# components.synchronisation.rfidcards + +Handles the synchronisation of RFID cards (audiofolder and card database entries). + +sync-all -> all card entries and audiofolders are synced from remote including deletions +sync-on-scan -> only the entry and audiofolder for the cardId will be synced from remote. + Deletions are only performed on files and subfolder inside the audiofolder. + A deletion of the audiofolder itself on remote side will not be propagated. + +card database: +On synchronisation the remote file will not be synced with the original cards database, but rather a local copy. +If a full sync is performed, the state is written back to the original file. +If a single card sync is performed, only the state of the specific cardId is updated in the original file. +This is done to allow to play audio offline. +Otherwise we would also update other cardIds where the audiofolders have not been synced yet. +The local copy is kept to reduce unnecessary syncing. + + + +## SyncRfidcards Objects + +```python +class SyncRfidcards() +``` + +Control class for sync RFID cards functionality + + + +#### sync\_change\_on\_rfid\_scan + +```python +@plugs.tag +def sync_change_on_rfid_scan(option: str = 'toggle') -> None +``` + +Change activation of 'on_rfid_scan_enabled' + +**Arguments**: + +- `option`: Must be one of 'enable', 'disable', 'toggle' + + + +#### sync\_all + +```python +@plugs.tag +def sync_all() -> bool +``` + +Sync all audiofolder and cardids from the remote server. +Removes local entries not existing at the remote server. + + + +#### sync\_card\_database + +```python +@plugs.tag +def sync_card_database(card_id: str) -> bool +``` + +Sync the card database from the remote server, if existing. + +If card_id is provided only this entry is updated. + +**Arguments**: + +- `card_id`: The cardid to update + + + +#### sync\_folder + +```python +@plugs.tag +def sync_folder(folder: str) -> bool +``` + +Sync the folder from the remote server, if existing + +**Arguments**: + +- `folder`: Folder path relative to music library path + + + +# components.synchronisation + + + +# components.synchronisation.syncutils + + + +# components.volume + +PulseAudio Volume Control Plugin Package + +Features + + * Volume Control + * Two outputs + * Watcher thread on volume / output change + +Publishes + + * volume.level + * volume.sink + +PulseAudio References + +https://brokkr.net/2018/05/24/down-the-drain-the-elusive-default-pulseaudio-sink/ + +Check fallback device (on device de-connect): +$ pacmd list-sinks | grep -e 'name:' -e 'index' + + +Integration + +Pulse Audio runs as a user process. Processes who want to communicate / stream to it +must also run as a user process. + +This means must also run as user process, as described in :ref:`userguide/system:Music Player Daemon (MPD)` + +Misc + +PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module +with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration +in ``/usr/pulse/default.pa``. So, we don't need to worry about it. +If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs +from the Jukebox. Remove it from the configuration! + +.. code-block:: text + + ### Use hot-plugged devices like Bluetooth or USB automatically (LP: `1702794`) + ### not available on PI? + .ifexists module-switch-on-connect.so + load-module module-switch-on-connect + .endif + +Why PulseAudio? + +The audio configuration of the system is one of those topics, +which has a myriad of options and possibilities. Every system is different and PulseAudio unifies these and +makes our life easier. Besides, it is only option to support Bluetooth at the moment. + +Callbacks: + +The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): + + ``. :func:`add_on_connect_callback` + ``. :func:`add_on_output_change_callbacks` + ``. :func:`add_on_volume_change_callback` + + + +## PulseMonitor Objects + +```python +class PulseMonitor(threading.Thread) +``` + +A thread for monitoring and interacting with the Pulse Lib via pulsectrl + +Whenever we want to access pulsectl, we need to exit the event listen loop. +This is handled by the context manager. It stops the event loop and returns +the pulsectl instance to be used (it does no return the monitor thread itself!) + +The context manager also locks the module to ensure proper thread sequencing, +as only a single thread may work with pulsectl at any time. Currently, an RLock is +used, even if it may not be necessary + + + +## SoundCardConnectCallbacks Objects + +```python +class SoundCardConnectCallbacks(CallbackHandler) +``` + +Callbacks are executed when + + * new sound card gets connected + + + +#### register + +```python +def register(func: Callable[[str, str], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(card_driver: str, device_name: str) + :noindex: + + :param card_driver: The PulseAudio card driver module, + e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` + :param device_name: The sound card device name as reported + in device properties + + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + +:meta private: + + + +#### toggle\_on\_connect + +```python +@property +def toggle_on_connect() +``` + +Returns :data:`True` if the sound card shall be changed when a new card connects/disconnects. Setting this +property changes the behavior. + +.. note:: A new card is always assumed to be the secondary device from the audio configuration. + At the moment there is no check it actually is the configured device. This means any new + device connection will initiate the toggle. This, however, is no real issue as the RPi's audio + system will be relatively stable once setup + + + +#### toggle\_on\_connect + +```python +@toggle_on_connect.setter +def toggle_on_connect(state=True) +``` + +Toggle Doc 2 + + + +#### stop + +```python +def stop() +``` + +Stop the pulse monitor thread + + + +#### run + +```python +def run() -> None +``` + +Starts the pulse monitor thread + + + +## PulseVolumeControl Objects + +```python +class PulseVolumeControl() +``` + +Volume control manager for PulseAudio + +When accessing the pulse library, it needs to be put into a special +state. Which is ensured by the context manager + +.. code-block: python + + with pulse_monitor as pulse ... + + +All private functions starting with `_function_name` assume that this is ensured by +the calling function. All user functions acquire proper context! + + + +## OutputChangeCallbackHandler Objects + +```python +class OutputChangeCallbackHandler(CallbackHandler) +``` + +Callbacks are executed when + + * audio sink is changed + + + +#### register + +```python +def register(func: Callable[[str, str, int, int], None]) +``` + +Add a new callback function :attr:`func`. + +Parameters always give the valid audio sink. That means, if an error +occurred, all parameters are valid. + +Callback signature is + +.. py:function:: func(sink_name: str, alias: str, sink_index: int, error_state: int) + :noindex: + + :param sink_name: PulseAudio's sink name + :param alias: The alias for :attr:`sink_name` + :param sink_index: The index of the sink in the configuration list + :param error_state: 1 if there was an attempt to change the output + but an error occurred. Above parameters always give the now valid sink! + If a sink change is successful, it is 0. + + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + +:meta private: + + + +## OutputVolumeCallbackHandler Objects + +```python +class OutputVolumeCallbackHandler(CallbackHandler) +``` + +Callbacks are executed when + + * audio volume level is changed + + + +#### register + +```python +def register(func: Callable[[int, bool, bool], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(volume: int, is_min: bool, is_max: bool) + :noindex: + + :param volume: Volume level + :param is_min: 1, if volume level is minimum, else 0 + :param is_max: 1, if volume level is maximum, else 0 + + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + +:meta private: + + + +#### toggle\_output + +```python +@plugin.tag +def toggle_output() +``` + +Toggle the audio output sink + + + +#### get\_outputs + +```python +@plugin.tag +def get_outputs() +``` + +Get current output and list of outputs + + + +#### publish\_volume + +```python +@plugin.tag +def publish_volume() +``` + +Publish (volume, mute) + + + +#### publish\_outputs + +```python +@plugin.tag +def publish_outputs() +``` + +Publish current output and list of outputs + + + +#### set\_volume + +```python +@plugin.tag +def set_volume(volume: int) +``` + +Set the volume (0-100) for the currently active output + + + +#### get\_volume + +```python +@plugin.tag +def get_volume() +``` + +Get the volume + + + +#### change\_volume + +```python +@plugin.tag +def change_volume(step: int) +``` + +Increase/decrease the volume by step for the currently active output + + + +#### get\_mute + +```python +@plugin.tag +def get_mute() +``` + +Return mute status for the currently active output + + + +#### mute + +```python +@plugin.tag +def mute(mute=True) +``` + +Set mute status for the currently active output + + + +#### set\_output + +```python +@plugin.tag +def set_output(sink_index: int) +``` + +Set the active output (sink_index = 0: primary, 1: secondary) + + + +#### set\_soft\_max\_volume + +```python +@plugin.tag +def set_soft_max_volume(max_volume: int) +``` + +Limit the maximum volume to max_volume for the currently active output + + + +#### get\_soft\_max\_volume + +```python +@plugin.tag +def get_soft_max_volume() +``` + +Return the maximum volume limit for the currently active output + + + +#### card\_list + +```python +def card_list() -> List[pulsectl.PulseCardInfo] +``` + +Return the list of present sound card + + + +# components.rfid + + + +# components.rfid.reader + + + +## RfidCardDetectCallbacks Objects + +```python +class RfidCardDetectCallbacks(CallbackHandler) +``` + +Callbacks are executed if rfid card is detected + + + +#### register + +```python +def register(func: Callable[[str, RfidCardDetectState], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(card_id: str, state: int) + :noindex: + + :param card_id: Card ID + :param state: See :class:`RfidCardDetectState` + + + + +#### run\_callbacks + +```python +def run_callbacks(card_id: str, state: RfidCardDetectState) +``` + +:meta private: + + + +#### rfid\_card\_detect\_callbacks + +Callback handler instance for rfid_card_detect_callbacks events. +See :class:`RfidCardDetectCallbacks` + + + +## CardRemovalTimerClass Objects + +```python +class CardRemovalTimerClass(threading.Thread) +``` + +A timer watchdog thread that calls timeout_action on time-out + + + +#### \_\_init\_\_ + +```python +def __init__(on_timeout_callback, logger: logging.Logger = None) +``` + +**Arguments**: + +- `on_timeout_callback`: The function to execute on time-out + + + +# components.rfid.configure + + + +#### reader\_install\_dependencies + +```python +def reader_install_dependencies(reader_path: str, + dependency_install: str) -> None +``` + +Install dependencies for the selected reader module + +**Arguments**: + +- `reader_path`: Path to the reader module +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies + + + +#### reader\_load\_module + +```python +def reader_load_module(reader_name) +``` + +Load the module for the reader_name + +A ModuleNotFoundError is unrecoverable, but we at least want to give some hint how to resolve that to the user +All other errors will NOT be handled. Modules that do not load due to compile errors have other problems + +**Arguments**: + +- `reader_name`: Name of the reader to load the module for + +**Returns**: + +module + + + +#### query\_user\_for\_reader + +```python +def query_user_for_reader(dependency_install='query') -> dict +``` + +Ask the user to select a RFID reader and prompt for the reader's configuration + +This function performs the following steps, to find and present all available readers to the user + +- search for available reader subpackages +- dynamically load the description module for each reader subpackage +- queries user for selection +- if no_dep_install=False, install dependencies as given by requirements.txt and execute setup.inc.sh of subpackage +- dynamically load the actual reader module from the reader subpackage +- if selected reader has customization options query user for that now +- return configuration + +There are checks to make sure we have the right reader modules and they are what we expect. +The are as few requirements towards the reader module as possible and everything else is optional +(see reader_template for these requirements) +However, there is no error handling w.r.t to user input and reader's query_config. Firstly, in this script +we cannot gracefully handle an exception that occurs on reader level, and secondly the exception will simply +exit the script w/o writing the config to file. No harm done. + +This script expects to reside in the directory with all the reader subpackages, i.e it is part of the rfid-reader package. +Otherwise you'll need to adjust sys.path + +**Arguments**: + +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies + +**Returns**: + +`dict as {section: {parameter: value}}`: nested dict with entire configuration that can be read into ConfigParser + + + +#### write\_config + +```python +def write_config(config_file: str, + config_dict: dict, + force_overwrite=False) -> None +``` + +Write configuration to config_file + +**Arguments**: + +- `config_file`: relative or absolute path to config file +- `config_dict`: nested dict with configuration parameters for ConfigParser consumption +- `force_overwrite`: overwrite existing configuration file without asking + + + +# components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui + + + +# components.rfid.hardware.fake\_reader\_gui.description + + + +# components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon + +Add GPIO input devices and output devices to the RFID Mock Reader GUI + + + +#### create\_inputs + +```python +def create_inputs(frame, default_btn_width, default_padx, default_pady) +``` + +Add all input devies to the GUI + +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to + +**Returns**: + +List of all added GUI buttons + + + +#### set\_state + +```python +def set_state(value, box_state_var) +``` + +Change the value of a checkbox state variable + + + +#### que\_set\_state + +```python +def que_set_state(value, box_state_var) +``` + +Queue the action to change a checkbox state variable to the TK GUI main thread + + + +#### fix\_state + +```python +def fix_state(box_state_var) +``` + +Prevent a checkbox state variable to change on checkbox mouse press + + + +#### pbox\_set\_state + +```python +def pbox_set_state(value, pbox_state_var, label_var) +``` + +Update progress bar state and related state label + + + +#### que\_set\_pbox + +```python +def que_set_pbox(value, pbox_state_var, label_var) +``` + +Queue the action to change the progress bar state to the TK GUI main thread + + + +#### create\_outputs + +```python +def create_outputs(frame, default_btn_width, default_padx, default_pady) +``` + +Add all output devices to the GUI + +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to + +**Returns**: + +List of all added GUI objects + + + +# components.rfid.hardware.generic\_usb.description + + + +# components.rfid.hardware.generic\_usb.generic\_usb + + + +# components.rfid.hardware.rc522\_spi.description + + + +# components.rfid.hardware.rc522\_spi.rc522\_spi + + + +# components.rfid.hardware.pn532\_i2c\_py532.description + + + +# components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532 + + + +# components.rfid.hardware.rdm6300\_serial.rdm6300\_serial + + + +#### decode + +```python +def decode(raw_card_id: bytearray, number_format: int) -> str +``` + +Decode the RDM6300 data format into actual card ID + + + +# components.rfid.hardware.rdm6300\_serial.description + + + +# components.rfid.hardware.template\_new\_reader.description + +Provide a short title for this reader. +This is what that user will see when asked for selecting his RFID reader +So, be precise but readable. Precise means 40 characters or less + + + +# components.rfid.hardware.template\_new\_reader.template\_new\_reader + + + +#### query\_customization + +```python +def query_customization() -> dict +``` + +Query the user for reader parameter customization + +This function will be called during the configuration/setup phase when the user selects this reader module. +It must return all configuration parameters that are necessary to later use the Reader class. +You can ask the user for selections and choices. And/or provide default values. +If your reader requires absolutely no configuration return {} + + + +## ReaderClass Objects + +```python +class ReaderClass(ReaderBaseClass) +``` + +The actual reader class that is used to read RFID cards. + +It will be instantiated once and then read_card() is called in an endless loop. + +It will be used in a manner + with Reader(reader_cfg_key) as reader: + for card_id in reader: + ... +which ensures proper resource de-allocation. For this to work derive this class from ReaderBaseClass. +All the required interfaces are implemented there. + +Put your code into these functions (see below for more information) + - __init__ + - read_card + - cleanup + - stop + + + +#### \_\_init\_\_ + +```python +def __init__(reader_cfg_key) +``` + +In the constructor, you will get the `reader_cfg_key` with which you can access the configuration data + +As you are dealing directly with potentially user-manipulated config information, it is +advisable to do some sanity checks and give useful error messages. Even if you cannot recover gracefully, +a good error message helps :-) + + + +#### cleanup + +```python +def cleanup() +``` + +The cleanup function: free and release all resources used by this card reader (if any). + +Put all your cleanup code here, e.g. if you are using the serial bus or GPIO pins. +Will be called implicitly via the __exit__ function +This function must exist! If there is nothing to do, just leave the pass statement in place below + + + +#### stop + +```python +def stop() +``` + +This function is called to tell the reader to exist it's reading function. + +This function is called before cleanup is called. + +.. note: This is usually called from a different thread than the reader's thread! And this is the reason for the + two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt + to read a card. Once called, the function read_card will not be called again. When the reader thread exits + cleanup is called from the reader thread itself. + + + +#### read\_card + +```python +def read_card() -> str +``` + +Blocking or non-blocking function that waits for a new card to appear and return the card's UID as string + +This is were your main code goes :-) +This function must return a string with the card id +In case of error, it may return None or an empty string + +The function should break and return with an empty string, once stop() is called + + + +# components.rfid.readerbase + + + +## ReaderBaseClass Objects + +```python +class ReaderBaseClass(ABC) +``` + +Abstract Base Class for all Reader Classes to ensure common API + +Look at template_new_reader.py for documentation how to integrate a new RFID reader + + + +# components.rfid.cards + +Handling the RFID card database + +A few considerations: +- Changing the Card DB influences to current state + - rfid.reader: Does not care, as it always freshly looks into the DB when a new card is triggered + - fake_reader_gui: Initializes the Drop-down menu once on start --> Will get out of date! + +Do we need a notifier? Or a callback for modules to get notified? +Do we want to publish the information about a card DB update? +TODO: Add callback for on_database_change + +TODO: check card id type (if int, convert to str) +TODO: check if args is really a list (convert if not?) + + + +#### list\_cards + +```python +@plugs.register +def list_cards() +``` + +Provide a summarized, decoded list of all card actions + +This is intended as basis for a formatter function + +Format: 'id': {decoded_function_call, ignore_same_id_delay, ignore_card_removal_action, description, from_alias} + + + +#### delete\_card + +```python +@plugs.register +def delete_card(card_id: str, auto_save: bool = True) +``` + +**Arguments**: + +- `auto_save`: +- `card_id`: + + + +#### register\_card + +```python +@plugs.register +def register_card(card_id: str, + cmd_alias: str, + args: Optional[List] = None, + kwargs: Optional[Dict] = None, + ignore_card_removal_action: Optional[bool] = None, + ignore_same_id_delay: Optional[bool] = None, + overwrite: bool = False, + auto_save: bool = True) +``` + +Register a new card based on quick-selection + +If you are going to call this through the RPC it will get a little verbose + +**Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume +(*here: 15*) and custom *ignore_same_id_delay value*:: + +plugin.call_ignore_errors('cards', 'register_card', +args=['0009', 'inc_volume'], +kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) + + + +#### register\_card\_custom + +```python +@plugs.register +def register_card_custom() +``` + +Register a new card with full RPC call specification (Not implemented yet) + + + +#### save\_card\_database + +```python +@plugs.register +def save_card_database(filename=None, *, only_if_changed=True) +``` + +Store the current card database. If filename is None, it is saved back to the file it was loaded from + + + +# components.rfid.cardutils + +Common card decoding functions + +TODO: Thread safety when accessing the card DB! + + + +#### decode\_card\_command + +```python +def decode_card_command(cfg_rpc_cmd: Mapping, logger: logging.Logger = log) +``` + +Extension of utils.decode_action with card-specific parameters + + + +#### card\_command\_to\_str + +```python +def card_command_to_str(cfg_rpc_cmd: Mapping, long=False) -> List[str] +``` + +Returns a list of strings with [card_action, ignore_same_id_delay, ignore_card_removal_action] + +The last two parameters are only present, if *long* is True and if they are present in the cfg_rpc_cmd + + + +#### card\_to\_str + +```python +def card_to_str(card_id: str, long=False) -> List[str] +``` + +Returns a list of strings from card entry command in the format of :func:`card_command_to_str` + + + +# components.publishing + +Plugin interface for Jukebox Publisher + +Thin wrapper around jukebox.publishing to benefit from the plugin loading / exit handling / function handling + +This is the first package to be loaded and the last to be closed: put Hello and Goodbye publish messages here. + + + +#### republish + +```python +@plugin.register +def republish(topic=None) +``` + +Re-publish the topic tree 'topic' to all subscribers + +**Arguments**: + +- `topic`: Topic tree to republish. None = resend all + + + +# components.player + + + +## MusicLibPath Objects + +```python +class MusicLibPath() +``` + +Extract the music directory from the mpd.conf file + + + +#### get\_music\_library\_path + +```python +def get_music_library_path() +``` + +Get the music library path + + + +# components.jingle + +Jingle Playback Factory for extensible run-time support of various file types + + + +## JingleFactory Objects + +```python +class JingleFactory() +``` + +Jingle Factory + + + +#### list + +```python +def list() +``` + +List the available volume services + + + +#### play + +```python +@plugin.register +def play(filename) +``` + +Play the jingle using the configured jingle service + +Note: This runs in a separate thread. And this may cause troubles +when changing the volume level before +and after the sound playback: There is nothing to prevent another +thread from changing the volume and sink while playback happens +and afterwards we change the volume back to where it was before! + +There is no way around this dilemma except for not running the jingle as a +separate thread. Currently (as thread) even the RPC is started before the sound +is finished and the volume is reset to normal... + +However: Volume plugin is loaded before jingle and sets the default +volume. No interference here. It can now only happen +if (a) through the RPC or (b) some other plugin the volume is changed. Okay, now +(a) let's hope that there is enough delay in the user requesting a volume change +(b) let's hope no other plugin wants to do that +(c) no bluetooth device connects during this time (and pulseaudio control is set to toggle_on_connect) +and take our changes with the threaded approach. + + + +#### play\_startup + +```python +@plugin.register +def play_startup() +``` + +Play the startup sound (using jingle.play) + + + +#### play\_shutdown + +```python +@plugin.register +def play_shutdown() +``` + +Play the shutdown sound (using jingle.play) + + + +# components.jingle.alsawave + +ALSA wave jingle Service for jingle.JingleFactory + + + +## AlsaWave Objects + +```python +@plugin.register +class AlsaWave() +``` + +Jingle Service for playing wave files directly from Python through ALSA + + + +#### play + +```python +@plugin.tag +def play(filename) +``` + +Play the wave file + + + +## AlsaWaveBuilder Objects + +```python +class AlsaWaveBuilder() +``` + + + +#### \_\_init\_\_ + +```python +def __init__() +``` + +Builder instantiates AlsaWave during init and not during first call because +we want AlsaWave registers as plugin function in any case if this plugin is loaded +(and not only on first use!) + + + +# components.jingle.jinglemp3 + +Generic MP3 jingle Service for jingle.JingleFactory + + + +## JingleMp3Play Objects + +```python +@plugin.register(auto_tag=True) +class JingleMp3Play() +``` + +Jingle Service for playing MP3 files + + + +#### play + +```python +def play(filename) +``` + +Play the MP3 file + + + +## JingleMp3PlayBuilder Objects + +```python +class JingleMp3PlayBuilder() +``` + + + +#### \_\_init\_\_ + +```python +def __init__() +``` + +Builder instantiates JingleMp3Play during init and not during first call because +we want JingleMp3Play registers as plugin function in any case if this plugin is loaded +(and not only on first use!) + + + +# components.hostif.linux + + + +#### shutdown + +```python +@plugin.register +def shutdown() +``` + +Shutdown the host machine + + + +#### reboot + +```python +@plugin.register +def reboot() +``` + +Reboot the host machine + + + +#### jukebox\_is\_service + +```python +@plugin.register +def jukebox_is_service() +``` + +Check if current Jukebox process is running as a service + + + +#### is\_any\_jukebox\_service\_active + +```python +@plugin.register +def is_any_jukebox_service_active() +``` + +Check if a Jukebox service is running + +.. note:: Does not have the be the current app, that is running as a service! + + + +#### restart\_service + +```python +@plugin.register +def restart_service() +``` + +Restart Jukebox App if running as a service + + + +#### get\_disk\_usage + +```python +@plugin.register() +def get_disk_usage(path='/') +``` + +Return the disk usage in Megabytes as dictionary for RPC export + + + +#### get\_cpu\_temperature + +```python +@plugin.register +def get_cpu_temperature() +``` + +Get the CPU temperature with single decimal point + +No error handling: this is expected to take place up-level! + + + +#### get\_ip\_address + +```python +@plugin.register +def get_ip_address() +``` + +Get the IP address + + + +#### wlan\_disable\_power\_down + +```python +@plugin.register() +def wlan_disable_power_down(card=None) +``` + +Turn off power management of wlan. Keep RPi reachable via WLAN + +This must be done after every reboot +card=None takes card from configuration file + + + +#### get\_autohotspot\_status + +```python +@plugin.register +def get_autohotspot_status() +``` + +Get the status of the auto hotspot feature + + + +#### stop\_autohotspot + +```python +@plugin.register() +def stop_autohotspot() +``` + +Stop auto hotspot functionality + +Basically disabling the cronjob and running the script one last time manually + + + +#### start\_autohotspot + +```python +@plugin.register() +def start_autohotspot() +``` + +start auto hotspot functionality + +Basically enabling the cronjob and running the script one time manually + + + +# components.misc + +Miscellaneous function package + + + +#### rpc\_cmd\_help + +```python +@plugin.register +def rpc_cmd_help() +``` + +Return all commands for RPC + + + +#### get\_all\_loaded\_packages + +```python +@plugin.register +def get_all_loaded_packages() +``` + +Get all successfully loaded plugins + + + +#### get\_all\_failed\_packages + +```python +@plugin.register +def get_all_failed_packages() +``` + +Get all plugins with error during load or initialization + + + +#### get\_start\_time + +```python +@plugin.register +def get_start_time() +``` + +Time when JukeBox has been started + + + +#### get\_log + +```python +def get_log(handler_name: str) +``` + +Get the log file from the loggers (debug_file_handler, error_file_handler) + + + +#### get\_log\_debug + +```python +@plugin.register +def get_log_debug() +``` + +Get the log file (from the debug_file_handler) + + + +#### get\_log\_error + +```python +@plugin.register +def get_log_error() +``` + +Get the log file (from the error_file_handler) + + + +#### get\_git\_state + +```python +@plugin.register +def get_git_state() +``` + +Return git state information for the current branch + + + +#### empty\_rpc\_call + +```python +@plugin.register +def empty_rpc_call(msg: str = '') +``` + +This function does nothing. + +The RPC command alias 'none' is mapped to this function. + +This is also used when configuration errors lead to non existing RPC command alias definitions. +When the alias definition is void, we still want to return a valid function to simplify error handling +up the module call stack. + +**Arguments**: + +- `msg`: If present, this message is send to the logger with severity warning + + + +# components.controls + + + +# components.controls.bluetooth\_audio\_buttons + +Plugin to attempt to automatically listen to it's buttons (play, next, ...) +when a bluetooth sound device (headphone, speakers) connects + +This effectively does: + + * register a callback with components.volume to get notified when a new sound card connects + * if that is a bluetooth device, try opening an input device with similar name using + * button listeners are run each in its own thread + + + +# components.controls.common.evdev\_listener + +Generalized listener for ``dev/input`` devices + + + +#### find\_device + +```python +def find_device(device_name: str, + exact_name: bool = True, + mandatory_keys: Optional[Set[int]] = None) -> str +``` + +Find an input device with device_name and mandatory keys. + +Raises + + ``. FileNotFoundError, if no device is found. + ``. AttributeError, if device does not have the mandatory keys + +If multiple devices match, the first match is returned + +**Arguments**: + +- `device_name`: See :func:`_filter_by_device_name` +- `exact_name`: See :func:`_filter_by_device_name` +- `mandatory_keys`: See :func:`_filter_by_mandatory_keys` + +**Returns**: + +The path to the device + + + +## EvDevKeyListener Objects + +```python +class EvDevKeyListener(threading.Thread) +``` + +Opens and event input device from ``/dev/inputs``, and runs callbacks upon the button presses. +Input devices could be .e.g. Keyboard, Bluetooth audio buttons, USB buttons + +Runs as a separate thread. When device disconnects or disappears, thread exists. A new thread must be started +when device re-connects. + +Assign callbacks to :attr:`EvDevKeyListener.button_callbacks` + + + +#### \_\_init\_\_ + +```python +def __init__(device_name_request: str, exact_name: bool, thread_name: str) +``` + +**Arguments**: + +- `device_name_request`: The device name to look for +- `exact_name`: If true, device_name must mach exactly, else a match is returned if device_name is a substring of +the reported device name +- `thread_name`: Name of the listener thread + + + +#### run + +```python +def run() +``` + + + + + +#### start + +```python +def start() -> None +``` + +Start the tread and start listening + + + +# components.music\_cover\_art + +Read all cover art from music save it to a cache for the UI to load + +.. note:: Not implemented. This is a feature planned for a future release. + + + +## MusicCoverArt Objects + +```python +class MusicCoverArt() +``` + + + +#### get\_by\_filename\_as\_base64 + +```python +@plugin.tag +def get_by_filename_as_base64(audio_src: str) +``` + +Not implemented. This is a feature planned for a future release. + + + +# components.battery\_monitor + + + +# components.battery\_monitor.BatteryMonitorBase + + + +## pt1\_frac Objects + +```python +class pt1_frac() +``` + +fixed point first order filter, fractional format: 2^16,2^16 + + + +## BattmonBase Objects + +```python +class BattmonBase() +``` + +Battery Monitor base class + + + +# components.battery\_monitor.batt\_mon\_simulator + + + +## battmon\_simulator Objects + +```python +class battmon_simulator(BatteryMonitorBase.BattmonBase) +``` + +Battery Monitor Simulator + + + +# components.battery\_monitor.batt\_mon\_i2c\_ads1015 + + + +## battmon\_ads1015 Objects + +```python +class battmon_ads1015(BatteryMonitorBase.BattmonBase) +``` + +Battery Monitor based on a ADS1015 + +CAUTION - WARNING +======================================================================== +Lithium and other batteries are dangerous and must be treated with care. +Rechargeable Lithium Ion batteries are potentially hazardous and can +present a serious FIRE HAZARD if damaged, defective or improperly used. +Do not use this circuit to a lithium ion battery without expertise and +training in handling and use of batteries of this type. +Use appropriate test equipment and safety protocols during development. + +There is no warranty, this may not work as expected or at all! +========================================================================= + +This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: + + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === + +Attention: + - the circuit is constantly draining the battery! (leak current up to: 2.1µA) + - the time between sample needs to be a minimum 1sec with this high impedance voltage divider + don't use the continuous conversion method! + + + +# components.gpio.gpioz.plugin + +The GPIOZ plugin interface build all input and output devices from the configuration file and connects +the actions and callbacks. It also provides a very restricted, but common API for the output devices to the RPC. +That API is mainly used for testing. All the relevant output state changes are usually made through callbacks directly +using the output device's API. + + + +#### output\_devices + +List of all created output devices + + + +#### input\_devices + +List of all created input devices + + + +#### factory + +The global pin factory used in this module +Using different pin factories for different devices is not supported + + + +#### IS\_ENABLED + +Indicates that the GPIOZ module is enabled and loaded w/o errors + + + +#### IS\_MOCKED + +Indicates that the pin factory is a mock factory + + + +#### CONFIG\_FILE + +The path of the config file the GPIOZ configuration was loaded from + + + +## ServiceIsRunningCallbacks Objects + +```python +class ServiceIsRunningCallbacks(CallbackHandler) +``` + +Callbacks are executed when + + * Jukebox app started + * Jukebox shuts down + +This is intended to e.g. signal an LED to change state. +This is integrated into this module because: + + * we need the GPIO to control a LED (it must be available when the status callback comes) + * the plugin callback functions provide all the functionality to control the status of the LED + * which means no need to adapt other modules + + + +#### register + +```python +def register(func: Callable[[int], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(status: int) + :noindex: + + :param status: 1 if app started, 0 if app shuts down + + + + +#### run\_callbacks + +```python +def run_callbacks(status: int) +``` + +:meta private: + + + +#### service\_is\_running\_callbacks + +Callback handler instance for service_is_running_callbacks events. +See :class:`ServiceIsRunningCallbacks` + + + +#### build\_output\_device + +```python +def build_output_device(name: str, config: Dict) +``` + +Construct and register a new output device + +In principal all supported GPIOZero output devices can be used. +For all devices a custom functions need to be written to control the state of the outputs + + + +#### build\_input\_device + +```python +def build_input_device(name: str, config) +``` + +Construct and connect a new input device + +Supported input devices are those from gpio.gpioz.core.input_devices + + + +#### get\_output + +```python +def get_output(name: str) +``` + +Get the output device instance based on the configured name + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### on + +```python +@plugin.register +def on(name: str) +``` + +Turn an output device on + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### off + +```python +@plugin.register +def off(name: str) +``` + +Turn an output device off + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### set\_value + +```python +@plugin.register +def set_value(name: str, value: Any) +``` + +Set the output device to :attr:`value` + +**Arguments**: + +- `name`: The alias name output device instance +- `value`: Value to set the device to + + + +#### flash + +```python +@plugin.register +def flash(name, + on_time=1, + off_time=1, + n=1, + *, + fade_in_time=0, + fade_out_time=0, + tone=None, + color=(1, 1, 1)) +``` + +Flash (blink or beep) an output device + +This is a generic function for all types of output devices. Parameters not applicable to an +specific output device are silently ignored + +**Arguments**: + +- `name`: The alias name output device instance +- `on_time`: Time in seconds in state ``ON`` +- `off_time`: Time in seconds in state ``OFF`` +- `n`: Number of flash cycles +- `tone`: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. +- `color`: The RGB color *only for PWMLED*. +- `fade_in_time`: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* +- `fade_out_time`: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* + + + +# components.gpio.gpioz.plugin.connectivity + +Provide connector functions to hook up to some kind of Jukebox functionality and change the output device's state +accordingly. + +Connector functions can often be used for various output devices. Some connector functions are specific to +an output device type. + + + +#### BUZZ\_TONE + +The tone to be used as buzz tone when the buzzer is an active buzzer + + + +#### register\_rfid\_callback + +```python +def register_rfid_callback(device) +``` + +Flash the output device once on successful RFID card detection and thrice if card ID is unknown + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.LED` + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + - :class:`components.gpio.gpioz.core.output_devices.Buzzer` + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_status\_led\_callback + +```python +def register_status_led_callback(device) +``` + +Turn LED on when Jukebox App has started + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.LED` + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + +#### register\_status\_buzzer\_callback + +```python +def register_status_buzzer_callback(device) +``` + +Buzz once when Jukebox App has started, twice when closing down + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.Buzzer` + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_status\_tonalbuzzer\_callback + +```python +def register_status_tonalbuzzer_callback(device) +``` + +Buzz a multi-note melody when Jukebox App has started and when closing down + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_audio\_sink\_change\_callback + +```python +def register_audio_sink_change_callback(device) +``` + +Turn LED on if secondary audio output is selected. If audio output change +fails, blink thrice + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.LED` + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + +#### register\_volume\_led\_callback + +```python +def register_volume_led_callback(device) +``` + +Have a PWMLED change it's brightness according to current volume. LED flashes when minimum or maximum volume +is reached. Minimum value is still a very dimly turned on LED (i.e. LED is never off). + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + + + +#### register\_volume\_buzzer\_callback + +```python +def register_volume_buzzer_callback(device) +``` + +Sound a buzzer once when minimum or maximum value is reached + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.Buzzer` + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_volume\_rgbled\_callback + +```python +def register_volume_rgbled_callback(device) +``` + +Have a :class:`RGBLED` change it's color according to current volume. LED flashes when minimum or maximum volume +is reached. + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + +# components.gpio.gpioz.core.converter + +Provides converter functions/classes for various Jukebox parameters to +values that can be assigned to GPIO output devices + + + +## ColorProperty Objects + +```python +class ColorProperty() +``` + +Color descriptor ensuring valid weight ranges + +:meta private: + + + +## VolumeToRGB Objects + +```python +class VolumeToRGB() +``` + +Converts linear volume level to an RGB color value running through the color spectrum + +**Arguments**: + +- `max_input`: Maximum input value of linear input data +- `offset`: Offset in degrees in the color circle. Color circle +traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) +- `section`: The section of the full color circle to use in degrees +Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 + +.. code-block:: python + + conv = VolumeToRGB(100, offset=120, section=180) + (r, g, b) = conv(50) + +The three components of an RGB LEDs do not have the same luminosity. +Weight factors are used to get a balanced color output + + + +#### \_\_call\_\_ + +```python +def __call__(volume) -> Tuple[float, float, float] +``` + +Perform conversion for single volume level + +**Returns**: + +Tuple(red, green, blue) + + + +#### luminize + +```python +def luminize(r, g, b) +``` + +Apply the color weight factors to the input color values + + + +# components.gpio.gpioz.core.mock + +Changes to the GPIOZero devices for using with the Mock RFID Reader + + + +#### patch\_mock\_outputs\_with\_callback + +```python +def patch_mock_outputs_with_callback() +``` + +Monkey Patch LED + Buzzer to get a callback when state changes + +This targets to represent the state in the TK GUI. +Other output devices cannot be represented in the GUI and are silently ignored. + +..note:: Only for developing purposes! + + + +# components.gpio.gpioz.core.input\_devices + +Provides all supported input devices for the GPIOZ plugin. + +Input devices are based on GPIOZero devices. So for certain configuration parameters, you should +their documentation. + +All callback handlers are replaced by GPIOZ callback handlers. These are usually configured +by using the :func:`set_rpc_actions` each input device exhibits. + +For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Input devices` + + + +## NameMixin Objects + +```python +class NameMixin(ABC) +``` + +Provides name property and RPC decode function + +:meta private: + + + +#### set\_rpc\_actions + +```python +@abstractmethod +def set_rpc_actions(action_config) -> None +``` + +Set all input device callbacks from :attr:`action_config` + +**Arguments**: + +- `action_config`: Dictionary with one +:ref:`RPC Command ` definition entry for every device callback + + + +## EventProperty Objects + +```python +class EventProperty() +``` + +Event callback property + +:meta private: + + + +## ButtonBase Objects + +```python +class ButtonBase(ABC) +``` + +Common stuff for single button devices + +:meta private: + + + +#### value + +```python +@property +def value() +``` + +Returns 1 if the button is currently pressed, and 0 if it is not. + + + +#### pin + +```python +@property +def pin() +``` + +Returns the underlying pin class from GPIOZero. + + + +#### pull\_up + +```python +@property +def pull_up() +``` + +If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. + + + +#### close + +```python +def close() +``` + +Close the device and release the pin + + + +## Button Objects + +```python +class Button(NameMixin, ButtonBase) +``` + +A basic Button that runs a single actions on button press + +**Arguments**: + +- `pull_up` (`bool`): If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. +If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external +resistor must be used and the :attr:`active_state` must be set. +- `active_state` (`bool or None`): If :data:`True`, when the hardware pin state is ``HIGH``, the software +pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when +the hardware pin state is ``HIGH``, the software pin state is ``LOW``. +Use this parameter to set the active state of the underlying pin when +configuring it as not pulled (when *pull_up* is :data:`None`). When +*pull_up* is :data:`True` or :data:`False`, the active state is +automatically set to the proper value. +- `bounce_time` (`float or None`): Specifies the length of time (in seconds) that the component will +ignore changes in state after an initial change. This defaults to +:data:`None` which indicates that no bounce compensation will be +performed. +- `hold_repeat` (`bool`): If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action +is run only once independent of the length of time the button is pressed for. +- `hold_time` (`float`): Time in seconds to wait between invocations of :attr:`on_press`. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file + +.. copied from GPIOZero's documentation: active_state, bounce_time +.. Copyright Ben Nuttall / SPDX-License-Identifier: BSD-3-Clause + + + +#### on\_press + +```python +@property +def on_press() +``` + +The function to run when the device has been pressed + + + +## LongPressButton Objects + +```python +class LongPressButton(NameMixin, ButtonBase) +``` + +A Button that runs a single actions only when the button is pressed long enough + +**Arguments**: + +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action +is run only once independent of the length of time the button is pressed for. +- `hold_time`: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. +Also the time in seconds to wait between invocations of :attr:`on_press`. + + + +#### on\_press + +```python +@on_press.setter +def on_press(func) +``` + +The function to run when the device has been pressed for longer than :attr:`hold_time` + + + +## ShortLongPressButton Objects + +```python +class ShortLongPressButton(NameMixin, ButtonBase) +``` + +A single button that runs two different actions depending if the button is pressed for a short or long time. + +The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press +can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. +But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release +event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run +in this case! + +**Arguments**: + +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ + + + +## RotaryEncoder Objects + +```python +class RotaryEncoder(NameMixin) +``` + +A rotary encoder to run one of two actions depending on the rotation direction. + +**Arguments**: + +- `bounce_time`: See `Button`_ +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ + + + +#### pin\_a + +```python +@property +def pin_a() +``` + +Returns the underlying pin A + + + +#### pin\_b + +```python +@property +def pin_b() +``` + +Returns the underlying pin B + + + +#### on\_rotate\_clockwise + +```python +@property +def on_rotate_clockwise() +``` + +The function to run when the encoder is rotated clockwise + + + +#### on\_rotate\_counter\_clockwise + +```python +@property +def on_rotate_counter_clockwise() +``` + +The function to run when the encoder is rotated counter clockwise + + + +#### close + +```python +def close() +``` + +Close the device and release the pin + + + +## TwinButton Objects + +```python +class TwinButton(NameMixin) +``` + +A two-button device which can run up to six different actions, a.k.a the six function beast. + +Per user press "input" of the TwinButton, only a single callback is executed (but this callback +may be executed several times). +The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press +can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. +But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release +event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run +in this case! + +It is not necessary to configure all actions. + +**Arguments**: + +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored. +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action. A long dual press is never repeated independent of this setting +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ + + + +## StateVar Objects + +```python +class StateVar(Enum) +``` + +State encoding of the Mealy FSM + +:meta private: + + + +#### close + +```python +def close() +``` + +Close the device and release the pins + + + +#### value + +```python +@property +def value() +``` + +2 bit integer indicating if and which button is currently pressed. Button A is the LSB. + + + +#### is\_active + +```python +@property +def is_active() +``` + +:data:`True` if one or both buttons are currently pressed + + + +# components.gpio.gpioz.core.output\_devices + +Provides all supported output devices for the GPIOZ plugin. + +For each device all constructor parameters can be set via the configuration file. Only exceptions +are the :attr:`name` and :attr:`pin_factory` which are set by internal mechanisms. + +The devices a are a relatively thin wrapper around the GPIOZero devices with the same name. +We add a name property to be used for error log message and similar and a :func:`flash` function +to all devices. This function provides a unified API to all devices. This means it can be called for every device +with parameters for this device and optional parameters from another device. Unused/unsupported parameters +are silently ignored. This is done to reduce the amount of coding required for connectivity functions. + +For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Output devices` + + + +## LED Objects + +```python +class LED(NameMixin, gpiozero.LED) +``` + +A binary LED + +**Arguments**: + +- `pin`: The GPIO pin which the LED is connected +- `active_high`: If :data:`true` the output pin will have a high logic level when the device is turned on. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file + + + +#### flash + +```python +def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) +``` + +Exactly like :func:`blink` but restores the original state after flashing the device + +**Arguments**: + +- `on_time` (`float`): Number of seconds on. Defaults to 1 second. +- `off_time` (`float`): Number of seconds off. Defaults to 1 second. +- `n`: Number of times to blink; :data:`None` means forever. +- `background` (`bool`): If :data:`True` (the default), start a background thread to +continue blinking and return immediately. If :data:`False`, only +return when the blink is finished +- `ignored_kwargs`: Ignore all other keywords so this function can be called with identical +parameters also for all other output devices + + + +## Buzzer Objects + +```python +class Buzzer(NameMixin, gpiozero.Buzzer) +``` + + + +#### flash + +```python +def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) +``` + +Flash the device and restore the previous value afterwards + + + +## PWMLED Objects + +```python +class PWMLED(NameMixin, gpiozero.PWMLED) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + n=1, + *, + fade_in_time=0, + fade_out_time=0, + background=True, + **ignored_kwargs) +``` + +Flash the LED and restore the previous value afterwards + + + +## RGBLED Objects + +```python +class RGBLED(NameMixin, gpiozero.RGBLED) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + *, + fade_in_time=0, + fade_out_time=0, + on_color=(1, 1, 1), + off_color=(0, 0, 0), + n=None, + background=True, + **igorned_kwargs) +``` + +Flash the LED with :attr:`on_color` and restore the previous value afterwards + + + +## TonalBuzzer Objects + +```python +class TonalBuzzer(NameMixin, gpiozero.TonalBuzzer) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + n=1, + *, + tone=None, + background=True, + **ignored_kwargs) +``` + +Play the tone :data:`tone` for :attr:`n` times + + + +#### melody + +```python +def melody(on_time=0.2, + off_time=0.05, + *, + tone: Optional[List[Tone]] = None, + background=True) +``` + +Play a melody from the list of tones in :attr:`tone` + + + +# components.timers + + + +# jukebox + + + +# jukebox.callingback + +Provides a generic callback handler + + + +## CallbackHandler Objects + +```python +class CallbackHandler() +``` + +Generic Callback Handler to collect callbacks functions through :func:`register` and execute them + +with :func:`run_callbacks` + +A lock is used to sequence registering of new functions and running callbacks. + +**Arguments**: + +- `name`: A name of this handler for usage in log messages +- `logger`: The logger instance to use for logging +- `context`: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created + + + +#### register + +```python +def register(func: Optional[Callable[..., None]]) +``` + +Register a new function to be executed when the callback event happens + +**Arguments**: + +- `func`: The function to register. If set to :data:`None`, this register request is silently ignored. + + + +#### run\_callbacks + +```python +def run_callbacks(*args, **kwargs) +``` + +Run all registered callbacks. + +*ALL* exceptions from callback functions will be caught and logged only. +Exceptions are not raised upwards! + + + +#### has\_callbacks + +```python +@property +def has_callbacks() +``` + +:data:`True` if there are any registered callbacks. Read-only property + + + +# jukebox.version + + + +#### version + +```python +def version() +``` + +Return the Jukebox version as a string + + + +#### version\_info + +```python +def version_info() +``` + +Return the Jukebox version as a tuple of three numbers + +If this is a development version, an identifier string will be appended after the third integer. + + + +# jukebox.cfghandler + +This module handles global and local configuration data + +The concept is that config handler is created and initialized once in the main thread:: + + cfg = get_handler('global') + load_yaml(cfg, 'filename.yaml') + +In all other modules (in potentially different threads) the same handler is obtained and used by:: + + cfg = get_handler('global') + +This eliminates the need to pass an effectively global configuration handler by parameters across the entire design. +Handlers are identified by their name (in the above example *global*) + +The function :func:`get_handler` is the main entry point to obtain a new or existing handler. + + + +## ConfigHandler Objects + +```python +class ConfigHandler() +``` + +The configuration handler class + +Don't instantiate directly. Always use :func:`get_handler`! + +**Threads:** + +All threads can read and write to the configuration data. +**Proper thread-safeness must be ensured** by the the thread modifying the data by acquiring the lock +Easiest and best way is to use the context handler:: + + with cfg: + cfg['key'] = 66 + cfg.setndefault('hello', value='world') + +For a single function call, this is done implicitly. In this case, there is no need +to explicitly acquire the lock. + +Alternatively, you can lock and release manually by using :func:`acquire` and :func:`release` +But be very sure to release the lock even in cases of errors an exceptions! +Else we have a deadlock. + +Reading may be done without acquiring a lock. But be aware that when reading multiple values without locking, another +thread may intervene and modify some values in between! So, locking is still recommended. + + + +#### loaded\_from + +```python +@property +def loaded_from() -> Optional[str] +``` + +Property to store filename from which the config was loaded + + + +#### get + +```python +def get(key, *, default=None) +``` + +Enforce keyword on default to avoid accidental misuse when actually getn is wanted + + + +#### setdefault + +```python +def setdefault(key, *, value) +``` + +Enforce keyword on default to avoid accidental misuse when actually setndefault is wanted + + + +#### getn + +```python +def getn(*keys, default=None) +``` + +Get the value at arbitrary hierarchy depth. Return ``default`` if key not present + +The *default* value is returned no matter at which hierarchy level the path aborts. +A hierarchy is considered as any type with a :func:`get` method. + + + +#### setn + +```python +def setn(*keys, value, hierarchy_type=None) -> None +``` + +Set the ``key: value`` pair at arbitrary hierarchy depth + +All non-existing hierarchy levels are created. + +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used + + + +#### setndefault + +```python +def setndefault(*keys, value, hierarchy_type=None) +``` + +Set the ``key: value`` pair at arbitrary hierarchy depth unless the key already exists + +All non-existing hierarchy levels are created. + +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The default value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used + +**Returns**: + +The actual value or or the default value if key does not exit + + + +#### config\_dict + +```python +def config_dict(data) +``` + +Initialize configuration data from dict-like data structure + +**Arguments**: + +- `data`: configuration data + + + +#### is\_modified + +```python +def is_modified() -> bool +``` + +Check if the data has changed since the last load/store + +.. note: This relies on the *__str__* representation of the underlying data structure + In case of ruamel, this ignores comments and only looks at the data + + + +#### clear\_modified + +```python +def clear_modified() -> None +``` + +Sets the current state as new baseline, clearing the is_modified state + + + +#### save + +```python +def save(only_if_changed: bool = False) -> None +``` + +Save config back to the file it was loaded from + +If you want to save to a different file, use :func:`write_yaml`. + + + +#### load + +```python +def load(filename: str) -> None +``` + +Load YAML config file into memory + + + +#### get\_handler + +```python +def get_handler(name: str) -> ConfigHandler +``` + +Get a configuration data handler with the specified name, creating it + +if it doesn't yet exit. If created, it is always created empty. + +This is the main entry point for obtaining an configuration handler + +**Arguments**: + +- `name`: Name of the config handler + +**Returns**: + +`ConfigHandler`: The configuration data handler for *name* + + + +#### load\_yaml + +```python +def load_yaml(cfg: ConfigHandler, filename: str) -> None +``` + +Load a yaml file into a ConfigHandler + +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to yaml file + +**Returns**: + +None + + + +#### write\_yaml + +```python +def write_yaml(cfg: ConfigHandler, + filename: str, + only_if_changed: bool = False, + *args, + **kwargs) -> None +``` + +Writes ConfigHandler data to yaml file / sys.stdout + +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to output file. If *sys.stdout*, output is written to console +- `only_if_changed`: Write file only, if ConfigHandler.is_modified() +- `args`: passed on to yaml.dump(...) +- `kwargs`: passed on to yaml.dump(...) + +**Returns**: + +None + + + +# jukebox.playlistgenerator + +Playlists are build from directory content in the following way: +a directory is parsed and files are added to the playlist in the following way + +1. files are added in alphabetic order +2. files ending with ``*livestream.txt`` are unpacked and the containing URL(s) are added verbatim to the playlist +3. files ending with ``*podcast.txt`` are unpacked and the containing Podcast URL(s) are expanded and added to the playlist +4. files ending with ``*.m3u`` are treated as folder playlist. Regular folder processing is suspended and the playlist + is build solely from the ``*.m3u`` content. Only the alphabetically first ``*.m3u`` is processed. URLs are added verbatim + to the playlist except for ``*.xml`` and ``*.podcast`` URLS, which are expanded first + +An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. + +.. code-block:: bash + + 01-livestream.txt + 02-livestream.txt + music.mp3 + podcast.txt + +All files are treated as music files and are added to the playlist, except those: + + * starting with ``.``, + * not having a file ending, i.e. do not contain a ``.``, + * ending with ``.txt``, + * ending with ``.m3u``, + * ending with one of the excluded file endings in :attr:`PlaylistCollector._exclude_endings` + +In recursive mode, the playlist is generated by concatenating all sub-folder playlists. Sub-folders are parsed +in alphabetic order. Symbolic links are being followed. The above rules are enforced on a per-folder bases. +This means, one ``*.m3u`` file per sub-folder is processed (if present). + +In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. + + + +#### TYPE\_DECODE + +Types if file entires in parsed directory + + + +## PlaylistCollector Objects + +```python +class PlaylistCollector() +``` + +Build a playlist from directory(s) + +This class is intended to be used with an absolute path to the music library:: + + plc = PlaylistCollector('/home/chris/music') + plc.parse('Traumfaenger') + print(f"res = {plc}") + +But it can also be used with relative paths from current working directory:: + + plc = PlaylistCollector('.') + plc.parse('../../../../music/Traumfaenger') + print(f"res = {plc}") + +The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. +If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. + + + +#### \_\_init\_\_ + +```python +def __init__(music_library_base_path='/') +``` + +Initialize the playlist generator with music_library_base_path + +**Arguments**: + +- `music_library_base_path`: Base path the the music library. This is used to locate the file in the disk +but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir + + + +#### set\_exclusion\_endings + +```python +@classmethod +def set_exclusion_endings(cls, endings: List[str]) +``` + +Set the class-wide file ending exclusion list + +See :attr:`PlaylistCollector._exclude_endings` + + + +#### get\_directory\_content + +```python +def get_directory_content(path='.') +``` + +Parse the folder ``path`` and create a content list. Depth is always the current level + +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` + +**Returns**: + +[ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] +where type is one of :attr:`TYPE_DECODE` + + + +#### parse + +```python +def parse(path='.', recursive=False) +``` + +Parse the folder ``path`` and create a playlist from it's content + +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` +- `recursive`: Parse folder recursivley, or stay in top-level folder + + + +# jukebox.NvManager + + + +# jukebox.publishing + + + +#### get\_publisher + +```python +def get_publisher() +``` + +Return the publisher instance for this thread + +Per thread, only one publisher instance is required to connect to the inproc socket. +A new instance is created if it does not already exist. + +If there is a remote-chance that your function publishing something may be called form +different threads, always make a fresh call to ``get_publisher()`` to get the correct instance for the current thread. + +Example:: + +import jukebox.publishing as publishing + +class MyClass: +def __init__(self): +pass + +def say_hello(name): +publishing.get_publisher().send('hello', f'Hi {name}, howya?') + +To stress what **NOT** to do: don't get a publisher instance in the constructor and save it to ``self._pub``. +If you do and ``say_hello`` gets called from different threads, the publisher of the thread which instantiated the class +will be used. + +If you need your very own private Publisher Instance, you'll need to instantiate it yourself. +But: the use cases are very rare for that. I cannot think of one at the moment. + +**Remember**: Don’t share ZeroMQ sockets between threads. + + + +# jukebox.publishing.subscriber + + + +# jukebox.publishing.server + +Publishing Server +******************** + +The common publishing server for the entire Jukebox using ZeroMQ + +Structure +---------------- + +.. code-block:: text + ++-----------------------+ +| functional interface | Publisher +| | - functional interface for single Thread +| PUB | - sends data to publisher (and thus across threads) ++-----------------------+ +| (1) +v ++-----------------------+ +| SUB (bind) | PublishServer +| | - Last Value (LV) Cache +| XPUB (bind) | - Subscriber notification and LV resend ++-----------------------+ - independent thread +| (2) +v + +Connection (1): Internal connection +Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) + +Protocol: Multi-part message + +Part 1: Topic (in topic tree format) +E.g. player.status.elapsed + +Part 2: Payload or Message in json serialization +If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same + +Part 3: Command +Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer +and the message is not forwarded to the outside. This third part of the message is never forwarded + +Connection (2): External connection +Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! +Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will +also get you all the branch topics. To get everything, subscribe to ``b''`` + +Protocol: Multi-part message + +Part 1: Topic (in topic tree format) +E.g. player.status.elapsed + +Part 2: Payload or Message in json serialization +If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) + +Why? Why? +------------- + +Check out the `ZeroMQ Documentation `_ +for why you need a proxy in a good design. + +For use case, we made a few simplifications + +Design Rationales +------------------- + +* "If you need `millions of messages per second `_ +sent to thousands of points, +you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." +* "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then +the `XSUB and XPUB `_" +* "Let’s imagine `our feed has an average of 100,000 100-byte messages a second +`_ [...]. +While 100K messages a second is easy for a ZeroMQ application, ..." + +**But we have:** + +* few dozen subscribers --> Check! +* limited number of topics --> Check! +* max ~10 messages per second --> Check! +* small common state information --> Check! +* only the server updates the state --> Check! + +This means, we can use less complex patters than used for these high-speed, high code count, high data rate networks :-) + +* XPUB / XSUB to detect new subscriber +* Cache the entire state in the publisher +* Re-send the entire state on-demand (and then even to every subscriber) +* Using the same channel: sends state to every subscriber + +**Reliability considerations** + +* Late joining client (or drop-off and re-join): get full state update +* Server crash etc: No special handling necessary, we are simple +and don't need recovery in this case. Server will publish initial state +after re-start +* Subscriber too slow: Subscribers problem (TODO: Do we need to do anything about it?) + +**Start-up sequence:** + +* Publisher plugin is first plugin to be loaded +* Due to Publisher - PublisherServer structure no further sequencing required + +Plugin interactions and usage +------------------------------ + +RPC can trigger through function call in components/publishing plugin that + +* entire state is re-published (from the cache) +* a specific topic tree is re-published (from the cache) + +Plugins publishing state information should publish initial state at @plugin.finalize + +.. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is +required per thread. But the publisher instance **must** be thread-local! +Always go through :func:`publishing.get_publisher()`. + +**Sockets** + +Three sockets are opened: + +``. TCP (on a configurable port) +``. Websocket (on a configurable port) +``. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules +that want to know about the current state on event based updates. + +**Further ZeroMQ References:** + +* `Working with Messages `_ +* `Multiple Threads `_ + + + +## PublishServer Objects + +```python +class PublishServer(threading.Thread) +``` + +The publish proxy server that collects and caches messages from all internal publishers and +forwards them to the outside world + +Handles new subscriptions by sending out the entire cached state to **all** subscribers + +The code is structures using a `Reactor Pattern `_ + + + +#### run + +```python +def run() +``` + +Thread's activity + + + +#### handle\_message + +```python +def handle_message(msg) +``` + +Handle incoming messages + + + +#### handle\_subscription + +```python +def handle_subscription(msg) +``` + +Handle new subscribers + + + +## Publisher Objects + +```python +class Publisher() +``` + +The publisher that provides the functional interface to the application + +.. note:: + * An instance must not be shared across threads! + * One instance per thread is enough + + + +#### \_\_init\_\_ + +```python +def __init__(check_thread_owner=True) +``` + +**Arguments**: + +- `check_thread_owner`: Check if send() is always called from the correct thread. This is debug feature +and is intended to expose the situation before it leads to real trouble. Leave it on! + + + +#### send + +```python +def send(topic: str, payload) +``` + +Send out a message for topic + + + +#### revoke + +```python +def revoke(topic: str) +``` + +Revoke a single topic element (not a topic tree!) + + + +#### resend + +```python +def resend(topic: Optional[str] = None) +``` + +Instructs the PublishServer to resend current status to all subscribers + +Not necessary to call after incremental updates or new subscriptions - that will happen automatically! + + + +#### close\_server + +```python +def close_server() +``` + +Instructs the PublishServer to close itself down + + + +# jukebox.daemon + + + +#### log\_active\_threads + +```python +@atexit.register +def log_active_threads() +``` + +This functions is registered with atexit very early, meaning it will be run very late. It is the best guess to +evaluate which Threads are still running (and probably shouldn't be) + +This function is registered before all the plugins and their dependencies are loaded + + + +## JukeBox Objects + +```python +class JukeBox() +``` + + + +#### signal\_handler + +```python +def signal_handler(esignal, frame) +``` + +Signal handler for orderly shutdown + +On first Ctrl-C (or SIGTERM) orderly shutdown procedure is embarked upon. It gets allocated a time-out! +On third Ctrl-C (or SIGTERM), this is interrupted and there will be a hard exit! + + + +# jukebox.plugs + +A plugin package with some special functionality + +Plugins packages are python packages that are dynamically loaded. From these packages only a subset of objects is exposed +through the plugs.call interface. The python packages can use decorators or dynamic function call to register (callable) +objects. + +The python package name may be different from the name the package is registered under in plugs. This allows to load different +python packages for a specific feature based on a configuration file. Note: Python package are still loaded as regular +python packages and can be accessed by normal means + +If you want to provide additional functionality to the same feature (probably even for run-time switching) +you can implement a Factory Pattern using this package. Take a look at volume.py as an example. + +**Example:** Decorate a function for auto-registering under it's own name:: + +import jukebox.plugs as plugs +@plugs.register +def func1(param): +pass + +**Example:** Decorate a function for auto-registering under a new name:: + +@plugs.register(name='better_name') +def func2(param): +pass + +**Example:** Register a function during run-time under it's own name:: + +def func3(param): +pass +plugs.register(func3) + +**Example:** Register a function during run-time under a new name:: + +def func4(param): +pass +plugs.register(func4, name='other_name', package='other_package') + +**Example:** Decorate a class for auto registering during initialization, +including all methods (see _register_class for more info):: + +@plugs.register(auto_tag=True) +class MyClass1: +pass + +**Example:** Register a class instance, from which only report is a callable method through the plugs interface:: + +class MyClass2: +@plugs.tag +def report(self): +pass +myinst2 = MyClass2() +plugin.register(myinst2, name='myinst2') + +Naming convention: + +package +1. Either a python package +2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) + +plugin +1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) +2. The string name to above object + +name +The string name of the plugin object for registration + +method +1. In case the object is a class instance a bound method to call from the class instance +2. The string name to above object + + + +## PluginPackageClass Objects + +```python +class PluginPackageClass() +``` + +A local data class for holding all information about a loaded plugin package + + + +#### register + +```python +@overload +def register(plugin: Callable) -> Callable +``` + +1-level decorator around a function + + + +#### register + +```python +@overload +def register(plugin: Type) -> Any +``` + +Signature: 1-level decorator around a class + + + +#### register + +```python +@overload +def register(*, name: str, package: Optional[str] = None) -> Callable +``` + +Signature: 2-level decorator around a function + + + +#### register + +```python +@overload +def register(*, auto_tag: bool = False, package: Optional[str] = None) -> Type +``` + +Signature: 2-level decorator around a class + + + +#### register + +```python +@overload +def register(plugin: Callable[..., Any] = None, + *, + name: Optional[str] = None, + package: Optional[str] = None, + replace: bool = False) -> Callable +``` + +Signature: Run-time registration of function / class instance / bound method + + + +#### register + +```python +def register(plugin: Optional[Callable] = None, + *, + name: Optional[str] = None, + package: Optional[str] = None, + replace: bool = False, + auto_tag: bool = False) -> Callable +``` + +A generic decorator / run-time function to register plugin module callables + +The functions comes in five distinct signatures for 5 use cases: + +1. ``@plugs.register``: decorator for a class w/o any arguments +2. ``@plugs.register``: decorator for a function w/o any arguments +3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments +4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments +5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of + + * function + * bound method + * class instance + +For more documentation see the functions + + * :func:`_register_obj` + * :func:`_register_class` + +See the examples in Module :mod:`plugs` how to use this decorator / function + +**Arguments**: + +- `plugin`: +- `name`: +- `package`: +- `replace`: +- `auto_tag`: + + + +#### tag + +```python +def tag(func: Callable) -> Callable +``` + +Method decorator for tagging a method as callable through the plugs interface + +Note that the instantiated class must still be registered as plugin object +(either with the class decorator or dynamically) + +**Arguments**: + +- `func`: function to decorate + +**Returns**: + +the function + + + +#### initialize + +```python +def initialize(func: Callable) -> Callable +``` + +Decorator for functions that shall be called by the plugs package directly after the module is loaded + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### finalize + +```python +def finalize(func: Callable) -> Callable +``` + +Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### atexit + +```python +def atexit(func: Callable[[int], Any]) -> Callable[[int], Any] +``` + +Decorator for functions that shall be called by the plugs package directly after at exit of program. + +.. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called + during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your + shutdown handler. + +The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) +It is intended for passing down the signal number that initiated the program termination + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### load + +```python +def load(package: str, + load_as: Optional[str] = None, + prefix: Optional[str] = None) +``` + +Loads a python package as plugin package + +Executes a regular python package load. That means a potentially existing __init__.py is executed. +Decorator @register can by used to register functions / classes / class istances as plugin callable +Decorator @initializer can be used to tag functions that shall be called after package loading +Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded +Instead of using @initializer, you may of course use __init__.py + +Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under +which they are loaded as plugin package also. + +**Arguments**: + +- `package`: Python package to load as plugin package +- `load_as`: Plugin package registration name. If None the name is the python's package simple name +- `prefix`: Prefix to python package to create fully qualified name. This is used only to locate the python package +and ignored otherwise. Useful if all the plugin module are in a dedicated folder + + + +#### load\_all\_named + +```python +def load_all_named(packages_named: Mapping[str, str], + prefix: Optional[str] = None, + ignore_errors=False) +``` + +Load all packages in packages_named with mapped names + +**Arguments**: + +- `packages_named`: Dict[load_as, package] + + + +#### load\_all\_unnamed + +```python +def load_all_unnamed(packages_unnamed: Iterable[str], + prefix: Optional[str] = None, + ignore_errors=False) +``` + +Load all packages in packages_unnamed with default names + + + +#### load\_all\_finalize + +```python +def load_all_finalize(ignore_errors=False) +``` + +Calls all functions registered with @finalize from all loaded modules in the order they were loaded + +This must be executed after the last plugin package is loaded + + + +#### close\_down + +```python +def close_down(**kwargs) -> Any +``` + +Calls all functions registered with @atexit from all loaded modules in reverse order of module load order + +Modules are processed in reverse order. Several at-exit tagged functions of a single module are processed +in the order of registration. + +Errors raised in functions are suppressed to ensure all plugins are processed + + + + +#### call + +```python +def call(package: str, + plugin: str, + method: Optional[str] = None, + *, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None) -> Any +``` + +Call a function/method from the loaded plugins + +If a plugin is a function or a callable instance of a class, this is equivalent to + +``package.plugin(*args, **kwargs)`` + +If plugin is a class instance from which a method is called, this is equivalent to the followig. +Also remember, that method must have the attribute ``plugin_callable = True`` + +``package.plugin.method(*args, **kwargs)`` + +Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. + +.. note:: + There is no logger in this function as they all belong up-level where the exceptions are handled. + If you want logger messages instead of exceptions, use :func:`call_ignore_errors` + +**Arguments**: + +- `package`: Name of the plugin package in which to look for function/class instance +- `plugin`: Function name or instance name of a class +- `method`: Method name when accessing a class instance' method. Leave at *None* if unneeded. +- `as_thread`: Run the callable in separate daemon thread. +There is no return value from the callable in this case! The return value is the thread object. +Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. +All threads are started as daemon threads with terminate upon main program termination. +There is not stop-thread mechanism. This is intended for short lived threads. +- `thread_name`: Name of the thread +- `args`: Arguments passed to callable +- `kwargs`: Keyword arguments passed to callable + +**Returns**: + +The return value from the called function, or, if started as thread the thread object + + + +#### call\_ignore\_errors + +```python +def call_ignore_errors(package: str, + plugin: str, + method: Optional[str] = None, + *, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None) -> Any +``` + +Call a function/method from the loaded plugins ignoring all raised Exceptions. + +Errors get logged. + +See :func:`call` for parameter documentation. + + + +#### exists + +```python +def exists(package: str, + plugin: Optional[str] = None, + method: Optional[str] = None) -> bool +``` + +Check if an object is registered within the plugs package + + + +#### get + +```python +def get(package: str, + plugin: Optional[str] = None, + method: Optional[str] = None) -> Any +``` + +Get a plugs-package registered object + +The return object depends on the number of parameters + +* 1 argument: Get the python module reference for the plugs *package* +* 2 arguments: Get the plugin reference for the plugs *package.plugin* +* 3 arguments: Get the plugin reference for the plugs *package.plugin.method* + + + +#### loaded\_as + +```python +def loaded_as(module_name: str) -> str +``` + +Return the plugin name a python module is loaded as + + + +#### delete + +```python +def delete(package: str, plugin: Optional[str] = None, ignore_errors=False) +``` + +Delete a plugin object from the registered plugs callables + +Note: This does not 'unload' the python module. It merely makes it un-callable via plugs! + + + +#### dump\_plugins + +```python +def dump_plugins(stream) +``` + +Write a human readable summary of all plugin callables to stream + + + +#### summarize + +```python +def summarize() +``` + +Create a reference summary of all plugin callables in dictionary format + + + +#### generate\_help\_rst + +```python +def generate_help_rst(stream) +``` + +Write a reference of all plugin callables in Restructured Text format + + + +#### get\_all\_loaded\_packages + +```python +def get_all_loaded_packages() -> Dict[str, str] +``` + +Report a short summary of all loaded packages + +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` + + + +#### get\_all\_failed\_packages + +```python +def get_all_failed_packages() -> Dict[str, str] +``` + +Report those packages that did not load error free + +.. note:: Package could fail to load + + 1. altogether: these package are not registered + 2. partially: during initializer, finalizer functions: The package is loaded, + but the function did not execute error-free + + Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED + +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` + + + +# jukebox.speaking\_text + +Text to Speech. Plugin to speak any given text via speaker + + + +# jukebox.multitimer + +Multitimer Module + + + +## MultiTimer Objects + +```python +class MultiTimer(threading.Thread) +``` + +Call a function after a specified number of seconds, repeat that iteration times + +May be cancelled during any of the wait times. +Function is called with keyword parameter 'iteration' (which decreases down to 0 for the last iteration) + +If iterations is negative, an endlessly repeating timer is created (which needs to be cancelled with cancel()) + +Initiates start and publishing by calling self.publish_callback + +Note: Inspired by threading.Timer and generally using the same API + + + +#### cancel + +```python +def cancel() +``` + +Stop the timer if it hasn't finished all iterations yet. + + + +## GenericTimerClass Objects + +```python +class GenericTimerClass() +``` + +Interface for plugin / RPC accessibility for a single event timer + + + +#### \_\_init\_\_ + +```python +def __init__(name, wait_seconds: float, function, args=None, kwargs=None) +``` + +**Arguments**: + +- `wait_seconds`: The time in seconds to wait before calling function +- `function`: The function to call with args and kwargs. +- `args`: Parameters for function call +- `kwargs`: Parameters for function call + + + +#### start + +```python +@plugin.tag +def start(wait_seconds=None) +``` + +Start the timer (with default or new parameters) + + + +#### cancel + +```python +@plugin.tag +def cancel() +``` + +Cancel the timer + + + +#### toggle + +```python +@plugin.tag +def toggle() +``` + +Toggle the activation of the timer + + + +#### trigger + +```python +@plugin.tag +def trigger() +``` + +Trigger the next target execution before the time is up + + + +#### is\_alive + +```python +@plugin.tag +def is_alive() +``` + +Check if timer is active + + + +#### get\_timeout + +```python +@plugin.tag +def get_timeout() +``` + +Get the configured time-out + +**Returns**: + +The total wait time. (Not the remaining wait time!) + + + +#### set\_timeout + +```python +@plugin.tag +def set_timeout(wait_seconds: float) +``` + +Set a new time-out in seconds. Re-starts the timer if already running! + + + +#### publish + +```python +@plugin.tag +def publish() +``` + +Publish the current state and config + + + +#### get\_state + +```python +@plugin.tag +def get_state() +``` + +Get the current state and config as dictionary + + + +## GenericEndlessTimerClass Objects + +```python +class GenericEndlessTimerClass(GenericTimerClass) +``` + +Interface for plugin / RPC accessibility for an event timer call function endlessly every m seconds + + + +## GenericMultiTimerClass Objects + +```python +class GenericMultiTimerClass(GenericTimerClass) +``` + +Interface for plugin / RPC accessibility for an event timer that performs an action n times every m seconds + + + +#### \_\_init\_\_ + +```python +def __init__(name, + iterations: int, + wait_seconds_per_iteration: float, + callee, + args=None, + kwargs=None) +``` + +**Arguments**: + +- `iterations`: Number of times callee is called +- `wait_seconds_per_iteration`: Wait in seconds before each iteration +- `callee`: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). +Then with every time out iteration __call__(*args, iteration=iteration, **kwargs) is called. +'iteration' is the current iteration count in decreasing order! +- `args`: +- `kwargs`: + + + +#### start + +```python +@plugin.tag +def start(iterations=None, wait_seconds_per_iteration=None) +``` + +Start the timer (with default or new parameters) + + + +# jukebox.utils + +Common utility functions + + + +#### decode\_rpc\_call + +```python +def decode_rpc_call(cfg_rpc_call: Dict) -> Optional[Dict] +``` + +Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. + +.. important: Leaves all other parameters in cfg_action untouched or later downstream processing! + +**Arguments**: + +- `cfg_rpc_call`: RPC command as configuration entry + +**Returns**: + +A fully populated deep copy of cfg_rpc_call + + + +#### decode\_rpc\_command + +```python +def decode_rpc_command(cfg_rpc_cmd: Dict, + logger: logging.Logger = log) -> Optional[Dict] +``` + +Decode an RPC Command from a config entry. + +This means + + * Decode RPC command alias (if present) + * Ensure all RPC call parameters have valid default values + +If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call +which emits a misuse warning when called +If an explicitly specified this is not done. However, it is ensured that the returned +dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling +for non-existing RPC commands and we get a clearer error message. + +**Arguments**: + +- `cfg_rpc_cmd`: RPC command as configuration entry +- `logger`: The logger to use + +**Returns**: + +A decoded, fully populated deep copy of cfg_rpc_cmd + + + +#### decode\_and\_call\_rpc\_command + +```python +def decode_and_call_rpc_command(rpc_cmd: Dict, logger: logging.Logger = log) +``` + +Convenience function combining decode_rpc_command and plugs.call_ignore_errors + + + +#### bind\_rpc\_command + +```python +def bind_rpc_command(cfg_rpc_cmd: Dict, + dereference=False, + logger: logging.Logger = log) +``` + +Decode an RPC command configuration entry and bind it to a function + +**Arguments**: + +- `dereference`: Dereference even the call to plugs.call(...) + ``. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with + all checks applied at bind time + ``. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with + all checks applied at bind time. + +Setting deference to True, circumvents the dynamic nature of the plugins: the function to call + must exist at bind time and cannot change. If False, the function to call must only exist at call time. + This can be important during the initialization where package ordering and initialization means that not all + classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls + is circumvented. Use with care! + +**Returns**: + +Callable function w/o parameters which directly runs the RPC command +using plugs.call_ignore_errors + + + +#### rpc\_call\_to\_str + +```python +def rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) -> str +``` + +Return a readable string of an RPC call config + +**Arguments**: + +- `cfg_rpc_call`: RPC call configuration entry +- `with_args`: Return string shall include the arguments of the function + + + +#### generate\_cmd\_alias\_rst + +```python +def generate_cmd_alias_rst(stream) +``` + +Write a reference of all rpc command aliases in Restructured Text format + + + +#### generate\_cmd\_alias\_reference + +```python +def generate_cmd_alias_reference(stream) +``` + +Write a reference of all rpc command aliases in text format + + + +#### get\_git\_state + +```python +def get_git_state() +``` + +Return git state information for the current branch + + + +# jukebox.rpc + + + +# jukebox.rpc.client + + + +# jukebox.rpc.server + +Remote Procedure Call Server (RPC) +************************************* + +Bind to tcp and/or websocket port and translates incoming requests to procedure calls. +Avaiable procedures to call are all functions registered with the plugin package. + +To protocol is loosely based on `jsonrpc `_ + +But with different elements directly relating to the plugin concept and Python function argument options + +.. code-block:: yaml + + { + 'package' : str # The plugin package loaded from python module + 'plugin' : str # The plugin object to be accessed from the package + # (i.e. function or class instance) + 'method' : str # (optional) The method of the class instance + 'args' : [ ] # (optional) Positional arguments as list + 'kwargs' : { } # (optional) Keyword arguments as dictionary + 'as_thread': bool # (optional) start call in separate thread + 'id' : Any # (optional) Round-trip id for response (may not be None) + 'tsp' : Any # (optional) measure and return total processing time for + # the call request (may not be None) + } + +**Response** + +A response will ALWAYS be send, independent of presence of 'id'. This is in difference to the +jsonrpc specification. But this is a ZeroMQB REQ/REP pattern requirement! + +If 'id' is omitted, the response will be 'None'! Unless an error occurred, then the error is returned. +The absence of 'id' indicates that the requester is not interested in the response. +If present, 'id' and 'tsp' may not be None. If they are None, there are treated as if non-existing. + +**Sockets** + +Three sockets are opened + +``. TCP (on a configurable port) +``. Websocket (on a configurable port) +``. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be + call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though + the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which + button triggers what action) + + + +## RpcServer Objects + +```python +class RpcServer() +``` + +The RPC Server Class + + + +#### \_\_init\_\_ + +```python +def __init__(context=None) +``` + +Initialize the connections and bind to the ports + + + +#### run + +```python +def run() +``` + +The main endless loop waiting for requests and forwarding the +call request to the plugin module + diff --git a/run_docstring2markdown.sh b/run_docstring2markdown.sh index 65dad42a6..ac2970612 100755 --- a/run_docstring2markdown.sh +++ b/run_docstring2markdown.sh @@ -16,5 +16,8 @@ lazydocs \ --ignored-modules="ruamel,pulsectl" \ ./src/jukebox +# Run pydoc-markdown +# make sure, directory exists +mkdir ./documentation/developers/docstring-pydoc-markdown # expects pydoc-markdown.yml at working dir pydoc-markdown \ No newline at end of file From 9b97659d803b532201c8e759e8cb795801287c42 Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 22 Dec 2023 09:38:00 +0100 Subject: [PATCH 06/41] fix the path to source code --- .../components.battery_monitor.md | 2 +- .../docstring-lazydocs/components.controls.md | 2 +- .../docstring-lazydocs/components.md | 2 +- .../docstring-lazydocs/components.rfid.md | 2 +- .../components.rfid.readerbase.md | 12 ++--- .../components.rpc_command_alias.md | 2 +- .../components.synchronisation.md | 2 +- .../components.synchronisation.syncutils.md | 10 ++-- .../docstring-lazydocs/jukebox.NvManager.md | 20 ++++---- .../docstring-lazydocs/jukebox.callingback.md | 10 ++-- .../developers/docstring-lazydocs/jukebox.md | 2 +- .../jukebox.playlistgenerator.md | 26 +++++----- .../docstring-lazydocs/jukebox.plugs.md | 50 +++++++++---------- .../docstring-lazydocs/jukebox.rpc.md | 2 +- .../docstring-lazydocs/jukebox.utils.md | 20 ++++---- .../docstring-lazydocs/jukebox.version.md | 6 +-- .../docstring-lazydocs/misc.inputminus.md | 8 +-- .../developers/docstring-lazydocs/misc.md | 8 +-- .../docstring-lazydocs/misc.simplecolors.md | 8 +-- requirements.txt | 2 + run_docstring2markdown.sh | 6 +-- 21 files changed, 102 insertions(+), 100 deletions(-) diff --git a/documentation/developers/docstring-lazydocs/components.battery_monitor.md b/documentation/developers/docstring-lazydocs/components.battery_monitor.md index f14525810..e54396848 100644 --- a/documentation/developers/docstring-lazydocs/components.battery_monitor.md +++ b/documentation/developers/docstring-lazydocs/components.battery_monitor.md @@ -1,6 +1,6 @@ - + # module `components.battery_monitor` diff --git a/documentation/developers/docstring-lazydocs/components.controls.md b/documentation/developers/docstring-lazydocs/components.controls.md index 36d1049da..57b61e430 100644 --- a/documentation/developers/docstring-lazydocs/components.controls.md +++ b/documentation/developers/docstring-lazydocs/components.controls.md @@ -1,6 +1,6 @@ - + # module `components.controls` diff --git a/documentation/developers/docstring-lazydocs/components.md b/documentation/developers/docstring-lazydocs/components.md index feb18a2e0..bef8bf650 100644 --- a/documentation/developers/docstring-lazydocs/components.md +++ b/documentation/developers/docstring-lazydocs/components.md @@ -1,6 +1,6 @@ - + # module `components` diff --git a/documentation/developers/docstring-lazydocs/components.rfid.md b/documentation/developers/docstring-lazydocs/components.rfid.md index bbf9477e4..6dc5a428c 100644 --- a/documentation/developers/docstring-lazydocs/components.rfid.md +++ b/documentation/developers/docstring-lazydocs/components.rfid.md @@ -1,6 +1,6 @@ - + # module `components.rfid` diff --git a/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md b/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md index 12be3db8a..805dba289 100644 --- a/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md +++ b/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md @@ -1,6 +1,6 @@ - + # module `components.rfid.readerbase` @@ -11,14 +11,14 @@ --- - + ## class `ReaderBaseClass` Abstract Base Class for all Reader Classes to ensure common API Look at template_new_reader.py for documentation how to integrate a new RFID reader - + ### method `__init__` @@ -35,7 +35,7 @@ __init__(reader_cfg_key: str, description: str, logger: Logger) --- - + ### method `cleanup` @@ -49,7 +49,7 @@ cleanup() --- - + ### method `read_card` @@ -63,7 +63,7 @@ read_card() --- - + ### method `stop` diff --git a/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md b/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md index cd5b4b889..c001e4cfe 100644 --- a/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md +++ b/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md @@ -1,6 +1,6 @@ - + # module `components.rpc_command_alias` This file provides definitions for RPC command aliases diff --git a/documentation/developers/docstring-lazydocs/components.synchronisation.md b/documentation/developers/docstring-lazydocs/components.synchronisation.md index b7fff4edc..54a208d07 100644 --- a/documentation/developers/docstring-lazydocs/components.synchronisation.md +++ b/documentation/developers/docstring-lazydocs/components.synchronisation.md @@ -1,6 +1,6 @@ - + # module `components.synchronisation` diff --git a/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md b/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md index fd6f37303..6e34601f3 100644 --- a/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md +++ b/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md @@ -1,6 +1,6 @@ - + # module `components.synchronisation.syncutils` @@ -10,7 +10,7 @@ --- - + ## function `clean_foldername` @@ -25,7 +25,7 @@ clean_foldername(lib_path: str, folder: str) → str --- - + ## function `ensure_trailing_slash` @@ -40,7 +40,7 @@ ensure_trailing_slash(path: str) → str --- - + ## function `remove_trailing_slash` @@ -55,7 +55,7 @@ remove_trailing_slash(path: str) → str --- - + ## function `remove_leading_slash` diff --git a/documentation/developers/docstring-lazydocs/jukebox.NvManager.md b/documentation/developers/docstring-lazydocs/jukebox.NvManager.md index e611fd91e..6a79f2aab 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.NvManager.md +++ b/documentation/developers/docstring-lazydocs/jukebox.NvManager.md @@ -1,6 +1,6 @@ - + # module `jukebox.NvManager` @@ -11,14 +11,14 @@ --- - + ## class `nv_manager` - + ### method `__init__` @@ -35,7 +35,7 @@ __init__() --- - + ### method `load` @@ -49,7 +49,7 @@ load(default_filename, type=None) --- - + ### method `save_all` @@ -64,14 +64,14 @@ save_all() --- - + ## class `nv_dict` - + ### method `__init__` @@ -88,7 +88,7 @@ __init__(default_filename=None) --- - + ### method `hash` @@ -102,7 +102,7 @@ hash() --- - + ### method `init_from_json` @@ -116,7 +116,7 @@ init_from_json(path=None, merge=False) --- - + ### method `save_to_json` diff --git a/documentation/developers/docstring-lazydocs/jukebox.callingback.md b/documentation/developers/docstring-lazydocs/jukebox.callingback.md index 63ca7f9c9..262e6f947 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.callingback.md +++ b/documentation/developers/docstring-lazydocs/jukebox.callingback.md @@ -1,6 +1,6 @@ - + # module `jukebox.callingback` Provides a generic callback handler @@ -9,7 +9,7 @@ Provides a generic callback handler --- - + ## class `CallbackHandler` Generic Callback Handler to collect callbacks functions through :func:`register` and execute them with :func:`run_callbacks` @@ -18,7 +18,7 @@ A lock is used to sequence registering of new functions and running callbacks. :param name: A name of this handler for usage in log messages :param logger: The logger instance to use for logging :param context: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created - + ### method `__init__` @@ -41,7 +41,7 @@ __init__(name: str, logger: Logger, context=None) --- - + ### method `register` @@ -55,7 +55,7 @@ Register a new function to be executed when the callback event happens --- - + ### method `run_callbacks` diff --git a/documentation/developers/docstring-lazydocs/jukebox.md b/documentation/developers/docstring-lazydocs/jukebox.md index d97777d88..ebef95c0d 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.md +++ b/documentation/developers/docstring-lazydocs/jukebox.md @@ -1,6 +1,6 @@ - + # module `jukebox` diff --git a/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md b/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md index d30b6bb2d..751374438 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md +++ b/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md @@ -1,6 +1,6 @@ - + # module `jukebox.playlistgenerator` Playlists are build from directory content in the following way: a directory is parsed and files are added to the playlist in the following way @@ -31,7 +31,7 @@ In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. --- - + ## function `decode_podcast_core` @@ -46,7 +46,7 @@ decode_podcast_core(url, playlist) --- - + ## function `decode_podcast` @@ -61,7 +61,7 @@ decode_podcast(filename: str, path, playlist) --- - + ## function `decode_livestream` @@ -76,7 +76,7 @@ decode_livestream(filename: DirEntry, path, playlist) --- - + ## function `decode_musicfile` @@ -91,7 +91,7 @@ decode_musicfile(filename: DirEntry, path, playlist) --- - + ## function `decode_m3u` @@ -106,14 +106,14 @@ decode_m3u(filename: DirEntry, path, playlist: List[PlaylistEntry]) --- - + ## class `PlaylistEntry` - + ### method `__init__` @@ -155,7 +155,7 @@ __init__(filetype: int, name: str, path: str) --- - + ## class `PlaylistCollector` Build a playlist from directory(s) @@ -173,7 +173,7 @@ But it can also be used with relative paths from current working directory: ``` The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. - + ### method `__init__` @@ -190,7 +190,7 @@ Initialize the playlist generator with music_library_base_path --- - + ### method `get_directory_content` @@ -204,7 +204,7 @@ Parse the folder ``path`` and create a content list. Depth is always the current --- - + ### method `parse` @@ -218,7 +218,7 @@ Parse the folder ``path`` and create a playlist from it's content --- - + ### classmethod `set_exclusion_endings` diff --git a/documentation/developers/docstring-lazydocs/jukebox.plugs.md b/documentation/developers/docstring-lazydocs/jukebox.plugs.md index b7cf01966..f97633dbe 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.plugs.md +++ b/documentation/developers/docstring-lazydocs/jukebox.plugs.md @@ -1,6 +1,6 @@ - + # module `jukebox.plugs` A plugin package with some special functionality @@ -59,7 +59,7 @@ method 1. In case the object is a class instance a bound method to call from th --- - + ## function `register` @@ -92,7 +92,7 @@ See the examples in Module :mod:`plugs` how to use this decorator / function --- - + ## function `tag` @@ -109,7 +109,7 @@ Note that the instantiated class must still be registered as plugin object (eith --- - + ## function `initialize` @@ -124,7 +124,7 @@ Decorator for functions that shall be called by the plugs package directly after --- - + ## function `finalize` @@ -139,7 +139,7 @@ Decorator for functions that shall be called by the plugs package directly after --- - + ## function `atexit` @@ -158,7 +158,7 @@ The atexit-functions are called with a single integer argument, which is passed --- - + ## function `load` @@ -177,7 +177,7 @@ Python packages may be loaded under a different plugs package name. Python packa --- - + ## function `load_all_named` @@ -196,7 +196,7 @@ Load all packages in packages_named with mapped names --- - + ## function `load_all_unnamed` @@ -213,7 +213,7 @@ Load all packages in packages_unnamed with default names --- - + ## function `load_all_finalize` @@ -228,7 +228,7 @@ This must be executed after the last plugin package is loaded --- - + ## function `close_down` @@ -245,7 +245,7 @@ Errors raised in functions are suppressed to ensure all plugins are processed :r --- - + ## function `dereference` @@ -266,7 +266,7 @@ dereference( --- - + ## function `call` @@ -303,7 +303,7 @@ Calls are serialized by a thread lock. The thread lock is shared with call_ignor --- - + ## function `call_ignore_errors` @@ -328,7 +328,7 @@ See :func:`call` for parameter documentation. --- - + ## function `exists` @@ -345,7 +345,7 @@ Check if an object is registered within the plugs package --- - + ## function `get` @@ -366,7 +366,7 @@ The return object depends on the number of parameters --- - + ## function `loaded_as` @@ -379,7 +379,7 @@ Return the plugin name a python module is loaded as --- - + ## function `delete` @@ -394,7 +394,7 @@ Note: This does not 'unload' the python module. It merely makes it un-callable v --- - + ## function `dump_plugins` @@ -407,7 +407,7 @@ Write a human readable summary of all plugin callables to stream --- - + ## function `summarize` @@ -420,7 +420,7 @@ Create a reference summary of all plugin callables in dictionary format --- - + ## function `generate_help_rst` @@ -433,7 +433,7 @@ Write a reference of all plugin callables in Restructured Text format --- - + ## function `get_all_loaded_packages` @@ -448,7 +448,7 @@ Report a short summary of all loaded packages --- - + ## function `get_all_failed_packages` @@ -469,12 +469,12 @@ Report those packages that did not load error free --- - + ## class `PluginPackageClass` A local data class for holding all information about a loaded plugin package - + ### method `__init__` diff --git a/documentation/developers/docstring-lazydocs/jukebox.rpc.md b/documentation/developers/docstring-lazydocs/jukebox.rpc.md index 90e0bca43..2af4393ae 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.rpc.md +++ b/documentation/developers/docstring-lazydocs/jukebox.rpc.md @@ -1,6 +1,6 @@ - + # module `jukebox.rpc` diff --git a/documentation/developers/docstring-lazydocs/jukebox.utils.md b/documentation/developers/docstring-lazydocs/jukebox.utils.md index b46361e7e..1bdbbfe40 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.utils.md +++ b/documentation/developers/docstring-lazydocs/jukebox.utils.md @@ -1,6 +1,6 @@ - + # module `jukebox.utils` Common utility functions @@ -11,7 +11,7 @@ Common utility functions --- - + ## function `decode_rpc_call` @@ -28,7 +28,7 @@ Makes sure that the core rpc call parameters have valid default values in cfg_rp --- - + ## function `decode_rpc_command` @@ -52,7 +52,7 @@ If the command alias cannot be decoded correctly, the command is mapped to misc. --- - + ## function `decode_and_call_rpc_command` @@ -68,7 +68,7 @@ Convenience function combining decode_rpc_command and plugs.call_ignore_errors --- - + ## function `bind_rpc_command` @@ -93,7 +93,7 @@ Decode an RPC command configuration entry and bind it to a function --- - + ## function `rpc_call_to_str` @@ -108,7 +108,7 @@ Return a readable string of an RPC call config --- - + ## function `indent` @@ -123,7 +123,7 @@ indent(doc, spaces=4) --- - + ## function `generate_cmd_alias_rst` @@ -136,7 +136,7 @@ Write a reference of all rpc command aliases in Restructured Text format --- - + ## function `generate_cmd_alias_reference` @@ -149,7 +149,7 @@ Write a reference of all rpc command aliases in text format --- - + ## function `get_git_state` diff --git a/documentation/developers/docstring-lazydocs/jukebox.version.md b/documentation/developers/docstring-lazydocs/jukebox.version.md index 7cf86940f..6fc69503f 100644 --- a/documentation/developers/docstring-lazydocs/jukebox.version.md +++ b/documentation/developers/docstring-lazydocs/jukebox.version.md @@ -1,6 +1,6 @@ - + # module `jukebox.version` @@ -16,7 +16,7 @@ --- - + ## function `version` @@ -29,7 +29,7 @@ Return the Jukebox version as a string --- - + ## function `version_info` diff --git a/documentation/developers/docstring-lazydocs/misc.inputminus.md b/documentation/developers/docstring-lazydocs/misc.inputminus.md index 020082bb3..1250ec8d7 100644 --- a/documentation/developers/docstring-lazydocs/misc.inputminus.md +++ b/documentation/developers/docstring-lazydocs/misc.inputminus.md @@ -1,6 +1,6 @@ - + # module `misc.inputminus` Zero 3rd-party dependency module for user prompting @@ -10,7 +10,7 @@ Yes, there are modules out there to do the same and they have more features. How --- - + ## function `input_int` @@ -32,7 +32,7 @@ Request an integer input from user --- - + ## function `input_yesno` @@ -49,7 +49,7 @@ Accepts multiple input for true/false and is case insensitive --- - + ## function `msg_highlight` diff --git a/documentation/developers/docstring-lazydocs/misc.md b/documentation/developers/docstring-lazydocs/misc.md index ea8406996..45acafd14 100644 --- a/documentation/developers/docstring-lazydocs/misc.md +++ b/documentation/developers/docstring-lazydocs/misc.md @@ -1,6 +1,6 @@ - + # module `misc` @@ -10,7 +10,7 @@ --- - + ## function `recursive_chmod` @@ -27,7 +27,7 @@ Reference: https://docs.python.org/3/library/os.html#os.chmod --- - + ## function `flatten` @@ -40,7 +40,7 @@ Flatten all levels of hierarchy in nested iterables --- - + ## function `getattr_hierarchical` diff --git a/documentation/developers/docstring-lazydocs/misc.simplecolors.md b/documentation/developers/docstring-lazydocs/misc.simplecolors.md index 2bc508fa4..7a2352caa 100644 --- a/documentation/developers/docstring-lazydocs/misc.simplecolors.md +++ b/documentation/developers/docstring-lazydocs/misc.simplecolors.md @@ -1,6 +1,6 @@ - + # module `misc.simplecolors` Zero 3rd-party dependency module to add colors to unix terminal output @@ -13,7 +13,7 @@ Yes, there are modules out there to do the same and they have more features. How --- - + ## function `resolve` @@ -28,7 +28,7 @@ Resolve a color name into the respective color constant --- - + ## function `print` @@ -50,7 +50,7 @@ Use just as a regular print function, but with first parameter as color --- - + ## class `Colors` Container class for all the colors as constants diff --git a/requirements.txt b/requirements.txt index a276df63a..6fe10e6a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,5 @@ mock # API docs generation lazydocs +pydocstyle +pydoc-markdown diff --git a/run_docstring2markdown.sh b/run_docstring2markdown.sh index ac2970612..509716b1a 100755 --- a/run_docstring2markdown.sh +++ b/run_docstring2markdown.sh @@ -12,12 +12,12 @@ cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && e lazydocs \ --output-path="./documentation/developers/docstring-lazydocs" \ --overview-file="docstring.md" \ - --src-base-url="https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/future3/develop/src/jukebox/" \ - --ignored-modules="ruamel,pulsectl" \ + --src-base-url="https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/future3/develop/" \ + --ignored-modules=r"ruamel,pulsectl" \ ./src/jukebox # Run pydoc-markdown # make sure, directory exists -mkdir ./documentation/developers/docstring-pydoc-markdown +mkdir -p ./documentation/developers/docstring-pydoc-markdown # expects pydoc-markdown.yml at working dir pydoc-markdown \ No newline at end of file From 845fe221141ff41c8e2c765caa9b17784ab4356c Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 22 Dec 2023 09:51:02 +0100 Subject: [PATCH 07/41] change filter --- .../docstring-pydoc-markdown/docstring.md | 994 ++++++++---------- pydoc-markdown.yml | 2 + 2 files changed, 425 insertions(+), 571 deletions(-) diff --git a/documentation/developers/docstring-pydoc-markdown/docstring.md b/documentation/developers/docstring-pydoc-markdown/docstring.md index b2184d060..72797297d 100644 --- a/documentation/developers/docstring-pydoc-markdown/docstring.md +++ b/documentation/developers/docstring-pydoc-markdown/docstring.md @@ -520,18 +520,13 @@ def input_int(prompt, Request an integer input from user -**Arguments**: - -- `prompt`: The prompt to display -- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid -- `min`: Minimum valid integer value (None disables this check) -- `max`: Maximum valid integer value (None disables this check) -- `prompt_color`: Color of the prompt. Color will be reset at end of prompt -- `prompt_hint`: Append a 'hint' with [min...max, default=xx] to end of prompt - -**Returns**: - -integer value read from user input +:param prompt: The prompt to display +:param blank: Value to return when user just hits enter. Leave at None, if blank is invalid +:param min: Minimum valid integer value (None disables this check) +:param max: Maximum valid integer value (None disables this check) +:param prompt_color: Color of the prompt. Color will be reset at end of prompt +:param prompt_hint: Append a 'hint' with [min...max, default=xx] to end of prompt +:return: integer value read from user input @@ -548,16 +543,11 @@ Request a yes / no choice from user Accepts multiple input for true/false and is case insensitive -**Arguments**: - -- `prompt`: The prompt to display -- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid -- `prompt_color`: Color of the prompt. Color will be reset at end of prompt -- `prompt_hint`: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized - -**Returns**: - -boolean value read from user input +:param prompt: The prompt to display +:param blank: Value to return when user just hits enter. Leave at None, if blank is invalid +:param prompt_color: Color of the prompt. Color will be reset at end of prompt +:param prompt_hint: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized +:return: boolean value read from user input @@ -574,19 +564,19 @@ Hierarchy separator is the '.'. If the logger already exits, getLogger will retu created on the spot. :Example: How to get logger and log away at your heart's content: ->>> import logging ->>> logger = logging.getLogger('jb.awesome_module') ->>> logger.info('Started general awesomeness aura') + >>> import logging + >>> logger = logging.getLogger('jb.awesome_module') + >>> logger.info('Started general awesomeness aura') Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: `` loggers: -jb: -level: WARNING -handlers: [console, debug_file_handler, error_file_handler] -propagate: no -jb.awesome_module: -level: DEBUG + jb: + level: WARNING + handlers: [console, debug_file_handler, error_file_handler] + propagate: no + jb.awesome_module: + level: DEBUG `` .. note:: @@ -619,10 +609,8 @@ Don't forget to {reset} the color settings at the end of the string. def __init__(enable=True, color_levelname=True) ``` -**Arguments**: - -- `enable`: Enable the coloring -- `color_levelname`: Enable auto-coloring when using the levelname keyword +:param enable: Enable the coloring +:param color_levelname: Enable auto-coloring when using the levelname keyword @@ -692,13 +680,8 @@ def resolve(color_name: str) Resolve a color name into the respective color constant -**Arguments**: - -- `color_name`: Name of the color - -**Returns**: - -color constant +:param color_name: Name of the color +:return: color constant @@ -753,7 +736,6 @@ Callback signature is :param folder: relativ path to folder to play :param state: indicator of the state inside the calling - #### run\_callbacks @@ -772,42 +754,42 @@ Package for interfacing with the MPD Music Player Daemon Status information in three topics 1) Player Status: published only on change -This is a subset of the MPD status (and not the full MPD status) ?? -- folder -- song -- volume (volume is published only via player status, and not separatly to avoid too many Threads) -- ... + This is a subset of the MPD status (and not the full MPD status) ?? + - folder + - song + - volume (volume is published only via player status, and not separatly to avoid too many Threads) + - ... 2) Elapsed time: published every 250 ms, unless constant -- elapsed + - elapsed 3) Folder Config: published only on change -This belongs to the folder being played -Publish: -- random, resume, single, loop -On save store this information: -Contains the information for resume functionality of each folder -- random, resume, single, loop -- if resume: -- current song, elapsed -- what is PLAYSTATUS for? -When to save -- on stop -Angstsave: -- on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) -- on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) -Load checks: -- if resume, but no song, elapsed -> log error and start from the beginning + This belongs to the folder being played + Publish: + - random, resume, single, loop + On save store this information: + Contains the information for resume functionality of each folder + - random, resume, single, loop + - if resume: + - current song, elapsed + - what is PLAYSTATUS for? + When to save + - on stop + Angstsave: + - on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) + - on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) + Load checks: + - if resume, but no song, elapsed -> log error and start from the beginning Status storing: -- Folder config for each folder (see above) -- Information to restart last folder playback, which is: -- last_folder -> folder_on_close -- song, elapsed -- random, resume, single, loop -- if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! -on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card + - Folder config for each folder (see above) + - Information to restart last folder playback, which is: + - last_folder -> folder_on_close + - song, elapsed + - random, resume, single, loop + - if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! + on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card Internal status -- last played folder: Needed to detect second swipe + - last played folder: Needed to detect second swipe Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, @@ -815,13 +797,12 @@ Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CUR {'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, 'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} -**References**: +References: +https://github.com/Mic92/python-mpd2 +https://python-mpd2.readthedocs.io/en/latest/topics/commands.html +https://mpd.readthedocs.io/en/latest/protocol.html - https://github.com/Mic92/python-mpd2 - https://python-mpd2.readthedocs.io/en/latest/topics/commands.html - https://mpd.readthedocs.io/en/latest/protocol.html - - sudo -u mpd speaker-test -t wav -c 2 +sudo -u mpd speaker-test -t wav -c 2 @@ -937,10 +918,8 @@ Main entry point for trigger music playing from RFID reader. Decodes second swip Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action accordingly. -**Arguments**: - -- `folder`: Folder path relative to music library path -- `recursive`: Add folder recursively +:param folder: Folder path relative to music library path +:param recursive: Add folder recursively @@ -955,9 +934,7 @@ Get the folder content as content list with meta-information. Depth is always 1. Call repeatedly to descend in hierarchy -**Arguments**: - -- `folder`: Folder path relative to music library path +:param folder: Folder path relative to music library path @@ -973,10 +950,8 @@ Playback a music folder. Folder content is added to the playlist as described by :mod:`jukebox.playlistgenerator`. The playlist is cleared first. -**Arguments**: - -- `folder`: Folder path relative to music library path -- `recursive`: Add folder recursively +:param folder: Folder path relative to music library path +:param recursive: Add folder recursively @@ -992,10 +967,8 @@ Playback a album found in MPD database. All album songs are added to the playlist The playlist is cleared first. -**Arguments**: - -- `albumartist`: Artist of the Album provided by MPD database -- `album`: Album name provided by MPD database +:param albumartist: Artist of the Album provided by MPD database +:param album: Album name provided by MPD database @@ -1081,9 +1054,7 @@ def sync_change_on_rfid_scan(option: str = 'toggle') -> None Change activation of 'on_rfid_scan_enabled' -**Arguments**: - -- `option`: Must be one of 'enable', 'disable', 'toggle' +:param option: Must be one of 'enable', 'disable', 'toggle' @@ -1107,12 +1078,9 @@ def sync_card_database(card_id: str) -> bool ``` Sync the card database from the remote server, if existing. - If card_id is provided only this entry is updated. -**Arguments**: - -- `card_id`: The cardid to update +:param card_id: The cardid to update @@ -1125,9 +1093,7 @@ def sync_folder(folder: str) -> bool Sync the folder from the remote server, if existing -**Arguments**: - -- `folder`: Folder path relative to music library path +:param folder: Folder path relative to music library path @@ -1179,7 +1145,7 @@ from the Jukebox. Remove it from the configuration! .. code-block:: text - ### Use hot-plugged devices like Bluetooth or USB automatically (LP: `1702794`) + ### Use hot-plugged devices like Bluetooth or USB automatically (LP: #1702794) ### not available on PI? .ifexists module-switch-on-connect.so load-module module-switch-on-connect @@ -1195,9 +1161,9 @@ Callbacks: The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): - ``. :func:`add_on_connect_callback` - ``. :func:`add_on_output_change_callbacks` - ``. :func:`add_on_volume_change_callback` + #. :func:`add_on_connect_callback` + #. :func:`add_on_output_change_callbacks` + #. :func:`add_on_volume_change_callback` @@ -1249,7 +1215,6 @@ Callback signature is :param device_name: The sound card device name as reported in device properties - #### run\_callbacks @@ -1350,7 +1315,6 @@ def register(func: Callable[[str, str, int, int], None]) ``` Add a new callback function :attr:`func`. - Parameters always give the valid audio sink. That means, if an error occurred, all parameters are valid. @@ -1366,7 +1330,6 @@ Callback signature is but an error occurred. Above parameters always give the now valid sink! If a sink change is successful, it is 0. - #### run\_callbacks @@ -1408,7 +1371,6 @@ Callback signature is :param is_min: 1, if volume level is minimum, else 0 :param is_max: 1, if volume level is maximum, else 0 - #### run\_callbacks @@ -1597,7 +1559,6 @@ Callback signature is :param card_id: Card ID :param state: See :class:`RfidCardDetectState` - #### run\_callbacks @@ -1633,9 +1594,7 @@ A timer watchdog thread that calls timeout_action on time-out def __init__(on_timeout_callback, logger: logging.Logger = None) ``` -**Arguments**: - -- `on_timeout_callback`: The function to execute on time-out +:param on_timeout_callback: The function to execute on time-out @@ -1652,13 +1611,11 @@ def reader_install_dependencies(reader_path: str, Install dependencies for the selected reader module -**Arguments**: - -- `reader_path`: Path to the reader module -- `dependency_install`: how to handle installing of dependencies -'query': query user (default) -'auto': automatically -'no': don't install dependencies +:param reader_path: Path to the reader module +:parameter dependency_install: how to handle installing of dependencies + 'query': query user (default) + 'auto': automatically + 'no': don't install dependencies @@ -1673,13 +1630,8 @@ Load the module for the reader_name A ModuleNotFoundError is unrecoverable, but we at least want to give some hint how to resolve that to the user All other errors will NOT be handled. Modules that do not load due to compile errors have other problems -**Arguments**: - -- `reader_name`: Name of the reader to load the module for - -**Returns**: - -module +:param reader_name: Name of the reader to load the module for +:return: module @@ -1711,16 +1663,12 @@ exit the script w/o writing the config to file. No harm done. This script expects to reside in the directory with all the reader subpackages, i.e it is part of the rfid-reader package. Otherwise you'll need to adjust sys.path -**Arguments**: - -- `dependency_install`: how to handle installing of dependencies -'query': query user (default) -'auto': automatically -'no': don't install dependencies - -**Returns**: - -`dict as {section: {parameter: value}}`: nested dict with entire configuration that can be read into ConfigParser +:parameter dependency_install: how to handle installing of dependencies + 'query': query user (default) + 'auto': automatically + 'no': don't install dependencies +:return: nested dict with entire configuration that can be read into ConfigParser +:rtype: dict as {section: {parameter: value}} @@ -1734,11 +1682,9 @@ def write_config(config_file: str, Write configuration to config_file -**Arguments**: - -- `config_file`: relative or absolute path to config file -- `config_dict`: nested dict with configuration parameters for ConfigParser consumption -- `force_overwrite`: overwrite existing configuration file without asking +:parameter config_file: relative or absolute path to config file +:parameter config_dict: nested dict with configuration parameters for ConfigParser consumption +:parameter force_overwrite: overwrite existing configuration file without asking @@ -1764,13 +1710,8 @@ def create_inputs(frame, default_btn_width, default_padx, default_pady) Add all input devies to the GUI -**Arguments**: - -- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to - -**Returns**: - -List of all added GUI buttons +:param frame: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to +:return: List of all added GUI buttons @@ -1832,13 +1773,8 @@ def create_outputs(frame, default_btn_width, default_padx, default_pady) Add all output devices to the GUI -**Arguments**: - -- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to - -**Returns**: - -List of all added GUI objects +:param frame: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to +:return: List of all added GUI objects @@ -2053,10 +1989,8 @@ Format: 'id': {decoded_function_call, ignore_same_id_delay, ignore_card_removal_ def delete_card(card_id: str, auto_save: bool = True) ``` -**Arguments**: - -- `auto_save`: -- `card_id`: +:param auto_save: +:param card_id: @@ -2081,9 +2015,9 @@ If you are going to call this through the RPC it will get a little verbose **Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume (*here: 15*) and custom *ignore_same_id_delay value*:: -plugin.call_ignore_errors('cards', 'register_card', -args=['0009', 'inc_volume'], -kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) + plugin.call_ignore_errors('cards', 'register_card', + args=['0009', 'inc_volume'], + kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) @@ -2168,9 +2102,7 @@ def republish(topic=None) Re-publish the topic tree 'topic' to all subscribers -**Arguments**: - -- `topic`: Topic tree to republish. None = resend all +:param topic: Topic tree to republish. None = resend all @@ -2625,9 +2557,7 @@ This is also used when configuration errors lead to non existing RPC command ali When the alias definition is void, we still want to return a valid function to simplify error handling up the module call stack. -**Arguments**: - -- `msg`: If present, this message is send to the logger with severity warning +:param msg: If present, this message is send to the logger with severity warning @@ -2666,20 +2596,15 @@ Find an input device with device_name and mandatory keys. Raises - ``. FileNotFoundError, if no device is found. - ``. AttributeError, if device does not have the mandatory keys + #. FileNotFoundError, if no device is found. + #. AttributeError, if device does not have the mandatory keys If multiple devices match, the first match is returned -**Arguments**: - -- `device_name`: See :func:`_filter_by_device_name` -- `exact_name`: See :func:`_filter_by_device_name` -- `mandatory_keys`: See :func:`_filter_by_mandatory_keys` - -**Returns**: - -The path to the device +:param device_name: See :func:`_filter_by_device_name` +:param exact_name: See :func:`_filter_by_device_name` +:param mandatory_keys: See :func:`_filter_by_mandatory_keys` +:return: The path to the device @@ -2705,12 +2630,10 @@ Assign callbacks to :attr:`EvDevKeyListener.button_callbacks` def __init__(device_name_request: str, exact_name: bool, thread_name: str) ``` -**Arguments**: - -- `device_name_request`: The device name to look for -- `exact_name`: If true, device_name must mach exactly, else a match is returned if device_name is a substring of -the reported device name -- `thread_name`: Name of the listener thread +:param device_name_request: The device name to look for +:param exact_name: If true, device_name must mach exactly, else a match is returned if device_name is a substring of + the reported device name +:param thread_name: Name of the listener thread @@ -2931,7 +2854,6 @@ Callback signature is :param status: 1 if app started, 0 if app shuts down - #### run\_callbacks @@ -2984,9 +2906,7 @@ def get_output(name: str) Get the output device instance based on the configured name -**Arguments**: - -- `name`: The alias name output device instance +:param name: The alias name output device instance @@ -2999,9 +2919,7 @@ def on(name: str) Turn an output device on -**Arguments**: - -- `name`: The alias name output device instance +:param name: The alias name output device instance @@ -3014,9 +2932,7 @@ def off(name: str) Turn an output device off -**Arguments**: - -- `name`: The alias name output device instance +:param name: The alias name output device instance @@ -3029,10 +2945,9 @@ def set_value(name: str, value: Any) Set the output device to :attr:`value` -**Arguments**: +:param name: The alias name output device instance -- `name`: The alias name output device instance -- `value`: Value to set the device to +:param value: Value to set the device to @@ -3056,16 +2971,21 @@ Flash (blink or beep) an output device This is a generic function for all types of output devices. Parameters not applicable to an specific output device are silently ignored -**Arguments**: +:param name: The alias name output device instance + +:param on_time: Time in seconds in state ``ON`` -- `name`: The alias name output device instance -- `on_time`: Time in seconds in state ``ON`` -- `off_time`: Time in seconds in state ``OFF`` -- `n`: Number of flash cycles -- `tone`: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. -- `color`: The RGB color *only for PWMLED*. -- `fade_in_time`: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* -- `fade_out_time`: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* +:param off_time: Time in seconds in state ``OFF`` + +:param n: Number of flash cycles + +:param tone: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. + +:param color: The RGB color *only for PWMLED*. + +:param fade_in_time: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* + +:param fade_out_time: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* @@ -3237,12 +3157,11 @@ class VolumeToRGB() Converts linear volume level to an RGB color value running through the color spectrum -**Arguments**: +:param max_input: Maximum input value of linear input data +:param offset: Offset in degrees in the color circle. Color circle + traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) +:param section: The section of the full color circle to use in degrees -- `max_input`: Maximum input value of linear input data -- `offset`: Offset in degrees in the color circle. Color circle -traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) -- `section`: The section of the full color circle to use in degrees Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 .. code-block:: python @@ -3263,9 +3182,8 @@ def __call__(volume) -> Tuple[float, float, float] Perform conversion for single volume level -**Returns**: - -Tuple(red, green, blue) +:return: Tuple(red, green, blue) +:meta public: @@ -3335,10 +3253,8 @@ def set_rpc_actions(action_config) -> None Set all input device callbacks from :attr:`action_config` -**Arguments**: - -- `action_config`: Dictionary with one -:ref:`RPC Command ` definition entry for every device callback +:param action_config: Dictionary with one + :ref:`RPC Command ` definition entry for every device callback @@ -3417,28 +3333,40 @@ class Button(NameMixin, ButtonBase) A basic Button that runs a single actions on button press -**Arguments**: - -- `pull_up` (`bool`): If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. -If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external -resistor must be used and the :attr:`active_state` must be set. -- `active_state` (`bool or None`): If :data:`True`, when the hardware pin state is ``HIGH``, the software -pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when -the hardware pin state is ``HIGH``, the software pin state is ``LOW``. -Use this parameter to set the active state of the underlying pin when -configuring it as not pulled (when *pull_up* is :data:`None`). When -*pull_up* is :data:`True` or :data:`False`, the active state is -automatically set to the proper value. -- `bounce_time` (`float or None`): Specifies the length of time (in seconds) that the component will -ignore changes in state after an initial change. This defaults to -:data:`None` which indicates that no bounce compensation will be -performed. -- `hold_repeat` (`bool`): If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action -is run only once independent of the length of time the button is pressed for. -- `hold_time` (`float`): Time in seconds to wait between invocations of :attr:`on_press`. -- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file -- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly -through the configuration file +:type pull_up: bool +:param pull_up: If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. + If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external + resistor must be used and the :attr:`active_state` must be set. + +:type active_state: bool or None +:param active_state: + If :data:`True`, when the hardware pin state is ``HIGH``, the software + pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when + the hardware pin state is ``HIGH``, the software pin state is ``LOW``. + Use this parameter to set the active state of the underlying pin when + configuring it as not pulled (when *pull_up* is :data:`None`). When + *pull_up* is :data:`True` or :data:`False`, the active state is + automatically set to the proper value. + +:type bounce_time: float or None +:param bounce_time: + Specifies the length of time (in seconds) that the component will + ignore changes in state after an initial change. This defaults to + :data:`None` which indicates that no bounce compensation will be + performed. + +:type hold_repeat: bool +:param hold_repeat: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action + is run only once independent of the length of time the button is pressed for. + +:type hold_time: float +:param hold_time: Time in seconds to wait between invocations of :attr:`on_press`. + +:param pin_factory: The GPIOZero pin factory. This parameter cannot be set through the configuration file + +:type name: str +:param name: The name of the button for use in error messages. This parameter cannot be set explicitly + through the configuration file .. copied from GPIOZero's documentation: active_state, bounce_time .. Copyright Ben Nuttall / SPDX-License-Identifier: BSD-3-Clause @@ -3464,15 +3392,17 @@ class LongPressButton(NameMixin, ButtonBase) A Button that runs a single actions only when the button is pressed long enough -**Arguments**: +:param pull_up: See `Button`_ + +:param active_state: See `Button`_ + +:param bounce_time: See `Button`_ -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ -- `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action -is run only once independent of the length of time the button is pressed for. -- `hold_time`: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. -Also the time in seconds to wait between invocations of :attr:`on_press`. +:param hold_repeat: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action + is run only once independent of the length of time the button is pressed for. + +:param hold_time: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. + Also the time in seconds to wait between invocations of :attr:`on_press`. @@ -3501,18 +3431,22 @@ But a long press can be identified as soon as :attr:`hold_time` is reached and t event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run in this case! -**Arguments**: +:param pull_up: See `Button`_ + +:param active_state: See `Button`_ + +:param bounce_time: See `Button`_ + +:param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before + this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the + short press action is ignored -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ -- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before -this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the -short press action is ignored -- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press -action -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ +:param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press + action + +:param pin_factory: See `Button`_ + +:param name: See `Button`_ @@ -3524,11 +3458,11 @@ class RotaryEncoder(NameMixin) A rotary encoder to run one of two actions depending on the rotation direction. -**Arguments**: +:param bounce_time: See `Button`_ -- `bounce_time`: See `Button`_ -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ +:param pin_factory: See `Button`_ + +:param name: See `Button`_ @@ -3604,18 +3538,22 @@ in this case! It is not necessary to configure all actions. -**Arguments**: +:param pull_up: See `Button`_ + +:param active_state: See `Button`_ -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ -- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before -this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the -short press action is ignored. -- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press -action. A long dual press is never repeated independent of this setting -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ +:param bounce_time: See `Button`_ + +:param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before + this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the + short press action is ignored. + +:param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press + action. A long dual press is never repeated independent of this setting + +:param pin_factory: See `Button`_ + +:param name: See `Button`_ @@ -3688,13 +3626,15 @@ class LED(NameMixin, gpiozero.LED) A binary LED -**Arguments**: +:param pin: The GPIO pin which the LED is connected + +:param active_high: If :data:`true` the output pin will have a high logic level when the device is turned on. -- `pin`: The GPIO pin which the LED is connected -- `active_high`: If :data:`true` the output pin will have a high logic level when the device is turned on. -- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file -- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly -through the configuration file +:param pin_factory: The GPIOZero pin factory. This parameter cannot be set through the configuration file + +:type name: str +:param name: The name of the button for use in error messages. This parameter cannot be set explicitly + through the configuration file @@ -3706,16 +3646,22 @@ def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) Exactly like :func:`blink` but restores the original state after flashing the device -**Arguments**: +:param float on_time: + Number of seconds on. Defaults to 1 second. + +:param float off_time: + Number of seconds off. Defaults to 1 second. + +:param n: + Number of times to blink; :data:`None` means forever. + +:param bool background: + If :data:`True` (the default), start a background thread to + continue blinking and return immediately. If :data:`False`, only + return when the blink is finished -- `on_time` (`float`): Number of seconds on. Defaults to 1 second. -- `off_time` (`float`): Number of seconds off. Defaults to 1 second. -- `n`: Number of times to blink; :data:`None` means forever. -- `background` (`bool`): If :data:`True` (the default), start a background thread to -continue blinking and return immediately. If :data:`False`, only -return when the blink is finished -- `ignored_kwargs`: Ignore all other keywords so this function can be called with identical -parameters also for all other output devices +:param ignored_kwargs: Ignore all other keywords so this function can be called with identical + parameters also for all other output devices @@ -3848,16 +3794,13 @@ class CallbackHandler() ``` Generic Callback Handler to collect callbacks functions through :func:`register` and execute them - with :func:`run_callbacks` A lock is used to sequence registering of new functions and running callbacks. -**Arguments**: - -- `name`: A name of this handler for usage in log messages -- `logger`: The logger instance to use for logging -- `context`: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created +:param name: A name of this handler for usage in log messages +:param logger: The logger instance to use for logging +:param context: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created @@ -3869,9 +3812,7 @@ def register(func: Optional[Callable[..., None]]) Register a new function to be executed when the callback event happens -**Arguments**: - -- `func`: The function to register. If set to :data:`None`, this register request is silently ignored. +:param func: The function to register. If set to :data:`None`, this register request is silently ignored. @@ -4031,12 +3972,10 @@ Set the ``key: value`` pair at arbitrary hierarchy depth All non-existing hierarchy levels are created. -**Arguments**: - -- `keys`: Key hierarchy path through the nested levels -- `value`: The value to set -- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type -is used +:param keys: Key hierarchy path through the nested levels +:param value: The value to set +:param hierarchy_type: The type for new hierarchy levels. If *None*, the top-level type + is used @@ -4050,16 +3989,11 @@ Set the ``key: value`` pair at arbitrary hierarchy depth unless the key already All non-existing hierarchy levels are created. -**Arguments**: - -- `keys`: Key hierarchy path through the nested levels -- `value`: The default value to set -- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type -is used - -**Returns**: - -The actual value or or the default value if key does not exit +:param keys: Key hierarchy path through the nested levels +:param value: The default value to set +:param hierarchy_type: The type for new hierarchy levels. If *None*, the top-level type + is used +:return: The actual value or or the default value if key does not exit @@ -4071,9 +4005,7 @@ def config_dict(data) Initialize configuration data from dict-like data structure -**Arguments**: - -- `data`: configuration data +:param data: configuration data @@ -4129,18 +4061,13 @@ def get_handler(name: str) -> ConfigHandler ``` Get a configuration data handler with the specified name, creating it - if it doesn't yet exit. If created, it is always created empty. This is the main entry point for obtaining an configuration handler -**Arguments**: - -- `name`: Name of the config handler - -**Returns**: - -`ConfigHandler`: The configuration data handler for *name* +:param name: Name of the config handler +:return: The configuration data handler for *name* +:rtype: ConfigHandler @@ -4152,14 +4079,9 @@ def load_yaml(cfg: ConfigHandler, filename: str) -> None Load a yaml file into a ConfigHandler -**Arguments**: - -- `cfg`: ConfigHandler instance -- `filename`: filename to yaml file - -**Returns**: - -None +:param cfg: ConfigHandler instance +:param filename: filename to yaml file +:return: None @@ -4175,17 +4097,12 @@ def write_yaml(cfg: ConfigHandler, Writes ConfigHandler data to yaml file / sys.stdout -**Arguments**: - -- `cfg`: ConfigHandler instance -- `filename`: filename to output file. If *sys.stdout*, output is written to console -- `only_if_changed`: Write file only, if ConfigHandler.is_modified() -- `args`: passed on to yaml.dump(...) -- `kwargs`: passed on to yaml.dump(...) - -**Returns**: - -None +:param cfg: ConfigHandler instance +:param filename: filename to output file. If *sys.stdout*, output is written to console +:param only_if_changed: Write file only, if ConfigHandler.is_modified() +:param args: passed on to yaml.dump(...) +:param kwargs: passed on to yaml.dump(...) +:return: None @@ -4265,9 +4182,7 @@ def __init__(music_library_base_path='/') Initialize the playlist generator with music_library_base_path -**Arguments**: - -- `music_library_base_path`: Base path the the music library. This is used to locate the file in the disk +:param music_library_base_path: Base path the the music library. This is used to locate the file in the disk but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir @@ -4293,14 +4208,9 @@ def get_directory_content(path='.') Parse the folder ``path`` and create a content list. Depth is always the current level -**Arguments**: - -- `path`: Path to folder **relative** to ``music_library_base_path`` - -**Returns**: - -[ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] -where type is one of :attr:`TYPE_DECODE` +:param path: Path to folder **relative** to ``music_library_base_path`` +:return: [ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] + where type is one of :attr:`TYPE_DECODE` @@ -4312,10 +4222,8 @@ def parse(path='.', recursive=False) Parse the folder ``path`` and create a playlist from it's content -**Arguments**: - -- `path`: Path to folder **relative** to ``music_library_base_path`` -- `recursive`: Parse folder recursivley, or stay in top-level folder +:param path: Path to folder **relative** to ``music_library_base_path`` +:param recursive: Parse folder recursivley, or stay in top-level folder @@ -4343,14 +4251,14 @@ different threads, always make a fresh call to ``get_publisher()`` to get the co Example:: -import jukebox.publishing as publishing + import jukebox.publishing as publishing -class MyClass: -def __init__(self): -pass + class MyClass: + def __init__(self): + pass -def say_hello(name): -publishing.get_publisher().send('hello', f'Hi {name}, howya?') + def say_hello(name): + publishing.get_publisher().send('hello', f'Hi {name}, howya?') To stress what **NOT** to do: don't get a publisher instance in the constructor and save it to ``self._pub``. If you do and ``say_hello`` gets called from different threads, the publisher of the thread which instantiated the class @@ -4379,48 +4287,48 @@ Structure .. code-block:: text -+-----------------------+ -| functional interface | Publisher -| | - functional interface for single Thread -| PUB | - sends data to publisher (and thus across threads) -+-----------------------+ -| (1) -v -+-----------------------+ -| SUB (bind) | PublishServer -| | - Last Value (LV) Cache -| XPUB (bind) | - Subscriber notification and LV resend -+-----------------------+ - independent thread -| (2) -v + +-----------------------+ + | functional interface | Publisher + | | - functional interface for single Thread + | PUB | - sends data to publisher (and thus across threads) + +-----------------------+ + | (1) + v + +-----------------------+ + | SUB (bind) | PublishServer + | | - Last Value (LV) Cache + | XPUB (bind) | - Subscriber notification and LV resend + +-----------------------+ - independent thread + | (2) + v Connection (1): Internal connection -Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) + Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) -Protocol: Multi-part message + Protocol: Multi-part message -Part 1: Topic (in topic tree format) -E.g. player.status.elapsed + Part 1: Topic (in topic tree format) + E.g. player.status.elapsed -Part 2: Payload or Message in json serialization -If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same + Part 2: Payload or Message in json serialization + If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same -Part 3: Command -Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer -and the message is not forwarded to the outside. This third part of the message is never forwarded + Part 3: Command + Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer + and the message is not forwarded to the outside. This third part of the message is never forwarded Connection (2): External connection -Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! -Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will -also get you all the branch topics. To get everything, subscribe to ``b''`` + Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! + Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will + also get you all the branch topics. To get everything, subscribe to ``b''`` -Protocol: Multi-part message + Protocol: Multi-part message -Part 1: Topic (in topic tree format) -E.g. player.status.elapsed + Part 1: Topic (in topic tree format) + E.g. player.status.elapsed -Part 2: Payload or Message in json serialization -If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) + Part 2: Payload or Message in json serialization + If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) Why? Why? ------------- @@ -4433,14 +4341,14 @@ For use case, we made a few simplifications Design Rationales ------------------- -* "If you need `millions of messages per second `_ -sent to thousands of points, -you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." +* "If you need `millions of messages per second `_ + sent to thousands of points, + you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then -the `XSUB and XPUB `_" + the `XSUB and XPUB `_" * "Let’s imagine `our feed has an average of 100,000 100-byte messages a second -`_ [...]. -While 100K messages a second is easy for a ZeroMQ application, ..." + `_ [...]. + While 100K messages a second is easy for a ZeroMQ application, ..." **But we have:** @@ -4461,8 +4369,8 @@ This means, we can use less complex patters than used for these high-speed, high * Late joining client (or drop-off and re-join): get full state update * Server crash etc: No special handling necessary, we are simple -and don't need recovery in this case. Server will publish initial state -after re-start + and don't need recovery in this case. Server will publish initial state + after re-start * Subscriber too slow: Subscribers problem (TODO: Do we need to do anything about it?) **Start-up sequence:** @@ -4481,22 +4389,22 @@ RPC can trigger through function call in components/publishing plugin that Plugins publishing state information should publish initial state at @plugin.finalize .. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is -required per thread. But the publisher instance **must** be thread-local! -Always go through :func:`publishing.get_publisher()`. + required per thread. But the publisher instance **must** be thread-local! + Always go through :func:`publishing.get_publisher()`. **Sockets** Three sockets are opened: -``. TCP (on a configurable port) -``. Websocket (on a configurable port) -``. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules -that want to know about the current state on event based updates. +#. TCP (on a configurable port) +#. Websocket (on a configurable port) +#. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules + that want to know about the current state on event based updates. **Further ZeroMQ References:** -* `Working with Messages `_ -* `Multiple Threads `_ +* `Working with Messages `_ +* `Multiple Threads `_ @@ -4511,7 +4419,7 @@ forwards them to the outside world Handles new subscriptions by sending out the entire cached state to **all** subscribers -The code is structures using a `Reactor Pattern `_ +The code is structures using a `Reactor Pattern `_ @@ -4565,9 +4473,7 @@ The publisher that provides the functional interface to the application def __init__(check_thread_owner=True) ``` -**Arguments**: - -- `check_thread_owner`: Check if send() is always called from the correct thread. This is debug feature +:param check_thread_owner: Check if send() is always called from the correct thread. This is debug feature and is intended to expose the situation before it leads to real trouble. Leave it on! @@ -4670,61 +4576,61 @@ you can implement a Factory Pattern using this package. Take a look at volume.py **Example:** Decorate a function for auto-registering under it's own name:: -import jukebox.plugs as plugs -@plugs.register -def func1(param): -pass + import jukebox.plugs as plugs + @plugs.register + def func1(param): + pass **Example:** Decorate a function for auto-registering under a new name:: -@plugs.register(name='better_name') -def func2(param): -pass + @plugs.register(name='better_name') + def func2(param): + pass **Example:** Register a function during run-time under it's own name:: -def func3(param): -pass -plugs.register(func3) + def func3(param): + pass + plugs.register(func3) **Example:** Register a function during run-time under a new name:: -def func4(param): -pass -plugs.register(func4, name='other_name', package='other_package') + def func4(param): + pass + plugs.register(func4, name='other_name', package='other_package') **Example:** Decorate a class for auto registering during initialization, including all methods (see _register_class for more info):: -@plugs.register(auto_tag=True) -class MyClass1: -pass + @plugs.register(auto_tag=True) + class MyClass1: + pass **Example:** Register a class instance, from which only report is a callable method through the plugs interface:: -class MyClass2: -@plugs.tag -def report(self): -pass -myinst2 = MyClass2() -plugin.register(myinst2, name='myinst2') + class MyClass2: + @plugs.tag + def report(self): + pass + myinst2 = MyClass2() + plugin.register(myinst2, name='myinst2') Naming convention: package -1. Either a python package -2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) + 1. Either a python package + 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) plugin -1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) -2. The string name to above object + 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) + 2. The string name to above object name -The string name of the plugin object for registration + The string name of the plugin object for registration method -1. In case the object is a class instance a bound method to call from the class instance -2. The string name to above object + 1. In case the object is a class instance a bound method to call from the class instance + 2. The string name to above object @@ -4829,13 +4735,12 @@ For more documentation see the functions See the examples in Module :mod:`plugs` how to use this decorator / function -**Arguments**: - -- `plugin`: -- `name`: -- `package`: -- `replace`: -- `auto_tag`: +:param plugin: +:param name: +:param package: +:param replace: +:param auto_tag: +:return: @@ -4850,13 +4755,8 @@ Method decorator for tagging a method as callable through the plugs interface Note that the instantiated class must still be registered as plugin object (either with the class decorator or dynamically) -**Arguments**: - -- `func`: function to decorate - -**Returns**: - -the function +:param func: function to decorate +:return: the function @@ -4868,13 +4768,8 @@ def initialize(func: Callable) -> Callable Decorator for functions that shall be called by the plugs package directly after the module is loaded -**Arguments**: - -- `func`: Function to decorate - -**Returns**: - -The function itself +:param func: Function to decorate +:return: The function itself @@ -4886,13 +4781,8 @@ def finalize(func: Callable) -> Callable Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded -**Arguments**: - -- `func`: Function to decorate - -**Returns**: - -The function itself +:param func: Function to decorate +:return: The function itself @@ -4911,13 +4801,8 @@ Decorator for functions that shall be called by the plugs package directly after The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) It is intended for passing down the signal number that initiated the program termination -**Arguments**: - -- `func`: Function to decorate - -**Returns**: - -The function itself +:param func: Function to decorate +:return: The function itself @@ -4940,12 +4825,11 @@ Instead of using @initializer, you may of course use __init__.py Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under which they are loaded as plugin package also. -**Arguments**: - -- `package`: Python package to load as plugin package -- `load_as`: Plugin package registration name. If None the name is the python's package simple name -- `prefix`: Prefix to python package to create fully qualified name. This is used only to locate the python package -and ignored otherwise. Useful if all the plugin module are in a dedicated folder +:param package: Python package to load as plugin package +:param load_as: Plugin package registration name. If None the name is the python's package simple name +:param prefix: Prefix to python package to create fully qualified name. This is used only to locate the python package + and ignored otherwise. Useful if all the plugin module are in a dedicated folder +:return: @@ -4959,9 +4843,7 @@ def load_all_named(packages_named: Mapping[str, str], Load all packages in packages_named with mapped names -**Arguments**: - -- `packages_named`: Dict[load_as, package] +:param packages_named: Dict[load_as, package] @@ -5001,7 +4883,7 @@ Modules are processed in reverse order. Several at-exit tagged functions of a si in the order of registration. Errors raised in functions are suppressed to ensure all plugins are processed - +:return: @@ -5035,23 +4917,18 @@ Calls are serialized by a thread lock. The thread lock is shared with call_ignor There is no logger in this function as they all belong up-level where the exceptions are handled. If you want logger messages instead of exceptions, use :func:`call_ignore_errors` -**Arguments**: - -- `package`: Name of the plugin package in which to look for function/class instance -- `plugin`: Function name or instance name of a class -- `method`: Method name when accessing a class instance' method. Leave at *None* if unneeded. -- `as_thread`: Run the callable in separate daemon thread. -There is no return value from the callable in this case! The return value is the thread object. -Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. -All threads are started as daemon threads with terminate upon main program termination. -There is not stop-thread mechanism. This is intended for short lived threads. -- `thread_name`: Name of the thread -- `args`: Arguments passed to callable -- `kwargs`: Keyword arguments passed to callable - -**Returns**: - -The return value from the called function, or, if started as thread the thread object +:param package: Name of the plugin package in which to look for function/class instance +:param plugin: Function name or instance name of a class +:param method: Method name when accessing a class instance' method. Leave at *None* if unneeded. +:param as_thread: Run the callable in separate daemon thread. + There is no return value from the callable in this case! The return value is the thread object. + Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. + All threads are started as daemon threads with terminate upon main program termination. + There is not stop-thread mechanism. This is intended for short lived threads. +:param thread_name: Name of the thread +:param args: Arguments passed to callable +:param kwargs: Keyword arguments passed to callable +:return: The return value from the called function, or, if started as thread the thread object @@ -5166,9 +5043,7 @@ def get_all_loaded_packages() -> Dict[str, str] Report a short summary of all loaded packages -**Returns**: - -Dictionary of the form `{loaded_as: loaded_from, ...}` +:return: Dictionary of the form `{loaded_as: loaded_from, ...}` @@ -5188,9 +5063,7 @@ Report those packages that did not load error free Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED -**Returns**: - -Dictionary of the form `{loaded_as: loaded_from, ...}` +:return: Dictionary of the form `{loaded_as: loaded_from, ...}` @@ -5251,12 +5124,10 @@ Interface for plugin / RPC accessibility for a single event timer def __init__(name, wait_seconds: float, function, args=None, kwargs=None) ``` -**Arguments**: - -- `wait_seconds`: The time in seconds to wait before calling function -- `function`: The function to call with args and kwargs. -- `args`: Parameters for function call -- `kwargs`: Parameters for function call +:param wait_seconds: The time in seconds to wait before calling function +:param function: The function to call with args and kwargs. +:param args: Parameters for function call +:param kwargs: Parameters for function call @@ -5324,9 +5195,7 @@ def get_timeout() Get the configured time-out -**Returns**: - -The total wait time. (Not the remaining wait time!) +:return: The total wait time. (Not the remaining wait time!) @@ -5394,15 +5263,13 @@ def __init__(name, kwargs=None) ``` -**Arguments**: - -- `iterations`: Number of times callee is called -- `wait_seconds_per_iteration`: Wait in seconds before each iteration -- `callee`: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). +:param iterations: Number of times callee is called +:param wait_seconds_per_iteration: Wait in seconds before each iteration +:param callee: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). Then with every time out iteration __call__(*args, iteration=iteration, **kwargs) is called. 'iteration' is the current iteration count in decreasing order! -- `args`: -- `kwargs`: +:param args: +:param kwargs: @@ -5433,13 +5300,8 @@ Makes sure that the core rpc call parameters have valid default values in cfg_rp .. important: Leaves all other parameters in cfg_action untouched or later downstream processing! -**Arguments**: - -- `cfg_rpc_call`: RPC command as configuration entry - -**Returns**: - -A fully populated deep copy of cfg_rpc_call +:param cfg_rpc_call: RPC command as configuration entry +:return: A fully populated deep copy of cfg_rpc_call @@ -5463,14 +5325,9 @@ If an explicitly specified this is not done. However, it is ensured that the ret dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling for non-existing RPC commands and we get a clearer error message. -**Arguments**: - -- `cfg_rpc_cmd`: RPC command as configuration entry -- `logger`: The logger to use - -**Returns**: - -A decoded, fully populated deep copy of cfg_rpc_cmd +:param cfg_rpc_cmd: RPC command as configuration entry +:param logger: The logger to use +:return: A decoded, fully populated deep copy of cfg_rpc_cmd @@ -5494,24 +5351,21 @@ def bind_rpc_command(cfg_rpc_cmd: Dict, Decode an RPC command configuration entry and bind it to a function -**Arguments**: +:param dereference: Dereference even the call to plugs.call(...) -- `dereference`: Dereference even the call to plugs.call(...) - ``. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with - all checks applied at bind time - ``. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with - all checks applied at bind time. + #. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with + all checks applied at bind time + #. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with + all checks applied at bind time. -Setting deference to True, circumvents the dynamic nature of the plugins: the function to call - must exist at bind time and cannot change. If False, the function to call must only exist at call time. - This can be important during the initialization where package ordering and initialization means that not all - classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls - is circumvented. Use with care! + Setting deference to True, circumvents the dynamic nature of the plugins: the function to call + must exist at bind time and cannot change. If False, the function to call must only exist at call time. + This can be important during the initialization where package ordering and initialization means that not all + classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls + is circumvented. Use with care! -**Returns**: - -Callable function w/o parameters which directly runs the RPC command -using plugs.call_ignore_errors +:return: Callable function w/o parameters which directly runs the RPC command + using plugs.call_ignore_errors @@ -5523,10 +5377,8 @@ def rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) -> str Return a readable string of an RPC call config -**Arguments**: - -- `cfg_rpc_call`: RPC call configuration entry -- `with_args`: Return string shall include the arguments of the function +:param cfg_rpc_call: RPC call configuration entry +:param with_args: Return string shall include the arguments of the function @@ -5608,9 +5460,9 @@ If present, 'id' and 'tsp' may not be None. If they are None, there are treated Three sockets are opened -``. TCP (on a configurable port) -``. Websocket (on a configurable port) -``. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be +#. TCP (on a configurable port) +#. Websocket (on a configurable port) +#. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which button triggers what action) diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 07c0eee20..ed606aa90 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -1,6 +1,8 @@ loaders: - type: python search_path: [./src/jukebox] +processors: +- type: filter renderer: type: markdown render_toc: true From 4be921c93bdd66ec657b02f18cf5b02a16e06823 Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 22 Dec 2023 09:53:13 +0100 Subject: [PATCH 08/41] revert last commit --- .../docstring-pydoc-markdown/docstring.md | 994 ++++++++++-------- pydoc-markdown.yml | 2 - 2 files changed, 571 insertions(+), 425 deletions(-) diff --git a/documentation/developers/docstring-pydoc-markdown/docstring.md b/documentation/developers/docstring-pydoc-markdown/docstring.md index 72797297d..b2184d060 100644 --- a/documentation/developers/docstring-pydoc-markdown/docstring.md +++ b/documentation/developers/docstring-pydoc-markdown/docstring.md @@ -520,13 +520,18 @@ def input_int(prompt, Request an integer input from user -:param prompt: The prompt to display -:param blank: Value to return when user just hits enter. Leave at None, if blank is invalid -:param min: Minimum valid integer value (None disables this check) -:param max: Maximum valid integer value (None disables this check) -:param prompt_color: Color of the prompt. Color will be reset at end of prompt -:param prompt_hint: Append a 'hint' with [min...max, default=xx] to end of prompt -:return: integer value read from user input +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `min`: Minimum valid integer value (None disables this check) +- `max`: Maximum valid integer value (None disables this check) +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [min...max, default=xx] to end of prompt + +**Returns**: + +integer value read from user input @@ -543,11 +548,16 @@ Request a yes / no choice from user Accepts multiple input for true/false and is case insensitive -:param prompt: The prompt to display -:param blank: Value to return when user just hits enter. Leave at None, if blank is invalid -:param prompt_color: Color of the prompt. Color will be reset at end of prompt -:param prompt_hint: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized -:return: boolean value read from user input +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized + +**Returns**: + +boolean value read from user input @@ -564,19 +574,19 @@ Hierarchy separator is the '.'. If the logger already exits, getLogger will retu created on the spot. :Example: How to get logger and log away at your heart's content: - >>> import logging - >>> logger = logging.getLogger('jb.awesome_module') - >>> logger.info('Started general awesomeness aura') +>>> import logging +>>> logger = logging.getLogger('jb.awesome_module') +>>> logger.info('Started general awesomeness aura') Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: `` loggers: - jb: - level: WARNING - handlers: [console, debug_file_handler, error_file_handler] - propagate: no - jb.awesome_module: - level: DEBUG +jb: +level: WARNING +handlers: [console, debug_file_handler, error_file_handler] +propagate: no +jb.awesome_module: +level: DEBUG `` .. note:: @@ -609,8 +619,10 @@ Don't forget to {reset} the color settings at the end of the string. def __init__(enable=True, color_levelname=True) ``` -:param enable: Enable the coloring -:param color_levelname: Enable auto-coloring when using the levelname keyword +**Arguments**: + +- `enable`: Enable the coloring +- `color_levelname`: Enable auto-coloring when using the levelname keyword @@ -680,8 +692,13 @@ def resolve(color_name: str) Resolve a color name into the respective color constant -:param color_name: Name of the color -:return: color constant +**Arguments**: + +- `color_name`: Name of the color + +**Returns**: + +color constant @@ -736,6 +753,7 @@ Callback signature is :param folder: relativ path to folder to play :param state: indicator of the state inside the calling + #### run\_callbacks @@ -754,42 +772,42 @@ Package for interfacing with the MPD Music Player Daemon Status information in three topics 1) Player Status: published only on change - This is a subset of the MPD status (and not the full MPD status) ?? - - folder - - song - - volume (volume is published only via player status, and not separatly to avoid too many Threads) - - ... +This is a subset of the MPD status (and not the full MPD status) ?? +- folder +- song +- volume (volume is published only via player status, and not separatly to avoid too many Threads) +- ... 2) Elapsed time: published every 250 ms, unless constant - - elapsed +- elapsed 3) Folder Config: published only on change - This belongs to the folder being played - Publish: - - random, resume, single, loop - On save store this information: - Contains the information for resume functionality of each folder - - random, resume, single, loop - - if resume: - - current song, elapsed - - what is PLAYSTATUS for? - When to save - - on stop - Angstsave: - - on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) - - on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) - Load checks: - - if resume, but no song, elapsed -> log error and start from the beginning +This belongs to the folder being played +Publish: +- random, resume, single, loop +On save store this information: +Contains the information for resume functionality of each folder +- random, resume, single, loop +- if resume: +- current song, elapsed +- what is PLAYSTATUS for? +When to save +- on stop +Angstsave: +- on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) +- on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) +Load checks: +- if resume, but no song, elapsed -> log error and start from the beginning Status storing: - - Folder config for each folder (see above) - - Information to restart last folder playback, which is: - - last_folder -> folder_on_close - - song, elapsed - - random, resume, single, loop - - if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! - on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card +- Folder config for each folder (see above) +- Information to restart last folder playback, which is: +- last_folder -> folder_on_close +- song, elapsed +- random, resume, single, loop +- if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! +on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card Internal status - - last played folder: Needed to detect second swipe +- last played folder: Needed to detect second swipe Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, @@ -797,12 +815,13 @@ Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CUR {'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, 'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} -References: -https://github.com/Mic92/python-mpd2 -https://python-mpd2.readthedocs.io/en/latest/topics/commands.html -https://mpd.readthedocs.io/en/latest/protocol.html +**References**: -sudo -u mpd speaker-test -t wav -c 2 + https://github.com/Mic92/python-mpd2 + https://python-mpd2.readthedocs.io/en/latest/topics/commands.html + https://mpd.readthedocs.io/en/latest/protocol.html + + sudo -u mpd speaker-test -t wav -c 2 @@ -918,8 +937,10 @@ Main entry point for trigger music playing from RFID reader. Decodes second swip Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action accordingly. -:param folder: Folder path relative to music library path -:param recursive: Add folder recursively +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively @@ -934,7 +955,9 @@ Get the folder content as content list with meta-information. Depth is always 1. Call repeatedly to descend in hierarchy -:param folder: Folder path relative to music library path +**Arguments**: + +- `folder`: Folder path relative to music library path @@ -950,8 +973,10 @@ Playback a music folder. Folder content is added to the playlist as described by :mod:`jukebox.playlistgenerator`. The playlist is cleared first. -:param folder: Folder path relative to music library path -:param recursive: Add folder recursively +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively @@ -967,8 +992,10 @@ Playback a album found in MPD database. All album songs are added to the playlist The playlist is cleared first. -:param albumartist: Artist of the Album provided by MPD database -:param album: Album name provided by MPD database +**Arguments**: + +- `albumartist`: Artist of the Album provided by MPD database +- `album`: Album name provided by MPD database @@ -1054,7 +1081,9 @@ def sync_change_on_rfid_scan(option: str = 'toggle') -> None Change activation of 'on_rfid_scan_enabled' -:param option: Must be one of 'enable', 'disable', 'toggle' +**Arguments**: + +- `option`: Must be one of 'enable', 'disable', 'toggle' @@ -1078,9 +1107,12 @@ def sync_card_database(card_id: str) -> bool ``` Sync the card database from the remote server, if existing. + If card_id is provided only this entry is updated. -:param card_id: The cardid to update +**Arguments**: + +- `card_id`: The cardid to update @@ -1093,7 +1125,9 @@ def sync_folder(folder: str) -> bool Sync the folder from the remote server, if existing -:param folder: Folder path relative to music library path +**Arguments**: + +- `folder`: Folder path relative to music library path @@ -1145,7 +1179,7 @@ from the Jukebox. Remove it from the configuration! .. code-block:: text - ### Use hot-plugged devices like Bluetooth or USB automatically (LP: #1702794) + ### Use hot-plugged devices like Bluetooth or USB automatically (LP: `1702794`) ### not available on PI? .ifexists module-switch-on-connect.so load-module module-switch-on-connect @@ -1161,9 +1195,9 @@ Callbacks: The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): - #. :func:`add_on_connect_callback` - #. :func:`add_on_output_change_callbacks` - #. :func:`add_on_volume_change_callback` + ``. :func:`add_on_connect_callback` + ``. :func:`add_on_output_change_callbacks` + ``. :func:`add_on_volume_change_callback` @@ -1215,6 +1249,7 @@ Callback signature is :param device_name: The sound card device name as reported in device properties + #### run\_callbacks @@ -1315,6 +1350,7 @@ def register(func: Callable[[str, str, int, int], None]) ``` Add a new callback function :attr:`func`. + Parameters always give the valid audio sink. That means, if an error occurred, all parameters are valid. @@ -1330,6 +1366,7 @@ Callback signature is but an error occurred. Above parameters always give the now valid sink! If a sink change is successful, it is 0. + #### run\_callbacks @@ -1371,6 +1408,7 @@ Callback signature is :param is_min: 1, if volume level is minimum, else 0 :param is_max: 1, if volume level is maximum, else 0 + #### run\_callbacks @@ -1559,6 +1597,7 @@ Callback signature is :param card_id: Card ID :param state: See :class:`RfidCardDetectState` + #### run\_callbacks @@ -1594,7 +1633,9 @@ A timer watchdog thread that calls timeout_action on time-out def __init__(on_timeout_callback, logger: logging.Logger = None) ``` -:param on_timeout_callback: The function to execute on time-out +**Arguments**: + +- `on_timeout_callback`: The function to execute on time-out @@ -1611,11 +1652,13 @@ def reader_install_dependencies(reader_path: str, Install dependencies for the selected reader module -:param reader_path: Path to the reader module -:parameter dependency_install: how to handle installing of dependencies - 'query': query user (default) - 'auto': automatically - 'no': don't install dependencies +**Arguments**: + +- `reader_path`: Path to the reader module +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies @@ -1630,8 +1673,13 @@ Load the module for the reader_name A ModuleNotFoundError is unrecoverable, but we at least want to give some hint how to resolve that to the user All other errors will NOT be handled. Modules that do not load due to compile errors have other problems -:param reader_name: Name of the reader to load the module for -:return: module +**Arguments**: + +- `reader_name`: Name of the reader to load the module for + +**Returns**: + +module @@ -1663,12 +1711,16 @@ exit the script w/o writing the config to file. No harm done. This script expects to reside in the directory with all the reader subpackages, i.e it is part of the rfid-reader package. Otherwise you'll need to adjust sys.path -:parameter dependency_install: how to handle installing of dependencies - 'query': query user (default) - 'auto': automatically - 'no': don't install dependencies -:return: nested dict with entire configuration that can be read into ConfigParser -:rtype: dict as {section: {parameter: value}} +**Arguments**: + +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies + +**Returns**: + +`dict as {section: {parameter: value}}`: nested dict with entire configuration that can be read into ConfigParser @@ -1682,9 +1734,11 @@ def write_config(config_file: str, Write configuration to config_file -:parameter config_file: relative or absolute path to config file -:parameter config_dict: nested dict with configuration parameters for ConfigParser consumption -:parameter force_overwrite: overwrite existing configuration file without asking +**Arguments**: + +- `config_file`: relative or absolute path to config file +- `config_dict`: nested dict with configuration parameters for ConfigParser consumption +- `force_overwrite`: overwrite existing configuration file without asking @@ -1710,8 +1764,13 @@ def create_inputs(frame, default_btn_width, default_padx, default_pady) Add all input devies to the GUI -:param frame: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to -:return: List of all added GUI buttons +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to + +**Returns**: + +List of all added GUI buttons @@ -1773,8 +1832,13 @@ def create_outputs(frame, default_btn_width, default_padx, default_pady) Add all output devices to the GUI -:param frame: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to -:return: List of all added GUI objects +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to + +**Returns**: + +List of all added GUI objects @@ -1989,8 +2053,10 @@ Format: 'id': {decoded_function_call, ignore_same_id_delay, ignore_card_removal_ def delete_card(card_id: str, auto_save: bool = True) ``` -:param auto_save: -:param card_id: +**Arguments**: + +- `auto_save`: +- `card_id`: @@ -2015,9 +2081,9 @@ If you are going to call this through the RPC it will get a little verbose **Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume (*here: 15*) and custom *ignore_same_id_delay value*:: - plugin.call_ignore_errors('cards', 'register_card', - args=['0009', 'inc_volume'], - kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) +plugin.call_ignore_errors('cards', 'register_card', +args=['0009', 'inc_volume'], +kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) @@ -2102,7 +2168,9 @@ def republish(topic=None) Re-publish the topic tree 'topic' to all subscribers -:param topic: Topic tree to republish. None = resend all +**Arguments**: + +- `topic`: Topic tree to republish. None = resend all @@ -2557,7 +2625,9 @@ This is also used when configuration errors lead to non existing RPC command ali When the alias definition is void, we still want to return a valid function to simplify error handling up the module call stack. -:param msg: If present, this message is send to the logger with severity warning +**Arguments**: + +- `msg`: If present, this message is send to the logger with severity warning @@ -2596,15 +2666,20 @@ Find an input device with device_name and mandatory keys. Raises - #. FileNotFoundError, if no device is found. - #. AttributeError, if device does not have the mandatory keys + ``. FileNotFoundError, if no device is found. + ``. AttributeError, if device does not have the mandatory keys If multiple devices match, the first match is returned -:param device_name: See :func:`_filter_by_device_name` -:param exact_name: See :func:`_filter_by_device_name` -:param mandatory_keys: See :func:`_filter_by_mandatory_keys` -:return: The path to the device +**Arguments**: + +- `device_name`: See :func:`_filter_by_device_name` +- `exact_name`: See :func:`_filter_by_device_name` +- `mandatory_keys`: See :func:`_filter_by_mandatory_keys` + +**Returns**: + +The path to the device @@ -2630,10 +2705,12 @@ Assign callbacks to :attr:`EvDevKeyListener.button_callbacks` def __init__(device_name_request: str, exact_name: bool, thread_name: str) ``` -:param device_name_request: The device name to look for -:param exact_name: If true, device_name must mach exactly, else a match is returned if device_name is a substring of - the reported device name -:param thread_name: Name of the listener thread +**Arguments**: + +- `device_name_request`: The device name to look for +- `exact_name`: If true, device_name must mach exactly, else a match is returned if device_name is a substring of +the reported device name +- `thread_name`: Name of the listener thread @@ -2854,6 +2931,7 @@ Callback signature is :param status: 1 if app started, 0 if app shuts down + #### run\_callbacks @@ -2906,7 +2984,9 @@ def get_output(name: str) Get the output device instance based on the configured name -:param name: The alias name output device instance +**Arguments**: + +- `name`: The alias name output device instance @@ -2919,7 +2999,9 @@ def on(name: str) Turn an output device on -:param name: The alias name output device instance +**Arguments**: + +- `name`: The alias name output device instance @@ -2932,7 +3014,9 @@ def off(name: str) Turn an output device off -:param name: The alias name output device instance +**Arguments**: + +- `name`: The alias name output device instance @@ -2945,9 +3029,10 @@ def set_value(name: str, value: Any) Set the output device to :attr:`value` -:param name: The alias name output device instance +**Arguments**: -:param value: Value to set the device to +- `name`: The alias name output device instance +- `value`: Value to set the device to @@ -2971,21 +3056,16 @@ Flash (blink or beep) an output device This is a generic function for all types of output devices. Parameters not applicable to an specific output device are silently ignored -:param name: The alias name output device instance - -:param on_time: Time in seconds in state ``ON`` +**Arguments**: -:param off_time: Time in seconds in state ``OFF`` - -:param n: Number of flash cycles - -:param tone: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. - -:param color: The RGB color *only for PWMLED*. - -:param fade_in_time: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* - -:param fade_out_time: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* +- `name`: The alias name output device instance +- `on_time`: Time in seconds in state ``ON`` +- `off_time`: Time in seconds in state ``OFF`` +- `n`: Number of flash cycles +- `tone`: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. +- `color`: The RGB color *only for PWMLED*. +- `fade_in_time`: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* +- `fade_out_time`: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* @@ -3157,11 +3237,12 @@ class VolumeToRGB() Converts linear volume level to an RGB color value running through the color spectrum -:param max_input: Maximum input value of linear input data -:param offset: Offset in degrees in the color circle. Color circle - traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) -:param section: The section of the full color circle to use in degrees +**Arguments**: +- `max_input`: Maximum input value of linear input data +- `offset`: Offset in degrees in the color circle. Color circle +traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) +- `section`: The section of the full color circle to use in degrees Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 .. code-block:: python @@ -3182,8 +3263,9 @@ def __call__(volume) -> Tuple[float, float, float] Perform conversion for single volume level -:return: Tuple(red, green, blue) -:meta public: +**Returns**: + +Tuple(red, green, blue) @@ -3253,8 +3335,10 @@ def set_rpc_actions(action_config) -> None Set all input device callbacks from :attr:`action_config` -:param action_config: Dictionary with one - :ref:`RPC Command ` definition entry for every device callback +**Arguments**: + +- `action_config`: Dictionary with one +:ref:`RPC Command ` definition entry for every device callback @@ -3333,40 +3417,28 @@ class Button(NameMixin, ButtonBase) A basic Button that runs a single actions on button press -:type pull_up: bool -:param pull_up: If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. - If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external - resistor must be used and the :attr:`active_state` must be set. - -:type active_state: bool or None -:param active_state: - If :data:`True`, when the hardware pin state is ``HIGH``, the software - pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when - the hardware pin state is ``HIGH``, the software pin state is ``LOW``. - Use this parameter to set the active state of the underlying pin when - configuring it as not pulled (when *pull_up* is :data:`None`). When - *pull_up* is :data:`True` or :data:`False`, the active state is - automatically set to the proper value. - -:type bounce_time: float or None -:param bounce_time: - Specifies the length of time (in seconds) that the component will - ignore changes in state after an initial change. This defaults to - :data:`None` which indicates that no bounce compensation will be - performed. - -:type hold_repeat: bool -:param hold_repeat: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action - is run only once independent of the length of time the button is pressed for. - -:type hold_time: float -:param hold_time: Time in seconds to wait between invocations of :attr:`on_press`. - -:param pin_factory: The GPIOZero pin factory. This parameter cannot be set through the configuration file - -:type name: str -:param name: The name of the button for use in error messages. This parameter cannot be set explicitly - through the configuration file +**Arguments**: + +- `pull_up` (`bool`): If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. +If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external +resistor must be used and the :attr:`active_state` must be set. +- `active_state` (`bool or None`): If :data:`True`, when the hardware pin state is ``HIGH``, the software +pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when +the hardware pin state is ``HIGH``, the software pin state is ``LOW``. +Use this parameter to set the active state of the underlying pin when +configuring it as not pulled (when *pull_up* is :data:`None`). When +*pull_up* is :data:`True` or :data:`False`, the active state is +automatically set to the proper value. +- `bounce_time` (`float or None`): Specifies the length of time (in seconds) that the component will +ignore changes in state after an initial change. This defaults to +:data:`None` which indicates that no bounce compensation will be +performed. +- `hold_repeat` (`bool`): If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action +is run only once independent of the length of time the button is pressed for. +- `hold_time` (`float`): Time in seconds to wait between invocations of :attr:`on_press`. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file .. copied from GPIOZero's documentation: active_state, bounce_time .. Copyright Ben Nuttall / SPDX-License-Identifier: BSD-3-Clause @@ -3392,17 +3464,15 @@ class LongPressButton(NameMixin, ButtonBase) A Button that runs a single actions only when the button is pressed long enough -:param pull_up: See `Button`_ - -:param active_state: See `Button`_ - -:param bounce_time: See `Button`_ +**Arguments**: -:param hold_repeat: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action - is run only once independent of the length of time the button is pressed for. - -:param hold_time: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. - Also the time in seconds to wait between invocations of :attr:`on_press`. +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action +is run only once independent of the length of time the button is pressed for. +- `hold_time`: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. +Also the time in seconds to wait between invocations of :attr:`on_press`. @@ -3431,22 +3501,18 @@ But a long press can be identified as soon as :attr:`hold_time` is reached and t event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run in this case! -:param pull_up: See `Button`_ - -:param active_state: See `Button`_ - -:param bounce_time: See `Button`_ - -:param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before - this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the - short press action is ignored +**Arguments**: -:param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press - action - -:param pin_factory: See `Button`_ - -:param name: See `Button`_ +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ @@ -3458,11 +3524,11 @@ class RotaryEncoder(NameMixin) A rotary encoder to run one of two actions depending on the rotation direction. -:param bounce_time: See `Button`_ +**Arguments**: -:param pin_factory: See `Button`_ - -:param name: See `Button`_ +- `bounce_time`: See `Button`_ +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ @@ -3538,22 +3604,18 @@ in this case! It is not necessary to configure all actions. -:param pull_up: See `Button`_ - -:param active_state: See `Button`_ +**Arguments**: -:param bounce_time: See `Button`_ - -:param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before - this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the - short press action is ignored. - -:param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press - action. A long dual press is never repeated independent of this setting - -:param pin_factory: See `Button`_ - -:param name: See `Button`_ +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored. +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action. A long dual press is never repeated independent of this setting +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ @@ -3626,15 +3688,13 @@ class LED(NameMixin, gpiozero.LED) A binary LED -:param pin: The GPIO pin which the LED is connected - -:param active_high: If :data:`true` the output pin will have a high logic level when the device is turned on. +**Arguments**: -:param pin_factory: The GPIOZero pin factory. This parameter cannot be set through the configuration file - -:type name: str -:param name: The name of the button for use in error messages. This parameter cannot be set explicitly - through the configuration file +- `pin`: The GPIO pin which the LED is connected +- `active_high`: If :data:`true` the output pin will have a high logic level when the device is turned on. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file @@ -3646,22 +3706,16 @@ def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) Exactly like :func:`blink` but restores the original state after flashing the device -:param float on_time: - Number of seconds on. Defaults to 1 second. - -:param float off_time: - Number of seconds off. Defaults to 1 second. - -:param n: - Number of times to blink; :data:`None` means forever. - -:param bool background: - If :data:`True` (the default), start a background thread to - continue blinking and return immediately. If :data:`False`, only - return when the blink is finished +**Arguments**: -:param ignored_kwargs: Ignore all other keywords so this function can be called with identical - parameters also for all other output devices +- `on_time` (`float`): Number of seconds on. Defaults to 1 second. +- `off_time` (`float`): Number of seconds off. Defaults to 1 second. +- `n`: Number of times to blink; :data:`None` means forever. +- `background` (`bool`): If :data:`True` (the default), start a background thread to +continue blinking and return immediately. If :data:`False`, only +return when the blink is finished +- `ignored_kwargs`: Ignore all other keywords so this function can be called with identical +parameters also for all other output devices @@ -3794,13 +3848,16 @@ class CallbackHandler() ``` Generic Callback Handler to collect callbacks functions through :func:`register` and execute them + with :func:`run_callbacks` A lock is used to sequence registering of new functions and running callbacks. -:param name: A name of this handler for usage in log messages -:param logger: The logger instance to use for logging -:param context: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created +**Arguments**: + +- `name`: A name of this handler for usage in log messages +- `logger`: The logger instance to use for logging +- `context`: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created @@ -3812,7 +3869,9 @@ def register(func: Optional[Callable[..., None]]) Register a new function to be executed when the callback event happens -:param func: The function to register. If set to :data:`None`, this register request is silently ignored. +**Arguments**: + +- `func`: The function to register. If set to :data:`None`, this register request is silently ignored. @@ -3972,10 +4031,12 @@ Set the ``key: value`` pair at arbitrary hierarchy depth All non-existing hierarchy levels are created. -:param keys: Key hierarchy path through the nested levels -:param value: The value to set -:param hierarchy_type: The type for new hierarchy levels. If *None*, the top-level type - is used +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used @@ -3989,11 +4050,16 @@ Set the ``key: value`` pair at arbitrary hierarchy depth unless the key already All non-existing hierarchy levels are created. -:param keys: Key hierarchy path through the nested levels -:param value: The default value to set -:param hierarchy_type: The type for new hierarchy levels. If *None*, the top-level type - is used -:return: The actual value or or the default value if key does not exit +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The default value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used + +**Returns**: + +The actual value or or the default value if key does not exit @@ -4005,7 +4071,9 @@ def config_dict(data) Initialize configuration data from dict-like data structure -:param data: configuration data +**Arguments**: + +- `data`: configuration data @@ -4061,13 +4129,18 @@ def get_handler(name: str) -> ConfigHandler ``` Get a configuration data handler with the specified name, creating it + if it doesn't yet exit. If created, it is always created empty. This is the main entry point for obtaining an configuration handler -:param name: Name of the config handler -:return: The configuration data handler for *name* -:rtype: ConfigHandler +**Arguments**: + +- `name`: Name of the config handler + +**Returns**: + +`ConfigHandler`: The configuration data handler for *name* @@ -4079,9 +4152,14 @@ def load_yaml(cfg: ConfigHandler, filename: str) -> None Load a yaml file into a ConfigHandler -:param cfg: ConfigHandler instance -:param filename: filename to yaml file -:return: None +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to yaml file + +**Returns**: + +None @@ -4097,12 +4175,17 @@ def write_yaml(cfg: ConfigHandler, Writes ConfigHandler data to yaml file / sys.stdout -:param cfg: ConfigHandler instance -:param filename: filename to output file. If *sys.stdout*, output is written to console -:param only_if_changed: Write file only, if ConfigHandler.is_modified() -:param args: passed on to yaml.dump(...) -:param kwargs: passed on to yaml.dump(...) -:return: None +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to output file. If *sys.stdout*, output is written to console +- `only_if_changed`: Write file only, if ConfigHandler.is_modified() +- `args`: passed on to yaml.dump(...) +- `kwargs`: passed on to yaml.dump(...) + +**Returns**: + +None @@ -4182,7 +4265,9 @@ def __init__(music_library_base_path='/') Initialize the playlist generator with music_library_base_path -:param music_library_base_path: Base path the the music library. This is used to locate the file in the disk +**Arguments**: + +- `music_library_base_path`: Base path the the music library. This is used to locate the file in the disk but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir @@ -4208,9 +4293,14 @@ def get_directory_content(path='.') Parse the folder ``path`` and create a content list. Depth is always the current level -:param path: Path to folder **relative** to ``music_library_base_path`` -:return: [ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] - where type is one of :attr:`TYPE_DECODE` +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` + +**Returns**: + +[ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] +where type is one of :attr:`TYPE_DECODE` @@ -4222,8 +4312,10 @@ def parse(path='.', recursive=False) Parse the folder ``path`` and create a playlist from it's content -:param path: Path to folder **relative** to ``music_library_base_path`` -:param recursive: Parse folder recursivley, or stay in top-level folder +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` +- `recursive`: Parse folder recursivley, or stay in top-level folder @@ -4251,14 +4343,14 @@ different threads, always make a fresh call to ``get_publisher()`` to get the co Example:: - import jukebox.publishing as publishing +import jukebox.publishing as publishing - class MyClass: - def __init__(self): - pass +class MyClass: +def __init__(self): +pass - def say_hello(name): - publishing.get_publisher().send('hello', f'Hi {name}, howya?') +def say_hello(name): +publishing.get_publisher().send('hello', f'Hi {name}, howya?') To stress what **NOT** to do: don't get a publisher instance in the constructor and save it to ``self._pub``. If you do and ``say_hello`` gets called from different threads, the publisher of the thread which instantiated the class @@ -4287,48 +4379,48 @@ Structure .. code-block:: text - +-----------------------+ - | functional interface | Publisher - | | - functional interface for single Thread - | PUB | - sends data to publisher (and thus across threads) - +-----------------------+ - | (1) - v - +-----------------------+ - | SUB (bind) | PublishServer - | | - Last Value (LV) Cache - | XPUB (bind) | - Subscriber notification and LV resend - +-----------------------+ - independent thread - | (2) - v ++-----------------------+ +| functional interface | Publisher +| | - functional interface for single Thread +| PUB | - sends data to publisher (and thus across threads) ++-----------------------+ +| (1) +v ++-----------------------+ +| SUB (bind) | PublishServer +| | - Last Value (LV) Cache +| XPUB (bind) | - Subscriber notification and LV resend ++-----------------------+ - independent thread +| (2) +v Connection (1): Internal connection - Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) +Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) - Protocol: Multi-part message +Protocol: Multi-part message - Part 1: Topic (in topic tree format) - E.g. player.status.elapsed +Part 1: Topic (in topic tree format) +E.g. player.status.elapsed - Part 2: Payload or Message in json serialization - If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same +Part 2: Payload or Message in json serialization +If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same - Part 3: Command - Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer - and the message is not forwarded to the outside. This third part of the message is never forwarded +Part 3: Command +Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer +and the message is not forwarded to the outside. This third part of the message is never forwarded Connection (2): External connection - Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! - Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will - also get you all the branch topics. To get everything, subscribe to ``b''`` +Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! +Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will +also get you all the branch topics. To get everything, subscribe to ``b''`` - Protocol: Multi-part message +Protocol: Multi-part message - Part 1: Topic (in topic tree format) - E.g. player.status.elapsed +Part 1: Topic (in topic tree format) +E.g. player.status.elapsed - Part 2: Payload or Message in json serialization - If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) +Part 2: Payload or Message in json serialization +If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) Why? Why? ------------- @@ -4341,14 +4433,14 @@ For use case, we made a few simplifications Design Rationales ------------------- -* "If you need `millions of messages per second `_ - sent to thousands of points, - you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." +* "If you need `millions of messages per second `_ +sent to thousands of points, +you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then - the `XSUB and XPUB `_" +the `XSUB and XPUB `_" * "Let’s imagine `our feed has an average of 100,000 100-byte messages a second - `_ [...]. - While 100K messages a second is easy for a ZeroMQ application, ..." +`_ [...]. +While 100K messages a second is easy for a ZeroMQ application, ..." **But we have:** @@ -4369,8 +4461,8 @@ This means, we can use less complex patters than used for these high-speed, high * Late joining client (or drop-off and re-join): get full state update * Server crash etc: No special handling necessary, we are simple - and don't need recovery in this case. Server will publish initial state - after re-start +and don't need recovery in this case. Server will publish initial state +after re-start * Subscriber too slow: Subscribers problem (TODO: Do we need to do anything about it?) **Start-up sequence:** @@ -4389,22 +4481,22 @@ RPC can trigger through function call in components/publishing plugin that Plugins publishing state information should publish initial state at @plugin.finalize .. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is - required per thread. But the publisher instance **must** be thread-local! - Always go through :func:`publishing.get_publisher()`. +required per thread. But the publisher instance **must** be thread-local! +Always go through :func:`publishing.get_publisher()`. **Sockets** Three sockets are opened: -#. TCP (on a configurable port) -#. Websocket (on a configurable port) -#. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules - that want to know about the current state on event based updates. +``. TCP (on a configurable port) +``. Websocket (on a configurable port) +``. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules +that want to know about the current state on event based updates. **Further ZeroMQ References:** -* `Working with Messages `_ -* `Multiple Threads `_ +* `Working with Messages `_ +* `Multiple Threads `_ @@ -4419,7 +4511,7 @@ forwards them to the outside world Handles new subscriptions by sending out the entire cached state to **all** subscribers -The code is structures using a `Reactor Pattern `_ +The code is structures using a `Reactor Pattern `_ @@ -4473,7 +4565,9 @@ The publisher that provides the functional interface to the application def __init__(check_thread_owner=True) ``` -:param check_thread_owner: Check if send() is always called from the correct thread. This is debug feature +**Arguments**: + +- `check_thread_owner`: Check if send() is always called from the correct thread. This is debug feature and is intended to expose the situation before it leads to real trouble. Leave it on! @@ -4576,61 +4670,61 @@ you can implement a Factory Pattern using this package. Take a look at volume.py **Example:** Decorate a function for auto-registering under it's own name:: - import jukebox.plugs as plugs - @plugs.register - def func1(param): - pass +import jukebox.plugs as plugs +@plugs.register +def func1(param): +pass **Example:** Decorate a function for auto-registering under a new name:: - @plugs.register(name='better_name') - def func2(param): - pass +@plugs.register(name='better_name') +def func2(param): +pass **Example:** Register a function during run-time under it's own name:: - def func3(param): - pass - plugs.register(func3) +def func3(param): +pass +plugs.register(func3) **Example:** Register a function during run-time under a new name:: - def func4(param): - pass - plugs.register(func4, name='other_name', package='other_package') +def func4(param): +pass +plugs.register(func4, name='other_name', package='other_package') **Example:** Decorate a class for auto registering during initialization, including all methods (see _register_class for more info):: - @plugs.register(auto_tag=True) - class MyClass1: - pass +@plugs.register(auto_tag=True) +class MyClass1: +pass **Example:** Register a class instance, from which only report is a callable method through the plugs interface:: - class MyClass2: - @plugs.tag - def report(self): - pass - myinst2 = MyClass2() - plugin.register(myinst2, name='myinst2') +class MyClass2: +@plugs.tag +def report(self): +pass +myinst2 = MyClass2() +plugin.register(myinst2, name='myinst2') Naming convention: package - 1. Either a python package - 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) +1. Either a python package +2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) plugin - 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) - 2. The string name to above object +1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) +2. The string name to above object name - The string name of the plugin object for registration +The string name of the plugin object for registration method - 1. In case the object is a class instance a bound method to call from the class instance - 2. The string name to above object +1. In case the object is a class instance a bound method to call from the class instance +2. The string name to above object @@ -4735,12 +4829,13 @@ For more documentation see the functions See the examples in Module :mod:`plugs` how to use this decorator / function -:param plugin: -:param name: -:param package: -:param replace: -:param auto_tag: -:return: +**Arguments**: + +- `plugin`: +- `name`: +- `package`: +- `replace`: +- `auto_tag`: @@ -4755,8 +4850,13 @@ Method decorator for tagging a method as callable through the plugs interface Note that the instantiated class must still be registered as plugin object (either with the class decorator or dynamically) -:param func: function to decorate -:return: the function +**Arguments**: + +- `func`: function to decorate + +**Returns**: + +the function @@ -4768,8 +4868,13 @@ def initialize(func: Callable) -> Callable Decorator for functions that shall be called by the plugs package directly after the module is loaded -:param func: Function to decorate -:return: The function itself +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself @@ -4781,8 +4886,13 @@ def finalize(func: Callable) -> Callable Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded -:param func: Function to decorate -:return: The function itself +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself @@ -4801,8 +4911,13 @@ Decorator for functions that shall be called by the plugs package directly after The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) It is intended for passing down the signal number that initiated the program termination -:param func: Function to decorate -:return: The function itself +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself @@ -4825,11 +4940,12 @@ Instead of using @initializer, you may of course use __init__.py Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under which they are loaded as plugin package also. -:param package: Python package to load as plugin package -:param load_as: Plugin package registration name. If None the name is the python's package simple name -:param prefix: Prefix to python package to create fully qualified name. This is used only to locate the python package - and ignored otherwise. Useful if all the plugin module are in a dedicated folder -:return: +**Arguments**: + +- `package`: Python package to load as plugin package +- `load_as`: Plugin package registration name. If None the name is the python's package simple name +- `prefix`: Prefix to python package to create fully qualified name. This is used only to locate the python package +and ignored otherwise. Useful if all the plugin module are in a dedicated folder @@ -4843,7 +4959,9 @@ def load_all_named(packages_named: Mapping[str, str], Load all packages in packages_named with mapped names -:param packages_named: Dict[load_as, package] +**Arguments**: + +- `packages_named`: Dict[load_as, package] @@ -4883,7 +5001,7 @@ Modules are processed in reverse order. Several at-exit tagged functions of a si in the order of registration. Errors raised in functions are suppressed to ensure all plugins are processed -:return: + @@ -4917,18 +5035,23 @@ Calls are serialized by a thread lock. The thread lock is shared with call_ignor There is no logger in this function as they all belong up-level where the exceptions are handled. If you want logger messages instead of exceptions, use :func:`call_ignore_errors` -:param package: Name of the plugin package in which to look for function/class instance -:param plugin: Function name or instance name of a class -:param method: Method name when accessing a class instance' method. Leave at *None* if unneeded. -:param as_thread: Run the callable in separate daemon thread. - There is no return value from the callable in this case! The return value is the thread object. - Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. - All threads are started as daemon threads with terminate upon main program termination. - There is not stop-thread mechanism. This is intended for short lived threads. -:param thread_name: Name of the thread -:param args: Arguments passed to callable -:param kwargs: Keyword arguments passed to callable -:return: The return value from the called function, or, if started as thread the thread object +**Arguments**: + +- `package`: Name of the plugin package in which to look for function/class instance +- `plugin`: Function name or instance name of a class +- `method`: Method name when accessing a class instance' method. Leave at *None* if unneeded. +- `as_thread`: Run the callable in separate daemon thread. +There is no return value from the callable in this case! The return value is the thread object. +Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. +All threads are started as daemon threads with terminate upon main program termination. +There is not stop-thread mechanism. This is intended for short lived threads. +- `thread_name`: Name of the thread +- `args`: Arguments passed to callable +- `kwargs`: Keyword arguments passed to callable + +**Returns**: + +The return value from the called function, or, if started as thread the thread object @@ -5043,7 +5166,9 @@ def get_all_loaded_packages() -> Dict[str, str] Report a short summary of all loaded packages -:return: Dictionary of the form `{loaded_as: loaded_from, ...}` +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` @@ -5063,7 +5188,9 @@ Report those packages that did not load error free Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED -:return: Dictionary of the form `{loaded_as: loaded_from, ...}` +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` @@ -5124,10 +5251,12 @@ Interface for plugin / RPC accessibility for a single event timer def __init__(name, wait_seconds: float, function, args=None, kwargs=None) ``` -:param wait_seconds: The time in seconds to wait before calling function -:param function: The function to call with args and kwargs. -:param args: Parameters for function call -:param kwargs: Parameters for function call +**Arguments**: + +- `wait_seconds`: The time in seconds to wait before calling function +- `function`: The function to call with args and kwargs. +- `args`: Parameters for function call +- `kwargs`: Parameters for function call @@ -5195,7 +5324,9 @@ def get_timeout() Get the configured time-out -:return: The total wait time. (Not the remaining wait time!) +**Returns**: + +The total wait time. (Not the remaining wait time!) @@ -5263,13 +5394,15 @@ def __init__(name, kwargs=None) ``` -:param iterations: Number of times callee is called -:param wait_seconds_per_iteration: Wait in seconds before each iteration -:param callee: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). +**Arguments**: + +- `iterations`: Number of times callee is called +- `wait_seconds_per_iteration`: Wait in seconds before each iteration +- `callee`: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). Then with every time out iteration __call__(*args, iteration=iteration, **kwargs) is called. 'iteration' is the current iteration count in decreasing order! -:param args: -:param kwargs: +- `args`: +- `kwargs`: @@ -5300,8 +5433,13 @@ Makes sure that the core rpc call parameters have valid default values in cfg_rp .. important: Leaves all other parameters in cfg_action untouched or later downstream processing! -:param cfg_rpc_call: RPC command as configuration entry -:return: A fully populated deep copy of cfg_rpc_call +**Arguments**: + +- `cfg_rpc_call`: RPC command as configuration entry + +**Returns**: + +A fully populated deep copy of cfg_rpc_call @@ -5325,9 +5463,14 @@ If an explicitly specified this is not done. However, it is ensured that the ret dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling for non-existing RPC commands and we get a clearer error message. -:param cfg_rpc_cmd: RPC command as configuration entry -:param logger: The logger to use -:return: A decoded, fully populated deep copy of cfg_rpc_cmd +**Arguments**: + +- `cfg_rpc_cmd`: RPC command as configuration entry +- `logger`: The logger to use + +**Returns**: + +A decoded, fully populated deep copy of cfg_rpc_cmd @@ -5351,21 +5494,24 @@ def bind_rpc_command(cfg_rpc_cmd: Dict, Decode an RPC command configuration entry and bind it to a function -:param dereference: Dereference even the call to plugs.call(...) +**Arguments**: - #. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with - all checks applied at bind time - #. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with - all checks applied at bind time. +- `dereference`: Dereference even the call to plugs.call(...) + ``. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with + all checks applied at bind time + ``. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with + all checks applied at bind time. - Setting deference to True, circumvents the dynamic nature of the plugins: the function to call - must exist at bind time and cannot change. If False, the function to call must only exist at call time. - This can be important during the initialization where package ordering and initialization means that not all - classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls - is circumvented. Use with care! +Setting deference to True, circumvents the dynamic nature of the plugins: the function to call + must exist at bind time and cannot change. If False, the function to call must only exist at call time. + This can be important during the initialization where package ordering and initialization means that not all + classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls + is circumvented. Use with care! -:return: Callable function w/o parameters which directly runs the RPC command - using plugs.call_ignore_errors +**Returns**: + +Callable function w/o parameters which directly runs the RPC command +using plugs.call_ignore_errors @@ -5377,8 +5523,10 @@ def rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) -> str Return a readable string of an RPC call config -:param cfg_rpc_call: RPC call configuration entry -:param with_args: Return string shall include the arguments of the function +**Arguments**: + +- `cfg_rpc_call`: RPC call configuration entry +- `with_args`: Return string shall include the arguments of the function @@ -5460,9 +5608,9 @@ If present, 'id' and 'tsp' may not be None. If they are None, there are treated Three sockets are opened -#. TCP (on a configurable port) -#. Websocket (on a configurable port) -#. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be +``. TCP (on a configurable port) +``. Websocket (on a configurable port) +``. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which button triggers what action) diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index ed606aa90..07c0eee20 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -1,8 +1,6 @@ loaders: - type: python search_path: [./src/jukebox] -processors: -- type: filter renderer: type: markdown render_toc: true From c0947687241422a33ff8daf50a96b0f54f6189dd Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 22 Dec 2023 22:42:12 +0100 Subject: [PATCH 09/41] use only pydoc-markdown --- documentation/developers/README.md | 1 + .../developers/docstring/docstring-api.md | 5648 +++++++++++++++++ pydoc-markdown.yml | 6 +- requirements.txt | 2 - run_docstring2markdown.sh | 10 +- 5 files changed, 5655 insertions(+), 12 deletions(-) create mode 100644 documentation/developers/docstring/docstring-api.md diff --git a/documentation/developers/README.md b/documentation/developers/README.md index 6c973d6ba..f98aa296e 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -9,5 +9,6 @@ * [Jukebox Apps](./coreapps.md) * [RFID Readers](./rfid) +* [Docstring API Docs (from py files)](./docstring/docstring-api.md) * [Feature Status](./status.md) * [Known Issues](./known-issues.md) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md new file mode 100644 index 000000000..b2184d060 --- /dev/null +++ b/documentation/developers/docstring/docstring-api.md @@ -0,0 +1,5648 @@ +# None + +## Table of Contents + +* [run\_jukebox](#run_jukebox) +* [\_\_init\_\_](#__init__) +* [run\_register\_rfid\_reader](#run_register_rfid_reader) +* [run\_rpc\_tool](#run_rpc_tool) + * [get\_common\_beginning](#run_rpc_tool.get_common_beginning) +* [run\_configure\_audio](#run_configure_audio) +* [run\_publicity\_sniffer](#run_publicity_sniffer) +* [misc](#misc) + * [recursive\_chmod](#misc.recursive_chmod) + * [flatten](#misc.flatten) + * [getattr\_hierarchical](#misc.getattr_hierarchical) +* [misc.inputminus](#misc.inputminus) + * [input\_int](#misc.inputminus.input_int) + * [input\_yesno](#misc.inputminus.input_yesno) +* [misc.loggingext](#misc.loggingext) + * [ColorFilter](#misc.loggingext.ColorFilter) + * [\_\_init\_\_](#misc.loggingext.ColorFilter.__init__) + * [PubStream](#misc.loggingext.PubStream) + * [PubStreamHandler](#misc.loggingext.PubStreamHandler) +* [misc.simplecolors](#misc.simplecolors) + * [Colors](#misc.simplecolors.Colors) + * [resolve](#misc.simplecolors.resolve) + * [print](#misc.simplecolors.print) +* [components](#components) +* [components.playermpd.playcontentcallback](#components.playermpd.playcontentcallback) + * [PlayContentCallbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks) + * [register](#components.playermpd.playcontentcallback.PlayContentCallbacks.register) + * [run\_callbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks.run_callbacks) +* [components.playermpd](#components.playermpd) + * [PlayerMPD](#components.playermpd.PlayerMPD) + * [mpd\_retry\_with\_mutex](#components.playermpd.PlayerMPD.mpd_retry_with_mutex) + * [pause](#components.playermpd.PlayerMPD.pause) + * [next](#components.playermpd.PlayerMPD.next) + * [rewind](#components.playermpd.PlayerMPD.rewind) + * [replay](#components.playermpd.PlayerMPD.replay) + * [toggle](#components.playermpd.PlayerMPD.toggle) + * [replay\_if\_stopped](#components.playermpd.PlayerMPD.replay_if_stopped) + * [play\_card](#components.playermpd.PlayerMPD.play_card) + * [get\_folder\_content](#components.playermpd.PlayerMPD.get_folder_content) + * [play\_folder](#components.playermpd.PlayerMPD.play_folder) + * [play\_album](#components.playermpd.PlayerMPD.play_album) + * [get\_volume](#components.playermpd.PlayerMPD.get_volume) + * [set\_volume](#components.playermpd.PlayerMPD.set_volume) + * [play\_card\_callbacks](#components.playermpd.play_card_callbacks) +* [components.rpc\_command\_alias](#components.rpc_command_alias) +* [components.synchronisation.rfidcards](#components.synchronisation.rfidcards) + * [SyncRfidcards](#components.synchronisation.rfidcards.SyncRfidcards) + * [sync\_change\_on\_rfid\_scan](#components.synchronisation.rfidcards.SyncRfidcards.sync_change_on_rfid_scan) + * [sync\_all](#components.synchronisation.rfidcards.SyncRfidcards.sync_all) + * [sync\_card\_database](#components.synchronisation.rfidcards.SyncRfidcards.sync_card_database) + * [sync\_folder](#components.synchronisation.rfidcards.SyncRfidcards.sync_folder) +* [components.synchronisation](#components.synchronisation) +* [components.synchronisation.syncutils](#components.synchronisation.syncutils) +* [components.volume](#components.volume) + * [PulseMonitor](#components.volume.PulseMonitor) + * [SoundCardConnectCallbacks](#components.volume.PulseMonitor.SoundCardConnectCallbacks) + * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) + * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) + * [stop](#components.volume.PulseMonitor.stop) + * [run](#components.volume.PulseMonitor.run) + * [PulseVolumeControl](#components.volume.PulseVolumeControl) + * [OutputChangeCallbackHandler](#components.volume.PulseVolumeControl.OutputChangeCallbackHandler) + * [OutputVolumeCallbackHandler](#components.volume.PulseVolumeControl.OutputVolumeCallbackHandler) + * [toggle\_output](#components.volume.PulseVolumeControl.toggle_output) + * [get\_outputs](#components.volume.PulseVolumeControl.get_outputs) + * [publish\_volume](#components.volume.PulseVolumeControl.publish_volume) + * [publish\_outputs](#components.volume.PulseVolumeControl.publish_outputs) + * [set\_volume](#components.volume.PulseVolumeControl.set_volume) + * [get\_volume](#components.volume.PulseVolumeControl.get_volume) + * [change\_volume](#components.volume.PulseVolumeControl.change_volume) + * [get\_mute](#components.volume.PulseVolumeControl.get_mute) + * [mute](#components.volume.PulseVolumeControl.mute) + * [set\_output](#components.volume.PulseVolumeControl.set_output) + * [set\_soft\_max\_volume](#components.volume.PulseVolumeControl.set_soft_max_volume) + * [get\_soft\_max\_volume](#components.volume.PulseVolumeControl.get_soft_max_volume) + * [card\_list](#components.volume.PulseVolumeControl.card_list) +* [components.rfid](#components.rfid) +* [components.rfid.reader](#components.rfid.reader) + * [RfidCardDetectCallbacks](#components.rfid.reader.RfidCardDetectCallbacks) + * [register](#components.rfid.reader.RfidCardDetectCallbacks.register) + * [run\_callbacks](#components.rfid.reader.RfidCardDetectCallbacks.run_callbacks) + * [rfid\_card\_detect\_callbacks](#components.rfid.reader.rfid_card_detect_callbacks) + * [CardRemovalTimerClass](#components.rfid.reader.CardRemovalTimerClass) + * [\_\_init\_\_](#components.rfid.reader.CardRemovalTimerClass.__init__) +* [components.rfid.configure](#components.rfid.configure) + * [reader\_install\_dependencies](#components.rfid.configure.reader_install_dependencies) + * [reader\_load\_module](#components.rfid.configure.reader_load_module) + * [query\_user\_for\_reader](#components.rfid.configure.query_user_for_reader) + * [write\_config](#components.rfid.configure.write_config) +* [components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui](#components.rfid.hardware.fake_reader_gui.fake_reader_gui) +* [components.rfid.hardware.fake\_reader\_gui.description](#components.rfid.hardware.fake_reader_gui.description) +* [components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon) + * [create\_inputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_inputs) + * [set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.set_state) + * [que\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_state) + * [fix\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.fix_state) + * [pbox\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.pbox_set_state) + * [que\_set\_pbox](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_pbox) + * [create\_outputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_outputs) +* [components.rfid.hardware.generic\_usb.description](#components.rfid.hardware.generic_usb.description) +* [components.rfid.hardware.generic\_usb.generic\_usb](#components.rfid.hardware.generic_usb.generic_usb) +* [components.rfid.hardware.rc522\_spi.description](#components.rfid.hardware.rc522_spi.description) +* [components.rfid.hardware.rc522\_spi.rc522\_spi](#components.rfid.hardware.rc522_spi.rc522_spi) +* [components.rfid.hardware.pn532\_i2c\_py532.description](#components.rfid.hardware.pn532_i2c_py532.description) +* [components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532](#components.rfid.hardware.pn532_i2c_py532.pn532_i2c_py532) +* [components.rfid.hardware.rdm6300\_serial.rdm6300\_serial](#components.rfid.hardware.rdm6300_serial.rdm6300_serial) + * [decode](#components.rfid.hardware.rdm6300_serial.rdm6300_serial.decode) +* [components.rfid.hardware.rdm6300\_serial.description](#components.rfid.hardware.rdm6300_serial.description) +* [components.rfid.hardware.template\_new\_reader.description](#components.rfid.hardware.template_new_reader.description) +* [components.rfid.hardware.template\_new\_reader.template\_new\_reader](#components.rfid.hardware.template_new_reader.template_new_reader) + * [query\_customization](#components.rfid.hardware.template_new_reader.template_new_reader.query_customization) + * [ReaderClass](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass) + * [\_\_init\_\_](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.__init__) + * [cleanup](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.cleanup) + * [stop](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.stop) + * [read\_card](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.read_card) +* [components.rfid.readerbase](#components.rfid.readerbase) + * [ReaderBaseClass](#components.rfid.readerbase.ReaderBaseClass) +* [components.rfid.cards](#components.rfid.cards) + * [list\_cards](#components.rfid.cards.list_cards) + * [delete\_card](#components.rfid.cards.delete_card) + * [register\_card](#components.rfid.cards.register_card) + * [register\_card\_custom](#components.rfid.cards.register_card_custom) + * [save\_card\_database](#components.rfid.cards.save_card_database) +* [components.rfid.cardutils](#components.rfid.cardutils) + * [decode\_card\_command](#components.rfid.cardutils.decode_card_command) + * [card\_command\_to\_str](#components.rfid.cardutils.card_command_to_str) + * [card\_to\_str](#components.rfid.cardutils.card_to_str) +* [components.publishing](#components.publishing) + * [republish](#components.publishing.republish) +* [components.player](#components.player) + * [MusicLibPath](#components.player.MusicLibPath) + * [get\_music\_library\_path](#components.player.get_music_library_path) +* [components.jingle](#components.jingle) + * [JingleFactory](#components.jingle.JingleFactory) + * [list](#components.jingle.JingleFactory.list) + * [play](#components.jingle.play) + * [play\_startup](#components.jingle.play_startup) + * [play\_shutdown](#components.jingle.play_shutdown) +* [components.jingle.alsawave](#components.jingle.alsawave) + * [AlsaWave](#components.jingle.alsawave.AlsaWave) + * [play](#components.jingle.alsawave.AlsaWave.play) + * [AlsaWaveBuilder](#components.jingle.alsawave.AlsaWaveBuilder) + * [\_\_init\_\_](#components.jingle.alsawave.AlsaWaveBuilder.__init__) +* [components.jingle.jinglemp3](#components.jingle.jinglemp3) + * [JingleMp3Play](#components.jingle.jinglemp3.JingleMp3Play) + * [play](#components.jingle.jinglemp3.JingleMp3Play.play) + * [JingleMp3PlayBuilder](#components.jingle.jinglemp3.JingleMp3PlayBuilder) + * [\_\_init\_\_](#components.jingle.jinglemp3.JingleMp3PlayBuilder.__init__) +* [components.hostif.linux](#components.hostif.linux) + * [shutdown](#components.hostif.linux.shutdown) + * [reboot](#components.hostif.linux.reboot) + * [jukebox\_is\_service](#components.hostif.linux.jukebox_is_service) + * [is\_any\_jukebox\_service\_active](#components.hostif.linux.is_any_jukebox_service_active) + * [restart\_service](#components.hostif.linux.restart_service) + * [get\_disk\_usage](#components.hostif.linux.get_disk_usage) + * [get\_cpu\_temperature](#components.hostif.linux.get_cpu_temperature) + * [get\_ip\_address](#components.hostif.linux.get_ip_address) + * [wlan\_disable\_power\_down](#components.hostif.linux.wlan_disable_power_down) + * [get\_autohotspot\_status](#components.hostif.linux.get_autohotspot_status) + * [stop\_autohotspot](#components.hostif.linux.stop_autohotspot) + * [start\_autohotspot](#components.hostif.linux.start_autohotspot) +* [components.misc](#components.misc) + * [rpc\_cmd\_help](#components.misc.rpc_cmd_help) + * [get\_all\_loaded\_packages](#components.misc.get_all_loaded_packages) + * [get\_all\_failed\_packages](#components.misc.get_all_failed_packages) + * [get\_start\_time](#components.misc.get_start_time) + * [get\_log](#components.misc.get_log) + * [get\_log\_debug](#components.misc.get_log_debug) + * [get\_log\_error](#components.misc.get_log_error) + * [get\_git\_state](#components.misc.get_git_state) + * [empty\_rpc\_call](#components.misc.empty_rpc_call) +* [components.controls](#components.controls) +* [components.controls.bluetooth\_audio\_buttons](#components.controls.bluetooth_audio_buttons) +* [components.controls.common.evdev\_listener](#components.controls.common.evdev_listener) + * [find\_device](#components.controls.common.evdev_listener.find_device) + * [EvDevKeyListener](#components.controls.common.evdev_listener.EvDevKeyListener) + * [\_\_init\_\_](#components.controls.common.evdev_listener.EvDevKeyListener.__init__) + * [run](#components.controls.common.evdev_listener.EvDevKeyListener.run) + * [start](#components.controls.common.evdev_listener.EvDevKeyListener.start) +* [components.music\_cover\_art](#components.music_cover_art) + * [MusicCoverArt](#components.music_cover_art.MusicCoverArt) + * [get\_by\_filename\_as\_base64](#components.music_cover_art.MusicCoverArt.get_by_filename_as_base64) +* [components.battery\_monitor](#components.battery_monitor) +* [components.battery\_monitor.BatteryMonitorBase](#components.battery_monitor.BatteryMonitorBase) + * [pt1\_frac](#components.battery_monitor.BatteryMonitorBase.pt1_frac) + * [BattmonBase](#components.battery_monitor.BatteryMonitorBase.BattmonBase) +* [components.battery\_monitor.batt\_mon\_simulator](#components.battery_monitor.batt_mon_simulator) + * [battmon\_simulator](#components.battery_monitor.batt_mon_simulator.battmon_simulator) +* [components.battery\_monitor.batt\_mon\_i2c\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015) + * [battmon\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015.battmon_ads1015) +* [components.gpio.gpioz.plugin](#components.gpio.gpioz.plugin) + * [output\_devices](#components.gpio.gpioz.plugin.output_devices) + * [input\_devices](#components.gpio.gpioz.plugin.input_devices) + * [factory](#components.gpio.gpioz.plugin.factory) + * [IS\_ENABLED](#components.gpio.gpioz.plugin.IS_ENABLED) + * [IS\_MOCKED](#components.gpio.gpioz.plugin.IS_MOCKED) + * [CONFIG\_FILE](#components.gpio.gpioz.plugin.CONFIG_FILE) + * [ServiceIsRunningCallbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks) + * [register](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.register) + * [run\_callbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.run_callbacks) + * [service\_is\_running\_callbacks](#components.gpio.gpioz.plugin.service_is_running_callbacks) + * [build\_output\_device](#components.gpio.gpioz.plugin.build_output_device) + * [build\_input\_device](#components.gpio.gpioz.plugin.build_input_device) + * [get\_output](#components.gpio.gpioz.plugin.get_output) + * [on](#components.gpio.gpioz.plugin.on) + * [off](#components.gpio.gpioz.plugin.off) + * [set\_value](#components.gpio.gpioz.plugin.set_value) + * [flash](#components.gpio.gpioz.plugin.flash) +* [components.gpio.gpioz.plugin.connectivity](#components.gpio.gpioz.plugin.connectivity) + * [BUZZ\_TONE](#components.gpio.gpioz.plugin.connectivity.BUZZ_TONE) + * [register\_rfid\_callback](#components.gpio.gpioz.plugin.connectivity.register_rfid_callback) + * [register\_status\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_led_callback) + * [register\_status\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_buzzer_callback) + * [register\_status\_tonalbuzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_tonalbuzzer_callback) + * [register\_audio\_sink\_change\_callback](#components.gpio.gpioz.plugin.connectivity.register_audio_sink_change_callback) + * [register\_volume\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_led_callback) + * [register\_volume\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_buzzer_callback) + * [register\_volume\_rgbled\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_rgbled_callback) +* [components.gpio.gpioz.core.converter](#components.gpio.gpioz.core.converter) + * [ColorProperty](#components.gpio.gpioz.core.converter.ColorProperty) + * [VolumeToRGB](#components.gpio.gpioz.core.converter.VolumeToRGB) + * [\_\_call\_\_](#components.gpio.gpioz.core.converter.VolumeToRGB.__call__) + * [luminize](#components.gpio.gpioz.core.converter.VolumeToRGB.luminize) +* [components.gpio.gpioz.core.mock](#components.gpio.gpioz.core.mock) + * [patch\_mock\_outputs\_with\_callback](#components.gpio.gpioz.core.mock.patch_mock_outputs_with_callback) +* [components.gpio.gpioz.core.input\_devices](#components.gpio.gpioz.core.input_devices) + * [NameMixin](#components.gpio.gpioz.core.input_devices.NameMixin) + * [set\_rpc\_actions](#components.gpio.gpioz.core.input_devices.NameMixin.set_rpc_actions) + * [EventProperty](#components.gpio.gpioz.core.input_devices.EventProperty) + * [ButtonBase](#components.gpio.gpioz.core.input_devices.ButtonBase) + * [value](#components.gpio.gpioz.core.input_devices.ButtonBase.value) + * [pin](#components.gpio.gpioz.core.input_devices.ButtonBase.pin) + * [pull\_up](#components.gpio.gpioz.core.input_devices.ButtonBase.pull_up) + * [close](#components.gpio.gpioz.core.input_devices.ButtonBase.close) + * [Button](#components.gpio.gpioz.core.input_devices.Button) + * [on\_press](#components.gpio.gpioz.core.input_devices.Button.on_press) + * [LongPressButton](#components.gpio.gpioz.core.input_devices.LongPressButton) + * [on\_press](#components.gpio.gpioz.core.input_devices.LongPressButton.on_press) + * [ShortLongPressButton](#components.gpio.gpioz.core.input_devices.ShortLongPressButton) + * [RotaryEncoder](#components.gpio.gpioz.core.input_devices.RotaryEncoder) + * [pin\_a](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_a) + * [pin\_b](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_b) + * [on\_rotate\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_clockwise) + * [on\_rotate\_counter\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_counter_clockwise) + * [close](#components.gpio.gpioz.core.input_devices.RotaryEncoder.close) + * [TwinButton](#components.gpio.gpioz.core.input_devices.TwinButton) + * [StateVar](#components.gpio.gpioz.core.input_devices.TwinButton.StateVar) + * [close](#components.gpio.gpioz.core.input_devices.TwinButton.close) + * [value](#components.gpio.gpioz.core.input_devices.TwinButton.value) + * [is\_active](#components.gpio.gpioz.core.input_devices.TwinButton.is_active) +* [components.gpio.gpioz.core.output\_devices](#components.gpio.gpioz.core.output_devices) + * [LED](#components.gpio.gpioz.core.output_devices.LED) + * [flash](#components.gpio.gpioz.core.output_devices.LED.flash) + * [Buzzer](#components.gpio.gpioz.core.output_devices.Buzzer) + * [flash](#components.gpio.gpioz.core.output_devices.Buzzer.flash) + * [PWMLED](#components.gpio.gpioz.core.output_devices.PWMLED) + * [flash](#components.gpio.gpioz.core.output_devices.PWMLED.flash) + * [RGBLED](#components.gpio.gpioz.core.output_devices.RGBLED) + * [flash](#components.gpio.gpioz.core.output_devices.RGBLED.flash) + * [TonalBuzzer](#components.gpio.gpioz.core.output_devices.TonalBuzzer) + * [flash](#components.gpio.gpioz.core.output_devices.TonalBuzzer.flash) + * [melody](#components.gpio.gpioz.core.output_devices.TonalBuzzer.melody) +* [components.timers](#components.timers) +* [jukebox](#jukebox) +* [jukebox.callingback](#jukebox.callingback) + * [CallbackHandler](#jukebox.callingback.CallbackHandler) + * [register](#jukebox.callingback.CallbackHandler.register) + * [run\_callbacks](#jukebox.callingback.CallbackHandler.run_callbacks) + * [has\_callbacks](#jukebox.callingback.CallbackHandler.has_callbacks) +* [jukebox.version](#jukebox.version) + * [version](#jukebox.version.version) + * [version\_info](#jukebox.version.version_info) +* [jukebox.cfghandler](#jukebox.cfghandler) + * [ConfigHandler](#jukebox.cfghandler.ConfigHandler) + * [loaded\_from](#jukebox.cfghandler.ConfigHandler.loaded_from) + * [get](#jukebox.cfghandler.ConfigHandler.get) + * [setdefault](#jukebox.cfghandler.ConfigHandler.setdefault) + * [getn](#jukebox.cfghandler.ConfigHandler.getn) + * [setn](#jukebox.cfghandler.ConfigHandler.setn) + * [setndefault](#jukebox.cfghandler.ConfigHandler.setndefault) + * [config\_dict](#jukebox.cfghandler.ConfigHandler.config_dict) + * [is\_modified](#jukebox.cfghandler.ConfigHandler.is_modified) + * [clear\_modified](#jukebox.cfghandler.ConfigHandler.clear_modified) + * [save](#jukebox.cfghandler.ConfigHandler.save) + * [load](#jukebox.cfghandler.ConfigHandler.load) + * [get\_handler](#jukebox.cfghandler.get_handler) + * [load\_yaml](#jukebox.cfghandler.load_yaml) + * [write\_yaml](#jukebox.cfghandler.write_yaml) +* [jukebox.playlistgenerator](#jukebox.playlistgenerator) + * [TYPE\_DECODE](#jukebox.playlistgenerator.TYPE_DECODE) + * [PlaylistCollector](#jukebox.playlistgenerator.PlaylistCollector) + * [\_\_init\_\_](#jukebox.playlistgenerator.PlaylistCollector.__init__) + * [set\_exclusion\_endings](#jukebox.playlistgenerator.PlaylistCollector.set_exclusion_endings) + * [get\_directory\_content](#jukebox.playlistgenerator.PlaylistCollector.get_directory_content) + * [parse](#jukebox.playlistgenerator.PlaylistCollector.parse) +* [jukebox.NvManager](#jukebox.NvManager) +* [jukebox.publishing](#jukebox.publishing) + * [get\_publisher](#jukebox.publishing.get_publisher) +* [jukebox.publishing.subscriber](#jukebox.publishing.subscriber) +* [jukebox.publishing.server](#jukebox.publishing.server) + * [PublishServer](#jukebox.publishing.server.PublishServer) + * [run](#jukebox.publishing.server.PublishServer.run) + * [handle\_message](#jukebox.publishing.server.PublishServer.handle_message) + * [handle\_subscription](#jukebox.publishing.server.PublishServer.handle_subscription) + * [Publisher](#jukebox.publishing.server.Publisher) + * [\_\_init\_\_](#jukebox.publishing.server.Publisher.__init__) + * [send](#jukebox.publishing.server.Publisher.send) + * [revoke](#jukebox.publishing.server.Publisher.revoke) + * [resend](#jukebox.publishing.server.Publisher.resend) + * [close\_server](#jukebox.publishing.server.Publisher.close_server) +* [jukebox.daemon](#jukebox.daemon) + * [log\_active\_threads](#jukebox.daemon.log_active_threads) + * [JukeBox](#jukebox.daemon.JukeBox) + * [signal\_handler](#jukebox.daemon.JukeBox.signal_handler) +* [jukebox.plugs](#jukebox.plugs) + * [PluginPackageClass](#jukebox.plugs.PluginPackageClass) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [tag](#jukebox.plugs.tag) + * [initialize](#jukebox.plugs.initialize) + * [finalize](#jukebox.plugs.finalize) + * [atexit](#jukebox.plugs.atexit) + * [load](#jukebox.plugs.load) + * [load\_all\_named](#jukebox.plugs.load_all_named) + * [load\_all\_unnamed](#jukebox.plugs.load_all_unnamed) + * [load\_all\_finalize](#jukebox.plugs.load_all_finalize) + * [close\_down](#jukebox.plugs.close_down) + * [call](#jukebox.plugs.call) + * [call\_ignore\_errors](#jukebox.plugs.call_ignore_errors) + * [exists](#jukebox.plugs.exists) + * [get](#jukebox.plugs.get) + * [loaded\_as](#jukebox.plugs.loaded_as) + * [delete](#jukebox.plugs.delete) + * [dump\_plugins](#jukebox.plugs.dump_plugins) + * [summarize](#jukebox.plugs.summarize) + * [generate\_help\_rst](#jukebox.plugs.generate_help_rst) + * [get\_all\_loaded\_packages](#jukebox.plugs.get_all_loaded_packages) + * [get\_all\_failed\_packages](#jukebox.plugs.get_all_failed_packages) +* [jukebox.speaking\_text](#jukebox.speaking_text) +* [jukebox.multitimer](#jukebox.multitimer) + * [MultiTimer](#jukebox.multitimer.MultiTimer) + * [cancel](#jukebox.multitimer.MultiTimer.cancel) + * [GenericTimerClass](#jukebox.multitimer.GenericTimerClass) + * [\_\_init\_\_](#jukebox.multitimer.GenericTimerClass.__init__) + * [start](#jukebox.multitimer.GenericTimerClass.start) + * [cancel](#jukebox.multitimer.GenericTimerClass.cancel) + * [toggle](#jukebox.multitimer.GenericTimerClass.toggle) + * [trigger](#jukebox.multitimer.GenericTimerClass.trigger) + * [is\_alive](#jukebox.multitimer.GenericTimerClass.is_alive) + * [get\_timeout](#jukebox.multitimer.GenericTimerClass.get_timeout) + * [set\_timeout](#jukebox.multitimer.GenericTimerClass.set_timeout) + * [publish](#jukebox.multitimer.GenericTimerClass.publish) + * [get\_state](#jukebox.multitimer.GenericTimerClass.get_state) + * [GenericEndlessTimerClass](#jukebox.multitimer.GenericEndlessTimerClass) + * [GenericMultiTimerClass](#jukebox.multitimer.GenericMultiTimerClass) + * [\_\_init\_\_](#jukebox.multitimer.GenericMultiTimerClass.__init__) + * [start](#jukebox.multitimer.GenericMultiTimerClass.start) +* [jukebox.utils](#jukebox.utils) + * [decode\_rpc\_call](#jukebox.utils.decode_rpc_call) + * [decode\_rpc\_command](#jukebox.utils.decode_rpc_command) + * [decode\_and\_call\_rpc\_command](#jukebox.utils.decode_and_call_rpc_command) + * [bind\_rpc\_command](#jukebox.utils.bind_rpc_command) + * [rpc\_call\_to\_str](#jukebox.utils.rpc_call_to_str) + * [generate\_cmd\_alias\_rst](#jukebox.utils.generate_cmd_alias_rst) + * [generate\_cmd\_alias\_reference](#jukebox.utils.generate_cmd_alias_reference) + * [get\_git\_state](#jukebox.utils.get_git_state) +* [jukebox.rpc](#jukebox.rpc) +* [jukebox.rpc.client](#jukebox.rpc.client) +* [jukebox.rpc.server](#jukebox.rpc.server) + * [RpcServer](#jukebox.rpc.server.RpcServer) + * [\_\_init\_\_](#jukebox.rpc.server.RpcServer.__init__) + * [run](#jukebox.rpc.server.RpcServer.run) + + + +# run\_jukebox + +This is the main app and starts the Jukebox Core. + +Usually this runs as a service, which is started automatically after boot-up. At times, it may be necessary to restart +the service. +For example after a configuration change. Not all configuration changes can be applied on-the-fly. +See :ref:`userguide/configuration:Jukebox Configuration`. + +For debugging, it is usually desirable to run the Jukebox directly from the console rather than +as service. This gives direct logging info in the console and allows changing command line parameters. +See :ref:`userguide/troubleshooting:Troubleshooting`. + + + +# \_\_init\_\_ + + + +# run\_register\_rfid\_reader + +Setup tool to configure the RFID Readers. + +Run this once to register and configure the RFID readers with the Jukebox. Can be re-run at any time to change +the settings. For more information see :ref:`rfid/rfid:RFID Readers`. + +.. note:: This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). + Any manual modifications to the settings will have to be re-applied + + + +# run\_rpc\_tool + +Command Line Interface to the Jukebox RPC Server + +A command line tool for sending RPC commands to the running jukebox app. +This uses the same interface as the WebUI. Can be used for additional control +or for debugging. + +The tool features auto-completion and command history. + +The list of available commands is fetched from the running Jukebox service. + +.. todo: + - kwargs support + + + +#### get\_common\_beginning + +```python +def get_common_beginning(strings) +``` + +Return the strings that are common to the beginning of each string in the strings list. + + + +# run\_configure\_audio + +Setup tool to register the PulseAudio sinks as primary and secondary audio outputs. + +Will also setup equalizer and mono down mixer in the pulseaudio config file. + +Run this once after installation. Can be re-run at any time to change the settings. +For more information see :ref:`userguide/audio:Audio Configuration`. + + + +# run\_publicity\_sniffer + +A command line tool that monitors all messages being sent out from the +Jukebox via the publishing interface. Received messages are printed in the console. +Mainly used for debugging. + + + +# misc + + + +#### recursive\_chmod + +```python +def recursive_chmod(path, mode_files, mode_dirs) +``` + +Recursively change folder and file permissions + +mode_files/mode dirs can be given in octal notation e.g. 0o777 +flags from the stats module. + +Reference: https://docs.python.org/3/library/os.html#os.chmod + + + +#### flatten + +```python +def flatten(iterable) +``` + +Flatten all levels of hierarchy in nested iterables + + + +#### getattr\_hierarchical + +```python +def getattr_hierarchical(obj: Any, name: str) -> Any +``` + +Like the builtin getattr, but descends though the hierarchy levels + + + +# misc.inputminus + +Zero 3rd-party dependency module for user prompting + +Yes, there are modules out there to do the same and they have more features. +However, this is low-complexity and has zero dependencies + + + +#### input\_int + +```python +def input_int(prompt, + blank=None, + min=None, + max=None, + prompt_color=None, + prompt_hint=False) -> int +``` + +Request an integer input from user + +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `min`: Minimum valid integer value (None disables this check) +- `max`: Maximum valid integer value (None disables this check) +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [min...max, default=xx] to end of prompt + +**Returns**: + +integer value read from user input + + + +#### input\_yesno + +```python +def input_yesno(prompt, + blank=None, + prompt_color=None, + prompt_hint=False) -> bool +``` + +Request a yes / no choice from user + +Accepts multiple input for true/false and is case insensitive + +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized + +**Returns**: + +boolean value read from user input + + + +# misc.loggingext + +############## +Logger +############## +We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. + +The top-level logger is called 'jb' (to make it short). In any module you may simple create a child-logger at any hierarchy +level below 'jb'. It will inherit settings from it's parent logger unless otherwise configured in the yaml file. +Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be +created on the spot. + +:Example: How to get logger and log away at your heart's content: +>>> import logging +>>> logger = logging.getLogger('jb.awesome_module') +>>> logger.info('Started general awesomeness aura') + +Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: +`` +loggers: +jb: +level: WARNING +handlers: [console, debug_file_handler, error_file_handler] +propagate: no +jb.awesome_module: +level: DEBUG +`` + +.. note:: +The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense) +There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output + + + +## ColorFilter Objects + +```python +class ColorFilter(logging.Filter) +``` + +This filter adds colors to the logger + +It adds all colors from simplecolors by using the color name as new keyword, +i.e. use %(colorname)c or {colorname} in the formatter string + +It also adds the keyword {levelnameColored} which is an auto-colored drop-in replacement +for the levelname depending on severity. + +Don't forget to {reset} the color settings at the end of the string. + + + +#### \_\_init\_\_ + +```python +def __init__(enable=True, color_levelname=True) +``` + +**Arguments**: + +- `enable`: Enable the coloring +- `color_levelname`: Enable auto-coloring when using the levelname keyword + + + +## PubStream Objects + +```python +class PubStream() +``` + +" +Stream handler wrapper around the publisher for logging.StreamHandler + +Allows logging to send all log information (based on logging configuration) +to the Publisher. + +ATTENTION: This can lead to recursions! + +Recursions come up when +(a) Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, + which causes a send, ..... +(b) Publisher initialization emits logs, which need a Publisher instance to send logs + +IMPORTANT: To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the +functions in the send-function stack! + + + +## PubStreamHandler Objects + +```python +class PubStreamHandler(logging.StreamHandler) +``` + +Wrapper for logging.StreamHandler with stream = PubStream + +This serves one purpose: In logger.yaml custom handlers +can be configured (which are automatically instantiated). +Using this Handler, we can output to PubStream whithout +support code to instantiate PubStream keeping this file generic + + + +# misc.simplecolors + +Zero 3rd-party dependency module to add colors to unix terminal output + +Yes, there are modules out there to do the same and they have more features. +However, this is low-complexity and has zero dependencies + + + +## Colors Objects + +```python +class Colors() +``` + +Container class for all the colors as constants + + + +#### resolve + +```python +def resolve(color_name: str) +``` + +Resolve a color name into the respective color constant + +**Arguments**: + +- `color_name`: Name of the color + +**Returns**: + +color constant + + + +#### print + +```python +def print(color: Colors, + *values, + sep=' ', + end='\n', + file=sys.stdout, + flush=False) +``` + +Drop-in replacement for print with color choice and auto color reset for convenience + +Use just as a regular print function, but with first parameter as color + + + +# components + + + +# components.playermpd.playcontentcallback + + + +## PlayContentCallbacks Objects + +```python +class PlayContentCallbacks(Generic[STATE], CallbackHandler) +``` + +Callbacks are executed in various play functions + + + +#### register + +```python +def register(func: Callable[[str, STATE], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(folder: str, state: STATE) + :noindex: + + :param folder: relativ path to folder to play + :param state: indicator of the state inside the calling + + + + +#### run\_callbacks + +```python +def run_callbacks(folder: str, state: STATE) +``` + +:meta private: + + + +# components.playermpd + +Package for interfacing with the MPD Music Player Daemon + +Status information in three topics +1) Player Status: published only on change +This is a subset of the MPD status (and not the full MPD status) ?? +- folder +- song +- volume (volume is published only via player status, and not separatly to avoid too many Threads) +- ... +2) Elapsed time: published every 250 ms, unless constant +- elapsed +3) Folder Config: published only on change +This belongs to the folder being played +Publish: +- random, resume, single, loop +On save store this information: +Contains the information for resume functionality of each folder +- random, resume, single, loop +- if resume: +- current song, elapsed +- what is PLAYSTATUS for? +When to save +- on stop +Angstsave: +- on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) +- on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) +Load checks: +- if resume, but no song, elapsed -> log error and start from the beginning + +Status storing: +- Folder config for each folder (see above) +- Information to restart last folder playback, which is: +- last_folder -> folder_on_close +- song, elapsed +- random, resume, single, loop +- if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! +on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card + +Internal status +- last played folder: Needed to detect second swipe + + +Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, +'audio_folder_status': +{'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, +'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} + +**References**: + + https://github.com/Mic92/python-mpd2 + https://python-mpd2.readthedocs.io/en/latest/topics/commands.html + https://mpd.readthedocs.io/en/latest/protocol.html + + sudo -u mpd speaker-test -t wav -c 2 + + + +## PlayerMPD Objects + +```python +class PlayerMPD() +``` + +Interface to MPD Music Player Daemon + + + +#### mpd\_retry\_with\_mutex + +```python +def mpd_retry_with_mutex(mpd_cmd, *args) +``` + +This method adds thread saftey for acceses to mpd via a mutex lock, +it shall be used for each access to mpd to ensure thread safety +In case of a communication error the connection will be reestablished and the pending command will be repeated 2 times + +I think this should be refactored to a decorator + + + +#### pause + +```python +@plugs.tag +def pause(state: int = 1) +``` + +Enforce pause to state (1: pause, 0: resume) + +This is what you want as card removal action: pause the playback, so it can be resumed when card is placed +on the reader again. What happens on re-placement depends on configured second swipe option + + + +#### next + +```python +@plugs.tag +def next() +``` + +Play next track in current playlist + + + +#### rewind + +```python +@plugs.tag +def rewind() +``` + +Re-start current playlist from first track + +Note: Will not re-read folder config, but leave settings untouched + + + +#### replay + +```python +@plugs.tag +def replay() +``` + +Re-start playing the last-played folder + +Will reset settings to folder config + + + +#### toggle + +```python +@plugs.tag +def toggle() +``` + +Toggle pause state, i.e. do a pause / resume depending on current state + + + +#### replay\_if\_stopped + +```python +@plugs.tag +def replay_if_stopped() +``` + +Re-start playing the last-played folder unless playlist is still playing + +.. note:: To me this seems much like the behaviour of play, + but we keep it as it is specifically implemented in box 2.X + + + +#### play\_card + +```python +@plugs.tag +def play_card(folder: str, recursive: bool = False) +``` + +Main entry point for trigger music playing from RFID reader. Decodes second swipe options before playing folder content + +Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action +accordingly. + +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively + + + +#### get\_folder\_content + +```python +@plugs.tag +def get_folder_content(folder: str) +``` + +Get the folder content as content list with meta-information. Depth is always 1. + +Call repeatedly to descend in hierarchy + +**Arguments**: + +- `folder`: Folder path relative to music library path + + + +#### play\_folder + +```python +@plugs.tag +def play_folder(folder: str, recursive: bool = False) -> None +``` + +Playback a music folder. + +Folder content is added to the playlist as described by :mod:`jukebox.playlistgenerator`. +The playlist is cleared first. + +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively + + + +#### play\_album + +```python +@plugs.tag +def play_album(albumartist: str, album: str) +``` + +Playback a album found in MPD database. + +All album songs are added to the playlist +The playlist is cleared first. + +**Arguments**: + +- `albumartist`: Artist of the Album provided by MPD database +- `album`: Album name provided by MPD database + + + +#### get\_volume + +```python +def get_volume() +``` + +Get the current volume + +For volume control do not use directly, but use through the plugin 'volume', +as the user may have configured a volume control manager other than MPD + + + +#### set\_volume + +```python +def set_volume(volume) +``` + +Set the volume + +For volume control do not use directly, but use through the plugin 'volume', +as the user may have configured a volume control manager other than MPD + + + +#### play\_card\_callbacks + +Callback handler instance for play_card events. +- is executed when play_card function is called +States: +- See :class:`PlayCardState` +See :class:`PlayContentCallbacks` + + + +# components.rpc\_command\_alias + +This file provides definitions for RPC command aliases + +See :ref:`userguide/rpc_commands` + + + +# components.synchronisation.rfidcards + +Handles the synchronisation of RFID cards (audiofolder and card database entries). + +sync-all -> all card entries and audiofolders are synced from remote including deletions +sync-on-scan -> only the entry and audiofolder for the cardId will be synced from remote. + Deletions are only performed on files and subfolder inside the audiofolder. + A deletion of the audiofolder itself on remote side will not be propagated. + +card database: +On synchronisation the remote file will not be synced with the original cards database, but rather a local copy. +If a full sync is performed, the state is written back to the original file. +If a single card sync is performed, only the state of the specific cardId is updated in the original file. +This is done to allow to play audio offline. +Otherwise we would also update other cardIds where the audiofolders have not been synced yet. +The local copy is kept to reduce unnecessary syncing. + + + +## SyncRfidcards Objects + +```python +class SyncRfidcards() +``` + +Control class for sync RFID cards functionality + + + +#### sync\_change\_on\_rfid\_scan + +```python +@plugs.tag +def sync_change_on_rfid_scan(option: str = 'toggle') -> None +``` + +Change activation of 'on_rfid_scan_enabled' + +**Arguments**: + +- `option`: Must be one of 'enable', 'disable', 'toggle' + + + +#### sync\_all + +```python +@plugs.tag +def sync_all() -> bool +``` + +Sync all audiofolder and cardids from the remote server. +Removes local entries not existing at the remote server. + + + +#### sync\_card\_database + +```python +@plugs.tag +def sync_card_database(card_id: str) -> bool +``` + +Sync the card database from the remote server, if existing. + +If card_id is provided only this entry is updated. + +**Arguments**: + +- `card_id`: The cardid to update + + + +#### sync\_folder + +```python +@plugs.tag +def sync_folder(folder: str) -> bool +``` + +Sync the folder from the remote server, if existing + +**Arguments**: + +- `folder`: Folder path relative to music library path + + + +# components.synchronisation + + + +# components.synchronisation.syncutils + + + +# components.volume + +PulseAudio Volume Control Plugin Package + +Features + + * Volume Control + * Two outputs + * Watcher thread on volume / output change + +Publishes + + * volume.level + * volume.sink + +PulseAudio References + +https://brokkr.net/2018/05/24/down-the-drain-the-elusive-default-pulseaudio-sink/ + +Check fallback device (on device de-connect): +$ pacmd list-sinks | grep -e 'name:' -e 'index' + + +Integration + +Pulse Audio runs as a user process. Processes who want to communicate / stream to it +must also run as a user process. + +This means must also run as user process, as described in :ref:`userguide/system:Music Player Daemon (MPD)` + +Misc + +PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module +with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration +in ``/usr/pulse/default.pa``. So, we don't need to worry about it. +If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs +from the Jukebox. Remove it from the configuration! + +.. code-block:: text + + ### Use hot-plugged devices like Bluetooth or USB automatically (LP: `1702794`) + ### not available on PI? + .ifexists module-switch-on-connect.so + load-module module-switch-on-connect + .endif + +Why PulseAudio? + +The audio configuration of the system is one of those topics, +which has a myriad of options and possibilities. Every system is different and PulseAudio unifies these and +makes our life easier. Besides, it is only option to support Bluetooth at the moment. + +Callbacks: + +The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): + + ``. :func:`add_on_connect_callback` + ``. :func:`add_on_output_change_callbacks` + ``. :func:`add_on_volume_change_callback` + + + +## PulseMonitor Objects + +```python +class PulseMonitor(threading.Thread) +``` + +A thread for monitoring and interacting with the Pulse Lib via pulsectrl + +Whenever we want to access pulsectl, we need to exit the event listen loop. +This is handled by the context manager. It stops the event loop and returns +the pulsectl instance to be used (it does no return the monitor thread itself!) + +The context manager also locks the module to ensure proper thread sequencing, +as only a single thread may work with pulsectl at any time. Currently, an RLock is +used, even if it may not be necessary + + + +## SoundCardConnectCallbacks Objects + +```python +class SoundCardConnectCallbacks(CallbackHandler) +``` + +Callbacks are executed when + + * new sound card gets connected + + + +#### register + +```python +def register(func: Callable[[str, str], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(card_driver: str, device_name: str) + :noindex: + + :param card_driver: The PulseAudio card driver module, + e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` + :param device_name: The sound card device name as reported + in device properties + + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + +:meta private: + + + +#### toggle\_on\_connect + +```python +@property +def toggle_on_connect() +``` + +Returns :data:`True` if the sound card shall be changed when a new card connects/disconnects. Setting this +property changes the behavior. + +.. note:: A new card is always assumed to be the secondary device from the audio configuration. + At the moment there is no check it actually is the configured device. This means any new + device connection will initiate the toggle. This, however, is no real issue as the RPi's audio + system will be relatively stable once setup + + + +#### toggle\_on\_connect + +```python +@toggle_on_connect.setter +def toggle_on_connect(state=True) +``` + +Toggle Doc 2 + + + +#### stop + +```python +def stop() +``` + +Stop the pulse monitor thread + + + +#### run + +```python +def run() -> None +``` + +Starts the pulse monitor thread + + + +## PulseVolumeControl Objects + +```python +class PulseVolumeControl() +``` + +Volume control manager for PulseAudio + +When accessing the pulse library, it needs to be put into a special +state. Which is ensured by the context manager + +.. code-block: python + + with pulse_monitor as pulse ... + + +All private functions starting with `_function_name` assume that this is ensured by +the calling function. All user functions acquire proper context! + + + +## OutputChangeCallbackHandler Objects + +```python +class OutputChangeCallbackHandler(CallbackHandler) +``` + +Callbacks are executed when + + * audio sink is changed + + + +#### register + +```python +def register(func: Callable[[str, str, int, int], None]) +``` + +Add a new callback function :attr:`func`. + +Parameters always give the valid audio sink. That means, if an error +occurred, all parameters are valid. + +Callback signature is + +.. py:function:: func(sink_name: str, alias: str, sink_index: int, error_state: int) + :noindex: + + :param sink_name: PulseAudio's sink name + :param alias: The alias for :attr:`sink_name` + :param sink_index: The index of the sink in the configuration list + :param error_state: 1 if there was an attempt to change the output + but an error occurred. Above parameters always give the now valid sink! + If a sink change is successful, it is 0. + + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + +:meta private: + + + +## OutputVolumeCallbackHandler Objects + +```python +class OutputVolumeCallbackHandler(CallbackHandler) +``` + +Callbacks are executed when + + * audio volume level is changed + + + +#### register + +```python +def register(func: Callable[[int, bool, bool], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(volume: int, is_min: bool, is_max: bool) + :noindex: + + :param volume: Volume level + :param is_min: 1, if volume level is minimum, else 0 + :param is_max: 1, if volume level is maximum, else 0 + + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + +:meta private: + + + +#### toggle\_output + +```python +@plugin.tag +def toggle_output() +``` + +Toggle the audio output sink + + + +#### get\_outputs + +```python +@plugin.tag +def get_outputs() +``` + +Get current output and list of outputs + + + +#### publish\_volume + +```python +@plugin.tag +def publish_volume() +``` + +Publish (volume, mute) + + + +#### publish\_outputs + +```python +@plugin.tag +def publish_outputs() +``` + +Publish current output and list of outputs + + + +#### set\_volume + +```python +@plugin.tag +def set_volume(volume: int) +``` + +Set the volume (0-100) for the currently active output + + + +#### get\_volume + +```python +@plugin.tag +def get_volume() +``` + +Get the volume + + + +#### change\_volume + +```python +@plugin.tag +def change_volume(step: int) +``` + +Increase/decrease the volume by step for the currently active output + + + +#### get\_mute + +```python +@plugin.tag +def get_mute() +``` + +Return mute status for the currently active output + + + +#### mute + +```python +@plugin.tag +def mute(mute=True) +``` + +Set mute status for the currently active output + + + +#### set\_output + +```python +@plugin.tag +def set_output(sink_index: int) +``` + +Set the active output (sink_index = 0: primary, 1: secondary) + + + +#### set\_soft\_max\_volume + +```python +@plugin.tag +def set_soft_max_volume(max_volume: int) +``` + +Limit the maximum volume to max_volume for the currently active output + + + +#### get\_soft\_max\_volume + +```python +@plugin.tag +def get_soft_max_volume() +``` + +Return the maximum volume limit for the currently active output + + + +#### card\_list + +```python +def card_list() -> List[pulsectl.PulseCardInfo] +``` + +Return the list of present sound card + + + +# components.rfid + + + +# components.rfid.reader + + + +## RfidCardDetectCallbacks Objects + +```python +class RfidCardDetectCallbacks(CallbackHandler) +``` + +Callbacks are executed if rfid card is detected + + + +#### register + +```python +def register(func: Callable[[str, RfidCardDetectState], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(card_id: str, state: int) + :noindex: + + :param card_id: Card ID + :param state: See :class:`RfidCardDetectState` + + + + +#### run\_callbacks + +```python +def run_callbacks(card_id: str, state: RfidCardDetectState) +``` + +:meta private: + + + +#### rfid\_card\_detect\_callbacks + +Callback handler instance for rfid_card_detect_callbacks events. +See :class:`RfidCardDetectCallbacks` + + + +## CardRemovalTimerClass Objects + +```python +class CardRemovalTimerClass(threading.Thread) +``` + +A timer watchdog thread that calls timeout_action on time-out + + + +#### \_\_init\_\_ + +```python +def __init__(on_timeout_callback, logger: logging.Logger = None) +``` + +**Arguments**: + +- `on_timeout_callback`: The function to execute on time-out + + + +# components.rfid.configure + + + +#### reader\_install\_dependencies + +```python +def reader_install_dependencies(reader_path: str, + dependency_install: str) -> None +``` + +Install dependencies for the selected reader module + +**Arguments**: + +- `reader_path`: Path to the reader module +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies + + + +#### reader\_load\_module + +```python +def reader_load_module(reader_name) +``` + +Load the module for the reader_name + +A ModuleNotFoundError is unrecoverable, but we at least want to give some hint how to resolve that to the user +All other errors will NOT be handled. Modules that do not load due to compile errors have other problems + +**Arguments**: + +- `reader_name`: Name of the reader to load the module for + +**Returns**: + +module + + + +#### query\_user\_for\_reader + +```python +def query_user_for_reader(dependency_install='query') -> dict +``` + +Ask the user to select a RFID reader and prompt for the reader's configuration + +This function performs the following steps, to find and present all available readers to the user + +- search for available reader subpackages +- dynamically load the description module for each reader subpackage +- queries user for selection +- if no_dep_install=False, install dependencies as given by requirements.txt and execute setup.inc.sh of subpackage +- dynamically load the actual reader module from the reader subpackage +- if selected reader has customization options query user for that now +- return configuration + +There are checks to make sure we have the right reader modules and they are what we expect. +The are as few requirements towards the reader module as possible and everything else is optional +(see reader_template for these requirements) +However, there is no error handling w.r.t to user input and reader's query_config. Firstly, in this script +we cannot gracefully handle an exception that occurs on reader level, and secondly the exception will simply +exit the script w/o writing the config to file. No harm done. + +This script expects to reside in the directory with all the reader subpackages, i.e it is part of the rfid-reader package. +Otherwise you'll need to adjust sys.path + +**Arguments**: + +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies + +**Returns**: + +`dict as {section: {parameter: value}}`: nested dict with entire configuration that can be read into ConfigParser + + + +#### write\_config + +```python +def write_config(config_file: str, + config_dict: dict, + force_overwrite=False) -> None +``` + +Write configuration to config_file + +**Arguments**: + +- `config_file`: relative or absolute path to config file +- `config_dict`: nested dict with configuration parameters for ConfigParser consumption +- `force_overwrite`: overwrite existing configuration file without asking + + + +# components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui + + + +# components.rfid.hardware.fake\_reader\_gui.description + + + +# components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon + +Add GPIO input devices and output devices to the RFID Mock Reader GUI + + + +#### create\_inputs + +```python +def create_inputs(frame, default_btn_width, default_padx, default_pady) +``` + +Add all input devies to the GUI + +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to + +**Returns**: + +List of all added GUI buttons + + + +#### set\_state + +```python +def set_state(value, box_state_var) +``` + +Change the value of a checkbox state variable + + + +#### que\_set\_state + +```python +def que_set_state(value, box_state_var) +``` + +Queue the action to change a checkbox state variable to the TK GUI main thread + + + +#### fix\_state + +```python +def fix_state(box_state_var) +``` + +Prevent a checkbox state variable to change on checkbox mouse press + + + +#### pbox\_set\_state + +```python +def pbox_set_state(value, pbox_state_var, label_var) +``` + +Update progress bar state and related state label + + + +#### que\_set\_pbox + +```python +def que_set_pbox(value, pbox_state_var, label_var) +``` + +Queue the action to change the progress bar state to the TK GUI main thread + + + +#### create\_outputs + +```python +def create_outputs(frame, default_btn_width, default_padx, default_pady) +``` + +Add all output devices to the GUI + +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to + +**Returns**: + +List of all added GUI objects + + + +# components.rfid.hardware.generic\_usb.description + + + +# components.rfid.hardware.generic\_usb.generic\_usb + + + +# components.rfid.hardware.rc522\_spi.description + + + +# components.rfid.hardware.rc522\_spi.rc522\_spi + + + +# components.rfid.hardware.pn532\_i2c\_py532.description + + + +# components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532 + + + +# components.rfid.hardware.rdm6300\_serial.rdm6300\_serial + + + +#### decode + +```python +def decode(raw_card_id: bytearray, number_format: int) -> str +``` + +Decode the RDM6300 data format into actual card ID + + + +# components.rfid.hardware.rdm6300\_serial.description + + + +# components.rfid.hardware.template\_new\_reader.description + +Provide a short title for this reader. +This is what that user will see when asked for selecting his RFID reader +So, be precise but readable. Precise means 40 characters or less + + + +# components.rfid.hardware.template\_new\_reader.template\_new\_reader + + + +#### query\_customization + +```python +def query_customization() -> dict +``` + +Query the user for reader parameter customization + +This function will be called during the configuration/setup phase when the user selects this reader module. +It must return all configuration parameters that are necessary to later use the Reader class. +You can ask the user for selections and choices. And/or provide default values. +If your reader requires absolutely no configuration return {} + + + +## ReaderClass Objects + +```python +class ReaderClass(ReaderBaseClass) +``` + +The actual reader class that is used to read RFID cards. + +It will be instantiated once and then read_card() is called in an endless loop. + +It will be used in a manner + with Reader(reader_cfg_key) as reader: + for card_id in reader: + ... +which ensures proper resource de-allocation. For this to work derive this class from ReaderBaseClass. +All the required interfaces are implemented there. + +Put your code into these functions (see below for more information) + - __init__ + - read_card + - cleanup + - stop + + + +#### \_\_init\_\_ + +```python +def __init__(reader_cfg_key) +``` + +In the constructor, you will get the `reader_cfg_key` with which you can access the configuration data + +As you are dealing directly with potentially user-manipulated config information, it is +advisable to do some sanity checks and give useful error messages. Even if you cannot recover gracefully, +a good error message helps :-) + + + +#### cleanup + +```python +def cleanup() +``` + +The cleanup function: free and release all resources used by this card reader (if any). + +Put all your cleanup code here, e.g. if you are using the serial bus or GPIO pins. +Will be called implicitly via the __exit__ function +This function must exist! If there is nothing to do, just leave the pass statement in place below + + + +#### stop + +```python +def stop() +``` + +This function is called to tell the reader to exist it's reading function. + +This function is called before cleanup is called. + +.. note: This is usually called from a different thread than the reader's thread! And this is the reason for the + two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt + to read a card. Once called, the function read_card will not be called again. When the reader thread exits + cleanup is called from the reader thread itself. + + + +#### read\_card + +```python +def read_card() -> str +``` + +Blocking or non-blocking function that waits for a new card to appear and return the card's UID as string + +This is were your main code goes :-) +This function must return a string with the card id +In case of error, it may return None or an empty string + +The function should break and return with an empty string, once stop() is called + + + +# components.rfid.readerbase + + + +## ReaderBaseClass Objects + +```python +class ReaderBaseClass(ABC) +``` + +Abstract Base Class for all Reader Classes to ensure common API + +Look at template_new_reader.py for documentation how to integrate a new RFID reader + + + +# components.rfid.cards + +Handling the RFID card database + +A few considerations: +- Changing the Card DB influences to current state + - rfid.reader: Does not care, as it always freshly looks into the DB when a new card is triggered + - fake_reader_gui: Initializes the Drop-down menu once on start --> Will get out of date! + +Do we need a notifier? Or a callback for modules to get notified? +Do we want to publish the information about a card DB update? +TODO: Add callback for on_database_change + +TODO: check card id type (if int, convert to str) +TODO: check if args is really a list (convert if not?) + + + +#### list\_cards + +```python +@plugs.register +def list_cards() +``` + +Provide a summarized, decoded list of all card actions + +This is intended as basis for a formatter function + +Format: 'id': {decoded_function_call, ignore_same_id_delay, ignore_card_removal_action, description, from_alias} + + + +#### delete\_card + +```python +@plugs.register +def delete_card(card_id: str, auto_save: bool = True) +``` + +**Arguments**: + +- `auto_save`: +- `card_id`: + + + +#### register\_card + +```python +@plugs.register +def register_card(card_id: str, + cmd_alias: str, + args: Optional[List] = None, + kwargs: Optional[Dict] = None, + ignore_card_removal_action: Optional[bool] = None, + ignore_same_id_delay: Optional[bool] = None, + overwrite: bool = False, + auto_save: bool = True) +``` + +Register a new card based on quick-selection + +If you are going to call this through the RPC it will get a little verbose + +**Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume +(*here: 15*) and custom *ignore_same_id_delay value*:: + +plugin.call_ignore_errors('cards', 'register_card', +args=['0009', 'inc_volume'], +kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) + + + +#### register\_card\_custom + +```python +@plugs.register +def register_card_custom() +``` + +Register a new card with full RPC call specification (Not implemented yet) + + + +#### save\_card\_database + +```python +@plugs.register +def save_card_database(filename=None, *, only_if_changed=True) +``` + +Store the current card database. If filename is None, it is saved back to the file it was loaded from + + + +# components.rfid.cardutils + +Common card decoding functions + +TODO: Thread safety when accessing the card DB! + + + +#### decode\_card\_command + +```python +def decode_card_command(cfg_rpc_cmd: Mapping, logger: logging.Logger = log) +``` + +Extension of utils.decode_action with card-specific parameters + + + +#### card\_command\_to\_str + +```python +def card_command_to_str(cfg_rpc_cmd: Mapping, long=False) -> List[str] +``` + +Returns a list of strings with [card_action, ignore_same_id_delay, ignore_card_removal_action] + +The last two parameters are only present, if *long* is True and if they are present in the cfg_rpc_cmd + + + +#### card\_to\_str + +```python +def card_to_str(card_id: str, long=False) -> List[str] +``` + +Returns a list of strings from card entry command in the format of :func:`card_command_to_str` + + + +# components.publishing + +Plugin interface for Jukebox Publisher + +Thin wrapper around jukebox.publishing to benefit from the plugin loading / exit handling / function handling + +This is the first package to be loaded and the last to be closed: put Hello and Goodbye publish messages here. + + + +#### republish + +```python +@plugin.register +def republish(topic=None) +``` + +Re-publish the topic tree 'topic' to all subscribers + +**Arguments**: + +- `topic`: Topic tree to republish. None = resend all + + + +# components.player + + + +## MusicLibPath Objects + +```python +class MusicLibPath() +``` + +Extract the music directory from the mpd.conf file + + + +#### get\_music\_library\_path + +```python +def get_music_library_path() +``` + +Get the music library path + + + +# components.jingle + +Jingle Playback Factory for extensible run-time support of various file types + + + +## JingleFactory Objects + +```python +class JingleFactory() +``` + +Jingle Factory + + + +#### list + +```python +def list() +``` + +List the available volume services + + + +#### play + +```python +@plugin.register +def play(filename) +``` + +Play the jingle using the configured jingle service + +Note: This runs in a separate thread. And this may cause troubles +when changing the volume level before +and after the sound playback: There is nothing to prevent another +thread from changing the volume and sink while playback happens +and afterwards we change the volume back to where it was before! + +There is no way around this dilemma except for not running the jingle as a +separate thread. Currently (as thread) even the RPC is started before the sound +is finished and the volume is reset to normal... + +However: Volume plugin is loaded before jingle and sets the default +volume. No interference here. It can now only happen +if (a) through the RPC or (b) some other plugin the volume is changed. Okay, now +(a) let's hope that there is enough delay in the user requesting a volume change +(b) let's hope no other plugin wants to do that +(c) no bluetooth device connects during this time (and pulseaudio control is set to toggle_on_connect) +and take our changes with the threaded approach. + + + +#### play\_startup + +```python +@plugin.register +def play_startup() +``` + +Play the startup sound (using jingle.play) + + + +#### play\_shutdown + +```python +@plugin.register +def play_shutdown() +``` + +Play the shutdown sound (using jingle.play) + + + +# components.jingle.alsawave + +ALSA wave jingle Service for jingle.JingleFactory + + + +## AlsaWave Objects + +```python +@plugin.register +class AlsaWave() +``` + +Jingle Service for playing wave files directly from Python through ALSA + + + +#### play + +```python +@plugin.tag +def play(filename) +``` + +Play the wave file + + + +## AlsaWaveBuilder Objects + +```python +class AlsaWaveBuilder() +``` + + + +#### \_\_init\_\_ + +```python +def __init__() +``` + +Builder instantiates AlsaWave during init and not during first call because +we want AlsaWave registers as plugin function in any case if this plugin is loaded +(and not only on first use!) + + + +# components.jingle.jinglemp3 + +Generic MP3 jingle Service for jingle.JingleFactory + + + +## JingleMp3Play Objects + +```python +@plugin.register(auto_tag=True) +class JingleMp3Play() +``` + +Jingle Service for playing MP3 files + + + +#### play + +```python +def play(filename) +``` + +Play the MP3 file + + + +## JingleMp3PlayBuilder Objects + +```python +class JingleMp3PlayBuilder() +``` + + + +#### \_\_init\_\_ + +```python +def __init__() +``` + +Builder instantiates JingleMp3Play during init and not during first call because +we want JingleMp3Play registers as plugin function in any case if this plugin is loaded +(and not only on first use!) + + + +# components.hostif.linux + + + +#### shutdown + +```python +@plugin.register +def shutdown() +``` + +Shutdown the host machine + + + +#### reboot + +```python +@plugin.register +def reboot() +``` + +Reboot the host machine + + + +#### jukebox\_is\_service + +```python +@plugin.register +def jukebox_is_service() +``` + +Check if current Jukebox process is running as a service + + + +#### is\_any\_jukebox\_service\_active + +```python +@plugin.register +def is_any_jukebox_service_active() +``` + +Check if a Jukebox service is running + +.. note:: Does not have the be the current app, that is running as a service! + + + +#### restart\_service + +```python +@plugin.register +def restart_service() +``` + +Restart Jukebox App if running as a service + + + +#### get\_disk\_usage + +```python +@plugin.register() +def get_disk_usage(path='/') +``` + +Return the disk usage in Megabytes as dictionary for RPC export + + + +#### get\_cpu\_temperature + +```python +@plugin.register +def get_cpu_temperature() +``` + +Get the CPU temperature with single decimal point + +No error handling: this is expected to take place up-level! + + + +#### get\_ip\_address + +```python +@plugin.register +def get_ip_address() +``` + +Get the IP address + + + +#### wlan\_disable\_power\_down + +```python +@plugin.register() +def wlan_disable_power_down(card=None) +``` + +Turn off power management of wlan. Keep RPi reachable via WLAN + +This must be done after every reboot +card=None takes card from configuration file + + + +#### get\_autohotspot\_status + +```python +@plugin.register +def get_autohotspot_status() +``` + +Get the status of the auto hotspot feature + + + +#### stop\_autohotspot + +```python +@plugin.register() +def stop_autohotspot() +``` + +Stop auto hotspot functionality + +Basically disabling the cronjob and running the script one last time manually + + + +#### start\_autohotspot + +```python +@plugin.register() +def start_autohotspot() +``` + +start auto hotspot functionality + +Basically enabling the cronjob and running the script one time manually + + + +# components.misc + +Miscellaneous function package + + + +#### rpc\_cmd\_help + +```python +@plugin.register +def rpc_cmd_help() +``` + +Return all commands for RPC + + + +#### get\_all\_loaded\_packages + +```python +@plugin.register +def get_all_loaded_packages() +``` + +Get all successfully loaded plugins + + + +#### get\_all\_failed\_packages + +```python +@plugin.register +def get_all_failed_packages() +``` + +Get all plugins with error during load or initialization + + + +#### get\_start\_time + +```python +@plugin.register +def get_start_time() +``` + +Time when JukeBox has been started + + + +#### get\_log + +```python +def get_log(handler_name: str) +``` + +Get the log file from the loggers (debug_file_handler, error_file_handler) + + + +#### get\_log\_debug + +```python +@plugin.register +def get_log_debug() +``` + +Get the log file (from the debug_file_handler) + + + +#### get\_log\_error + +```python +@plugin.register +def get_log_error() +``` + +Get the log file (from the error_file_handler) + + + +#### get\_git\_state + +```python +@plugin.register +def get_git_state() +``` + +Return git state information for the current branch + + + +#### empty\_rpc\_call + +```python +@plugin.register +def empty_rpc_call(msg: str = '') +``` + +This function does nothing. + +The RPC command alias 'none' is mapped to this function. + +This is also used when configuration errors lead to non existing RPC command alias definitions. +When the alias definition is void, we still want to return a valid function to simplify error handling +up the module call stack. + +**Arguments**: + +- `msg`: If present, this message is send to the logger with severity warning + + + +# components.controls + + + +# components.controls.bluetooth\_audio\_buttons + +Plugin to attempt to automatically listen to it's buttons (play, next, ...) +when a bluetooth sound device (headphone, speakers) connects + +This effectively does: + + * register a callback with components.volume to get notified when a new sound card connects + * if that is a bluetooth device, try opening an input device with similar name using + * button listeners are run each in its own thread + + + +# components.controls.common.evdev\_listener + +Generalized listener for ``dev/input`` devices + + + +#### find\_device + +```python +def find_device(device_name: str, + exact_name: bool = True, + mandatory_keys: Optional[Set[int]] = None) -> str +``` + +Find an input device with device_name and mandatory keys. + +Raises + + ``. FileNotFoundError, if no device is found. + ``. AttributeError, if device does not have the mandatory keys + +If multiple devices match, the first match is returned + +**Arguments**: + +- `device_name`: See :func:`_filter_by_device_name` +- `exact_name`: See :func:`_filter_by_device_name` +- `mandatory_keys`: See :func:`_filter_by_mandatory_keys` + +**Returns**: + +The path to the device + + + +## EvDevKeyListener Objects + +```python +class EvDevKeyListener(threading.Thread) +``` + +Opens and event input device from ``/dev/inputs``, and runs callbacks upon the button presses. +Input devices could be .e.g. Keyboard, Bluetooth audio buttons, USB buttons + +Runs as a separate thread. When device disconnects or disappears, thread exists. A new thread must be started +when device re-connects. + +Assign callbacks to :attr:`EvDevKeyListener.button_callbacks` + + + +#### \_\_init\_\_ + +```python +def __init__(device_name_request: str, exact_name: bool, thread_name: str) +``` + +**Arguments**: + +- `device_name_request`: The device name to look for +- `exact_name`: If true, device_name must mach exactly, else a match is returned if device_name is a substring of +the reported device name +- `thread_name`: Name of the listener thread + + + +#### run + +```python +def run() +``` + + + + + +#### start + +```python +def start() -> None +``` + +Start the tread and start listening + + + +# components.music\_cover\_art + +Read all cover art from music save it to a cache for the UI to load + +.. note:: Not implemented. This is a feature planned for a future release. + + + +## MusicCoverArt Objects + +```python +class MusicCoverArt() +``` + + + +#### get\_by\_filename\_as\_base64 + +```python +@plugin.tag +def get_by_filename_as_base64(audio_src: str) +``` + +Not implemented. This is a feature planned for a future release. + + + +# components.battery\_monitor + + + +# components.battery\_monitor.BatteryMonitorBase + + + +## pt1\_frac Objects + +```python +class pt1_frac() +``` + +fixed point first order filter, fractional format: 2^16,2^16 + + + +## BattmonBase Objects + +```python +class BattmonBase() +``` + +Battery Monitor base class + + + +# components.battery\_monitor.batt\_mon\_simulator + + + +## battmon\_simulator Objects + +```python +class battmon_simulator(BatteryMonitorBase.BattmonBase) +``` + +Battery Monitor Simulator + + + +# components.battery\_monitor.batt\_mon\_i2c\_ads1015 + + + +## battmon\_ads1015 Objects + +```python +class battmon_ads1015(BatteryMonitorBase.BattmonBase) +``` + +Battery Monitor based on a ADS1015 + +CAUTION - WARNING +======================================================================== +Lithium and other batteries are dangerous and must be treated with care. +Rechargeable Lithium Ion batteries are potentially hazardous and can +present a serious FIRE HAZARD if damaged, defective or improperly used. +Do not use this circuit to a lithium ion battery without expertise and +training in handling and use of batteries of this type. +Use appropriate test equipment and safety protocols during development. + +There is no warranty, this may not work as expected or at all! +========================================================================= + +This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: + + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === + +Attention: + - the circuit is constantly draining the battery! (leak current up to: 2.1µA) + - the time between sample needs to be a minimum 1sec with this high impedance voltage divider + don't use the continuous conversion method! + + + +# components.gpio.gpioz.plugin + +The GPIOZ plugin interface build all input and output devices from the configuration file and connects +the actions and callbacks. It also provides a very restricted, but common API for the output devices to the RPC. +That API is mainly used for testing. All the relevant output state changes are usually made through callbacks directly +using the output device's API. + + + +#### output\_devices + +List of all created output devices + + + +#### input\_devices + +List of all created input devices + + + +#### factory + +The global pin factory used in this module +Using different pin factories for different devices is not supported + + + +#### IS\_ENABLED + +Indicates that the GPIOZ module is enabled and loaded w/o errors + + + +#### IS\_MOCKED + +Indicates that the pin factory is a mock factory + + + +#### CONFIG\_FILE + +The path of the config file the GPIOZ configuration was loaded from + + + +## ServiceIsRunningCallbacks Objects + +```python +class ServiceIsRunningCallbacks(CallbackHandler) +``` + +Callbacks are executed when + + * Jukebox app started + * Jukebox shuts down + +This is intended to e.g. signal an LED to change state. +This is integrated into this module because: + + * we need the GPIO to control a LED (it must be available when the status callback comes) + * the plugin callback functions provide all the functionality to control the status of the LED + * which means no need to adapt other modules + + + +#### register + +```python +def register(func: Callable[[int], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(status: int) + :noindex: + + :param status: 1 if app started, 0 if app shuts down + + + + +#### run\_callbacks + +```python +def run_callbacks(status: int) +``` + +:meta private: + + + +#### service\_is\_running\_callbacks + +Callback handler instance for service_is_running_callbacks events. +See :class:`ServiceIsRunningCallbacks` + + + +#### build\_output\_device + +```python +def build_output_device(name: str, config: Dict) +``` + +Construct and register a new output device + +In principal all supported GPIOZero output devices can be used. +For all devices a custom functions need to be written to control the state of the outputs + + + +#### build\_input\_device + +```python +def build_input_device(name: str, config) +``` + +Construct and connect a new input device + +Supported input devices are those from gpio.gpioz.core.input_devices + + + +#### get\_output + +```python +def get_output(name: str) +``` + +Get the output device instance based on the configured name + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### on + +```python +@plugin.register +def on(name: str) +``` + +Turn an output device on + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### off + +```python +@plugin.register +def off(name: str) +``` + +Turn an output device off + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### set\_value + +```python +@plugin.register +def set_value(name: str, value: Any) +``` + +Set the output device to :attr:`value` + +**Arguments**: + +- `name`: The alias name output device instance +- `value`: Value to set the device to + + + +#### flash + +```python +@plugin.register +def flash(name, + on_time=1, + off_time=1, + n=1, + *, + fade_in_time=0, + fade_out_time=0, + tone=None, + color=(1, 1, 1)) +``` + +Flash (blink or beep) an output device + +This is a generic function for all types of output devices. Parameters not applicable to an +specific output device are silently ignored + +**Arguments**: + +- `name`: The alias name output device instance +- `on_time`: Time in seconds in state ``ON`` +- `off_time`: Time in seconds in state ``OFF`` +- `n`: Number of flash cycles +- `tone`: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. +- `color`: The RGB color *only for PWMLED*. +- `fade_in_time`: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* +- `fade_out_time`: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* + + + +# components.gpio.gpioz.plugin.connectivity + +Provide connector functions to hook up to some kind of Jukebox functionality and change the output device's state +accordingly. + +Connector functions can often be used for various output devices. Some connector functions are specific to +an output device type. + + + +#### BUZZ\_TONE + +The tone to be used as buzz tone when the buzzer is an active buzzer + + + +#### register\_rfid\_callback + +```python +def register_rfid_callback(device) +``` + +Flash the output device once on successful RFID card detection and thrice if card ID is unknown + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.LED` + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + - :class:`components.gpio.gpioz.core.output_devices.Buzzer` + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_status\_led\_callback + +```python +def register_status_led_callback(device) +``` + +Turn LED on when Jukebox App has started + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.LED` + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + +#### register\_status\_buzzer\_callback + +```python +def register_status_buzzer_callback(device) +``` + +Buzz once when Jukebox App has started, twice when closing down + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.Buzzer` + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_status\_tonalbuzzer\_callback + +```python +def register_status_tonalbuzzer_callback(device) +``` + +Buzz a multi-note melody when Jukebox App has started and when closing down + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_audio\_sink\_change\_callback + +```python +def register_audio_sink_change_callback(device) +``` + +Turn LED on if secondary audio output is selected. If audio output change +fails, blink thrice + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.LED` + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + +#### register\_volume\_led\_callback + +```python +def register_volume_led_callback(device) +``` + +Have a PWMLED change it's brightness according to current volume. LED flashes when minimum or maximum volume +is reached. Minimum value is still a very dimly turned on LED (i.e. LED is never off). + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + + + +#### register\_volume\_buzzer\_callback + +```python +def register_volume_buzzer_callback(device) +``` + +Sound a buzzer once when minimum or maximum value is reached + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.Buzzer` + - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + +#### register\_volume\_rgbled\_callback + +```python +def register_volume_rgbled_callback(device) +``` + +Have a :class:`RGBLED` change it's color according to current volume. LED flashes when minimum or maximum volume +is reached. + +Compatible devices: + + - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + +# components.gpio.gpioz.core.converter + +Provides converter functions/classes for various Jukebox parameters to +values that can be assigned to GPIO output devices + + + +## ColorProperty Objects + +```python +class ColorProperty() +``` + +Color descriptor ensuring valid weight ranges + +:meta private: + + + +## VolumeToRGB Objects + +```python +class VolumeToRGB() +``` + +Converts linear volume level to an RGB color value running through the color spectrum + +**Arguments**: + +- `max_input`: Maximum input value of linear input data +- `offset`: Offset in degrees in the color circle. Color circle +traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) +- `section`: The section of the full color circle to use in degrees +Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 + +.. code-block:: python + + conv = VolumeToRGB(100, offset=120, section=180) + (r, g, b) = conv(50) + +The three components of an RGB LEDs do not have the same luminosity. +Weight factors are used to get a balanced color output + + + +#### \_\_call\_\_ + +```python +def __call__(volume) -> Tuple[float, float, float] +``` + +Perform conversion for single volume level + +**Returns**: + +Tuple(red, green, blue) + + + +#### luminize + +```python +def luminize(r, g, b) +``` + +Apply the color weight factors to the input color values + + + +# components.gpio.gpioz.core.mock + +Changes to the GPIOZero devices for using with the Mock RFID Reader + + + +#### patch\_mock\_outputs\_with\_callback + +```python +def patch_mock_outputs_with_callback() +``` + +Monkey Patch LED + Buzzer to get a callback when state changes + +This targets to represent the state in the TK GUI. +Other output devices cannot be represented in the GUI and are silently ignored. + +..note:: Only for developing purposes! + + + +# components.gpio.gpioz.core.input\_devices + +Provides all supported input devices for the GPIOZ plugin. + +Input devices are based on GPIOZero devices. So for certain configuration parameters, you should +their documentation. + +All callback handlers are replaced by GPIOZ callback handlers. These are usually configured +by using the :func:`set_rpc_actions` each input device exhibits. + +For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Input devices` + + + +## NameMixin Objects + +```python +class NameMixin(ABC) +``` + +Provides name property and RPC decode function + +:meta private: + + + +#### set\_rpc\_actions + +```python +@abstractmethod +def set_rpc_actions(action_config) -> None +``` + +Set all input device callbacks from :attr:`action_config` + +**Arguments**: + +- `action_config`: Dictionary with one +:ref:`RPC Command ` definition entry for every device callback + + + +## EventProperty Objects + +```python +class EventProperty() +``` + +Event callback property + +:meta private: + + + +## ButtonBase Objects + +```python +class ButtonBase(ABC) +``` + +Common stuff for single button devices + +:meta private: + + + +#### value + +```python +@property +def value() +``` + +Returns 1 if the button is currently pressed, and 0 if it is not. + + + +#### pin + +```python +@property +def pin() +``` + +Returns the underlying pin class from GPIOZero. + + + +#### pull\_up + +```python +@property +def pull_up() +``` + +If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. + + + +#### close + +```python +def close() +``` + +Close the device and release the pin + + + +## Button Objects + +```python +class Button(NameMixin, ButtonBase) +``` + +A basic Button that runs a single actions on button press + +**Arguments**: + +- `pull_up` (`bool`): If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. +If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external +resistor must be used and the :attr:`active_state` must be set. +- `active_state` (`bool or None`): If :data:`True`, when the hardware pin state is ``HIGH``, the software +pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when +the hardware pin state is ``HIGH``, the software pin state is ``LOW``. +Use this parameter to set the active state of the underlying pin when +configuring it as not pulled (when *pull_up* is :data:`None`). When +*pull_up* is :data:`True` or :data:`False`, the active state is +automatically set to the proper value. +- `bounce_time` (`float or None`): Specifies the length of time (in seconds) that the component will +ignore changes in state after an initial change. This defaults to +:data:`None` which indicates that no bounce compensation will be +performed. +- `hold_repeat` (`bool`): If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action +is run only once independent of the length of time the button is pressed for. +- `hold_time` (`float`): Time in seconds to wait between invocations of :attr:`on_press`. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file + +.. copied from GPIOZero's documentation: active_state, bounce_time +.. Copyright Ben Nuttall / SPDX-License-Identifier: BSD-3-Clause + + + +#### on\_press + +```python +@property +def on_press() +``` + +The function to run when the device has been pressed + + + +## LongPressButton Objects + +```python +class LongPressButton(NameMixin, ButtonBase) +``` + +A Button that runs a single actions only when the button is pressed long enough + +**Arguments**: + +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action +is run only once independent of the length of time the button is pressed for. +- `hold_time`: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. +Also the time in seconds to wait between invocations of :attr:`on_press`. + + + +#### on\_press + +```python +@on_press.setter +def on_press(func) +``` + +The function to run when the device has been pressed for longer than :attr:`hold_time` + + + +## ShortLongPressButton Objects + +```python +class ShortLongPressButton(NameMixin, ButtonBase) +``` + +A single button that runs two different actions depending if the button is pressed for a short or long time. + +The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press +can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. +But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release +event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run +in this case! + +**Arguments**: + +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ + + + +## RotaryEncoder Objects + +```python +class RotaryEncoder(NameMixin) +``` + +A rotary encoder to run one of two actions depending on the rotation direction. + +**Arguments**: + +- `bounce_time`: See `Button`_ +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ + + + +#### pin\_a + +```python +@property +def pin_a() +``` + +Returns the underlying pin A + + + +#### pin\_b + +```python +@property +def pin_b() +``` + +Returns the underlying pin B + + + +#### on\_rotate\_clockwise + +```python +@property +def on_rotate_clockwise() +``` + +The function to run when the encoder is rotated clockwise + + + +#### on\_rotate\_counter\_clockwise + +```python +@property +def on_rotate_counter_clockwise() +``` + +The function to run when the encoder is rotated counter clockwise + + + +#### close + +```python +def close() +``` + +Close the device and release the pin + + + +## TwinButton Objects + +```python +class TwinButton(NameMixin) +``` + +A two-button device which can run up to six different actions, a.k.a the six function beast. + +Per user press "input" of the TwinButton, only a single callback is executed (but this callback +may be executed several times). +The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press +can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. +But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release +event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run +in this case! + +It is not necessary to configure all actions. + +**Arguments**: + +- `pull_up`: See `Button`_ +- `active_state`: See `Button`_ +- `bounce_time`: See `Button`_ +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored. +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action. A long dual press is never repeated independent of this setting +- `pin_factory`: See `Button`_ +- `name`: See `Button`_ + + + +## StateVar Objects + +```python +class StateVar(Enum) +``` + +State encoding of the Mealy FSM + +:meta private: + + + +#### close + +```python +def close() +``` + +Close the device and release the pins + + + +#### value + +```python +@property +def value() +``` + +2 bit integer indicating if and which button is currently pressed. Button A is the LSB. + + + +#### is\_active + +```python +@property +def is_active() +``` + +:data:`True` if one or both buttons are currently pressed + + + +# components.gpio.gpioz.core.output\_devices + +Provides all supported output devices for the GPIOZ plugin. + +For each device all constructor parameters can be set via the configuration file. Only exceptions +are the :attr:`name` and :attr:`pin_factory` which are set by internal mechanisms. + +The devices a are a relatively thin wrapper around the GPIOZero devices with the same name. +We add a name property to be used for error log message and similar and a :func:`flash` function +to all devices. This function provides a unified API to all devices. This means it can be called for every device +with parameters for this device and optional parameters from another device. Unused/unsupported parameters +are silently ignored. This is done to reduce the amount of coding required for connectivity functions. + +For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Output devices` + + + +## LED Objects + +```python +class LED(NameMixin, gpiozero.LED) +``` + +A binary LED + +**Arguments**: + +- `pin`: The GPIO pin which the LED is connected +- `active_high`: If :data:`true` the output pin will have a high logic level when the device is turned on. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file + + + +#### flash + +```python +def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) +``` + +Exactly like :func:`blink` but restores the original state after flashing the device + +**Arguments**: + +- `on_time` (`float`): Number of seconds on. Defaults to 1 second. +- `off_time` (`float`): Number of seconds off. Defaults to 1 second. +- `n`: Number of times to blink; :data:`None` means forever. +- `background` (`bool`): If :data:`True` (the default), start a background thread to +continue blinking and return immediately. If :data:`False`, only +return when the blink is finished +- `ignored_kwargs`: Ignore all other keywords so this function can be called with identical +parameters also for all other output devices + + + +## Buzzer Objects + +```python +class Buzzer(NameMixin, gpiozero.Buzzer) +``` + + + +#### flash + +```python +def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) +``` + +Flash the device and restore the previous value afterwards + + + +## PWMLED Objects + +```python +class PWMLED(NameMixin, gpiozero.PWMLED) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + n=1, + *, + fade_in_time=0, + fade_out_time=0, + background=True, + **ignored_kwargs) +``` + +Flash the LED and restore the previous value afterwards + + + +## RGBLED Objects + +```python +class RGBLED(NameMixin, gpiozero.RGBLED) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + *, + fade_in_time=0, + fade_out_time=0, + on_color=(1, 1, 1), + off_color=(0, 0, 0), + n=None, + background=True, + **igorned_kwargs) +``` + +Flash the LED with :attr:`on_color` and restore the previous value afterwards + + + +## TonalBuzzer Objects + +```python +class TonalBuzzer(NameMixin, gpiozero.TonalBuzzer) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + n=1, + *, + tone=None, + background=True, + **ignored_kwargs) +``` + +Play the tone :data:`tone` for :attr:`n` times + + + +#### melody + +```python +def melody(on_time=0.2, + off_time=0.05, + *, + tone: Optional[List[Tone]] = None, + background=True) +``` + +Play a melody from the list of tones in :attr:`tone` + + + +# components.timers + + + +# jukebox + + + +# jukebox.callingback + +Provides a generic callback handler + + + +## CallbackHandler Objects + +```python +class CallbackHandler() +``` + +Generic Callback Handler to collect callbacks functions through :func:`register` and execute them + +with :func:`run_callbacks` + +A lock is used to sequence registering of new functions and running callbacks. + +**Arguments**: + +- `name`: A name of this handler for usage in log messages +- `logger`: The logger instance to use for logging +- `context`: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created + + + +#### register + +```python +def register(func: Optional[Callable[..., None]]) +``` + +Register a new function to be executed when the callback event happens + +**Arguments**: + +- `func`: The function to register. If set to :data:`None`, this register request is silently ignored. + + + +#### run\_callbacks + +```python +def run_callbacks(*args, **kwargs) +``` + +Run all registered callbacks. + +*ALL* exceptions from callback functions will be caught and logged only. +Exceptions are not raised upwards! + + + +#### has\_callbacks + +```python +@property +def has_callbacks() +``` + +:data:`True` if there are any registered callbacks. Read-only property + + + +# jukebox.version + + + +#### version + +```python +def version() +``` + +Return the Jukebox version as a string + + + +#### version\_info + +```python +def version_info() +``` + +Return the Jukebox version as a tuple of three numbers + +If this is a development version, an identifier string will be appended after the third integer. + + + +# jukebox.cfghandler + +This module handles global and local configuration data + +The concept is that config handler is created and initialized once in the main thread:: + + cfg = get_handler('global') + load_yaml(cfg, 'filename.yaml') + +In all other modules (in potentially different threads) the same handler is obtained and used by:: + + cfg = get_handler('global') + +This eliminates the need to pass an effectively global configuration handler by parameters across the entire design. +Handlers are identified by their name (in the above example *global*) + +The function :func:`get_handler` is the main entry point to obtain a new or existing handler. + + + +## ConfigHandler Objects + +```python +class ConfigHandler() +``` + +The configuration handler class + +Don't instantiate directly. Always use :func:`get_handler`! + +**Threads:** + +All threads can read and write to the configuration data. +**Proper thread-safeness must be ensured** by the the thread modifying the data by acquiring the lock +Easiest and best way is to use the context handler:: + + with cfg: + cfg['key'] = 66 + cfg.setndefault('hello', value='world') + +For a single function call, this is done implicitly. In this case, there is no need +to explicitly acquire the lock. + +Alternatively, you can lock and release manually by using :func:`acquire` and :func:`release` +But be very sure to release the lock even in cases of errors an exceptions! +Else we have a deadlock. + +Reading may be done without acquiring a lock. But be aware that when reading multiple values without locking, another +thread may intervene and modify some values in between! So, locking is still recommended. + + + +#### loaded\_from + +```python +@property +def loaded_from() -> Optional[str] +``` + +Property to store filename from which the config was loaded + + + +#### get + +```python +def get(key, *, default=None) +``` + +Enforce keyword on default to avoid accidental misuse when actually getn is wanted + + + +#### setdefault + +```python +def setdefault(key, *, value) +``` + +Enforce keyword on default to avoid accidental misuse when actually setndefault is wanted + + + +#### getn + +```python +def getn(*keys, default=None) +``` + +Get the value at arbitrary hierarchy depth. Return ``default`` if key not present + +The *default* value is returned no matter at which hierarchy level the path aborts. +A hierarchy is considered as any type with a :func:`get` method. + + + +#### setn + +```python +def setn(*keys, value, hierarchy_type=None) -> None +``` + +Set the ``key: value`` pair at arbitrary hierarchy depth + +All non-existing hierarchy levels are created. + +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used + + + +#### setndefault + +```python +def setndefault(*keys, value, hierarchy_type=None) +``` + +Set the ``key: value`` pair at arbitrary hierarchy depth unless the key already exists + +All non-existing hierarchy levels are created. + +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The default value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used + +**Returns**: + +The actual value or or the default value if key does not exit + + + +#### config\_dict + +```python +def config_dict(data) +``` + +Initialize configuration data from dict-like data structure + +**Arguments**: + +- `data`: configuration data + + + +#### is\_modified + +```python +def is_modified() -> bool +``` + +Check if the data has changed since the last load/store + +.. note: This relies on the *__str__* representation of the underlying data structure + In case of ruamel, this ignores comments and only looks at the data + + + +#### clear\_modified + +```python +def clear_modified() -> None +``` + +Sets the current state as new baseline, clearing the is_modified state + + + +#### save + +```python +def save(only_if_changed: bool = False) -> None +``` + +Save config back to the file it was loaded from + +If you want to save to a different file, use :func:`write_yaml`. + + + +#### load + +```python +def load(filename: str) -> None +``` + +Load YAML config file into memory + + + +#### get\_handler + +```python +def get_handler(name: str) -> ConfigHandler +``` + +Get a configuration data handler with the specified name, creating it + +if it doesn't yet exit. If created, it is always created empty. + +This is the main entry point for obtaining an configuration handler + +**Arguments**: + +- `name`: Name of the config handler + +**Returns**: + +`ConfigHandler`: The configuration data handler for *name* + + + +#### load\_yaml + +```python +def load_yaml(cfg: ConfigHandler, filename: str) -> None +``` + +Load a yaml file into a ConfigHandler + +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to yaml file + +**Returns**: + +None + + + +#### write\_yaml + +```python +def write_yaml(cfg: ConfigHandler, + filename: str, + only_if_changed: bool = False, + *args, + **kwargs) -> None +``` + +Writes ConfigHandler data to yaml file / sys.stdout + +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to output file. If *sys.stdout*, output is written to console +- `only_if_changed`: Write file only, if ConfigHandler.is_modified() +- `args`: passed on to yaml.dump(...) +- `kwargs`: passed on to yaml.dump(...) + +**Returns**: + +None + + + +# jukebox.playlistgenerator + +Playlists are build from directory content in the following way: +a directory is parsed and files are added to the playlist in the following way + +1. files are added in alphabetic order +2. files ending with ``*livestream.txt`` are unpacked and the containing URL(s) are added verbatim to the playlist +3. files ending with ``*podcast.txt`` are unpacked and the containing Podcast URL(s) are expanded and added to the playlist +4. files ending with ``*.m3u`` are treated as folder playlist. Regular folder processing is suspended and the playlist + is build solely from the ``*.m3u`` content. Only the alphabetically first ``*.m3u`` is processed. URLs are added verbatim + to the playlist except for ``*.xml`` and ``*.podcast`` URLS, which are expanded first + +An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. + +.. code-block:: bash + + 01-livestream.txt + 02-livestream.txt + music.mp3 + podcast.txt + +All files are treated as music files and are added to the playlist, except those: + + * starting with ``.``, + * not having a file ending, i.e. do not contain a ``.``, + * ending with ``.txt``, + * ending with ``.m3u``, + * ending with one of the excluded file endings in :attr:`PlaylistCollector._exclude_endings` + +In recursive mode, the playlist is generated by concatenating all sub-folder playlists. Sub-folders are parsed +in alphabetic order. Symbolic links are being followed. The above rules are enforced on a per-folder bases. +This means, one ``*.m3u`` file per sub-folder is processed (if present). + +In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. + + + +#### TYPE\_DECODE + +Types if file entires in parsed directory + + + +## PlaylistCollector Objects + +```python +class PlaylistCollector() +``` + +Build a playlist from directory(s) + +This class is intended to be used with an absolute path to the music library:: + + plc = PlaylistCollector('/home/chris/music') + plc.parse('Traumfaenger') + print(f"res = {plc}") + +But it can also be used with relative paths from current working directory:: + + plc = PlaylistCollector('.') + plc.parse('../../../../music/Traumfaenger') + print(f"res = {plc}") + +The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. +If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. + + + +#### \_\_init\_\_ + +```python +def __init__(music_library_base_path='/') +``` + +Initialize the playlist generator with music_library_base_path + +**Arguments**: + +- `music_library_base_path`: Base path the the music library. This is used to locate the file in the disk +but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir + + + +#### set\_exclusion\_endings + +```python +@classmethod +def set_exclusion_endings(cls, endings: List[str]) +``` + +Set the class-wide file ending exclusion list + +See :attr:`PlaylistCollector._exclude_endings` + + + +#### get\_directory\_content + +```python +def get_directory_content(path='.') +``` + +Parse the folder ``path`` and create a content list. Depth is always the current level + +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` + +**Returns**: + +[ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] +where type is one of :attr:`TYPE_DECODE` + + + +#### parse + +```python +def parse(path='.', recursive=False) +``` + +Parse the folder ``path`` and create a playlist from it's content + +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` +- `recursive`: Parse folder recursivley, or stay in top-level folder + + + +# jukebox.NvManager + + + +# jukebox.publishing + + + +#### get\_publisher + +```python +def get_publisher() +``` + +Return the publisher instance for this thread + +Per thread, only one publisher instance is required to connect to the inproc socket. +A new instance is created if it does not already exist. + +If there is a remote-chance that your function publishing something may be called form +different threads, always make a fresh call to ``get_publisher()`` to get the correct instance for the current thread. + +Example:: + +import jukebox.publishing as publishing + +class MyClass: +def __init__(self): +pass + +def say_hello(name): +publishing.get_publisher().send('hello', f'Hi {name}, howya?') + +To stress what **NOT** to do: don't get a publisher instance in the constructor and save it to ``self._pub``. +If you do and ``say_hello`` gets called from different threads, the publisher of the thread which instantiated the class +will be used. + +If you need your very own private Publisher Instance, you'll need to instantiate it yourself. +But: the use cases are very rare for that. I cannot think of one at the moment. + +**Remember**: Don’t share ZeroMQ sockets between threads. + + + +# jukebox.publishing.subscriber + + + +# jukebox.publishing.server + +Publishing Server +******************** + +The common publishing server for the entire Jukebox using ZeroMQ + +Structure +---------------- + +.. code-block:: text + ++-----------------------+ +| functional interface | Publisher +| | - functional interface for single Thread +| PUB | - sends data to publisher (and thus across threads) ++-----------------------+ +| (1) +v ++-----------------------+ +| SUB (bind) | PublishServer +| | - Last Value (LV) Cache +| XPUB (bind) | - Subscriber notification and LV resend ++-----------------------+ - independent thread +| (2) +v + +Connection (1): Internal connection +Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) + +Protocol: Multi-part message + +Part 1: Topic (in topic tree format) +E.g. player.status.elapsed + +Part 2: Payload or Message in json serialization +If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same + +Part 3: Command +Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer +and the message is not forwarded to the outside. This third part of the message is never forwarded + +Connection (2): External connection +Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! +Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will +also get you all the branch topics. To get everything, subscribe to ``b''`` + +Protocol: Multi-part message + +Part 1: Topic (in topic tree format) +E.g. player.status.elapsed + +Part 2: Payload or Message in json serialization +If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) + +Why? Why? +------------- + +Check out the `ZeroMQ Documentation `_ +for why you need a proxy in a good design. + +For use case, we made a few simplifications + +Design Rationales +------------------- + +* "If you need `millions of messages per second `_ +sent to thousands of points, +you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." +* "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then +the `XSUB and XPUB `_" +* "Let’s imagine `our feed has an average of 100,000 100-byte messages a second +`_ [...]. +While 100K messages a second is easy for a ZeroMQ application, ..." + +**But we have:** + +* few dozen subscribers --> Check! +* limited number of topics --> Check! +* max ~10 messages per second --> Check! +* small common state information --> Check! +* only the server updates the state --> Check! + +This means, we can use less complex patters than used for these high-speed, high code count, high data rate networks :-) + +* XPUB / XSUB to detect new subscriber +* Cache the entire state in the publisher +* Re-send the entire state on-demand (and then even to every subscriber) +* Using the same channel: sends state to every subscriber + +**Reliability considerations** + +* Late joining client (or drop-off and re-join): get full state update +* Server crash etc: No special handling necessary, we are simple +and don't need recovery in this case. Server will publish initial state +after re-start +* Subscriber too slow: Subscribers problem (TODO: Do we need to do anything about it?) + +**Start-up sequence:** + +* Publisher plugin is first plugin to be loaded +* Due to Publisher - PublisherServer structure no further sequencing required + +Plugin interactions and usage +------------------------------ + +RPC can trigger through function call in components/publishing plugin that + +* entire state is re-published (from the cache) +* a specific topic tree is re-published (from the cache) + +Plugins publishing state information should publish initial state at @plugin.finalize + +.. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is +required per thread. But the publisher instance **must** be thread-local! +Always go through :func:`publishing.get_publisher()`. + +**Sockets** + +Three sockets are opened: + +``. TCP (on a configurable port) +``. Websocket (on a configurable port) +``. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules +that want to know about the current state on event based updates. + +**Further ZeroMQ References:** + +* `Working with Messages `_ +* `Multiple Threads `_ + + + +## PublishServer Objects + +```python +class PublishServer(threading.Thread) +``` + +The publish proxy server that collects and caches messages from all internal publishers and +forwards them to the outside world + +Handles new subscriptions by sending out the entire cached state to **all** subscribers + +The code is structures using a `Reactor Pattern `_ + + + +#### run + +```python +def run() +``` + +Thread's activity + + + +#### handle\_message + +```python +def handle_message(msg) +``` + +Handle incoming messages + + + +#### handle\_subscription + +```python +def handle_subscription(msg) +``` + +Handle new subscribers + + + +## Publisher Objects + +```python +class Publisher() +``` + +The publisher that provides the functional interface to the application + +.. note:: + * An instance must not be shared across threads! + * One instance per thread is enough + + + +#### \_\_init\_\_ + +```python +def __init__(check_thread_owner=True) +``` + +**Arguments**: + +- `check_thread_owner`: Check if send() is always called from the correct thread. This is debug feature +and is intended to expose the situation before it leads to real trouble. Leave it on! + + + +#### send + +```python +def send(topic: str, payload) +``` + +Send out a message for topic + + + +#### revoke + +```python +def revoke(topic: str) +``` + +Revoke a single topic element (not a topic tree!) + + + +#### resend + +```python +def resend(topic: Optional[str] = None) +``` + +Instructs the PublishServer to resend current status to all subscribers + +Not necessary to call after incremental updates or new subscriptions - that will happen automatically! + + + +#### close\_server + +```python +def close_server() +``` + +Instructs the PublishServer to close itself down + + + +# jukebox.daemon + + + +#### log\_active\_threads + +```python +@atexit.register +def log_active_threads() +``` + +This functions is registered with atexit very early, meaning it will be run very late. It is the best guess to +evaluate which Threads are still running (and probably shouldn't be) + +This function is registered before all the plugins and their dependencies are loaded + + + +## JukeBox Objects + +```python +class JukeBox() +``` + + + +#### signal\_handler + +```python +def signal_handler(esignal, frame) +``` + +Signal handler for orderly shutdown + +On first Ctrl-C (or SIGTERM) orderly shutdown procedure is embarked upon. It gets allocated a time-out! +On third Ctrl-C (or SIGTERM), this is interrupted and there will be a hard exit! + + + +# jukebox.plugs + +A plugin package with some special functionality + +Plugins packages are python packages that are dynamically loaded. From these packages only a subset of objects is exposed +through the plugs.call interface. The python packages can use decorators or dynamic function call to register (callable) +objects. + +The python package name may be different from the name the package is registered under in plugs. This allows to load different +python packages for a specific feature based on a configuration file. Note: Python package are still loaded as regular +python packages and can be accessed by normal means + +If you want to provide additional functionality to the same feature (probably even for run-time switching) +you can implement a Factory Pattern using this package. Take a look at volume.py as an example. + +**Example:** Decorate a function for auto-registering under it's own name:: + +import jukebox.plugs as plugs +@plugs.register +def func1(param): +pass + +**Example:** Decorate a function for auto-registering under a new name:: + +@plugs.register(name='better_name') +def func2(param): +pass + +**Example:** Register a function during run-time under it's own name:: + +def func3(param): +pass +plugs.register(func3) + +**Example:** Register a function during run-time under a new name:: + +def func4(param): +pass +plugs.register(func4, name='other_name', package='other_package') + +**Example:** Decorate a class for auto registering during initialization, +including all methods (see _register_class for more info):: + +@plugs.register(auto_tag=True) +class MyClass1: +pass + +**Example:** Register a class instance, from which only report is a callable method through the plugs interface:: + +class MyClass2: +@plugs.tag +def report(self): +pass +myinst2 = MyClass2() +plugin.register(myinst2, name='myinst2') + +Naming convention: + +package +1. Either a python package +2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) + +plugin +1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) +2. The string name to above object + +name +The string name of the plugin object for registration + +method +1. In case the object is a class instance a bound method to call from the class instance +2. The string name to above object + + + +## PluginPackageClass Objects + +```python +class PluginPackageClass() +``` + +A local data class for holding all information about a loaded plugin package + + + +#### register + +```python +@overload +def register(plugin: Callable) -> Callable +``` + +1-level decorator around a function + + + +#### register + +```python +@overload +def register(plugin: Type) -> Any +``` + +Signature: 1-level decorator around a class + + + +#### register + +```python +@overload +def register(*, name: str, package: Optional[str] = None) -> Callable +``` + +Signature: 2-level decorator around a function + + + +#### register + +```python +@overload +def register(*, auto_tag: bool = False, package: Optional[str] = None) -> Type +``` + +Signature: 2-level decorator around a class + + + +#### register + +```python +@overload +def register(plugin: Callable[..., Any] = None, + *, + name: Optional[str] = None, + package: Optional[str] = None, + replace: bool = False) -> Callable +``` + +Signature: Run-time registration of function / class instance / bound method + + + +#### register + +```python +def register(plugin: Optional[Callable] = None, + *, + name: Optional[str] = None, + package: Optional[str] = None, + replace: bool = False, + auto_tag: bool = False) -> Callable +``` + +A generic decorator / run-time function to register plugin module callables + +The functions comes in five distinct signatures for 5 use cases: + +1. ``@plugs.register``: decorator for a class w/o any arguments +2. ``@plugs.register``: decorator for a function w/o any arguments +3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments +4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments +5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of + + * function + * bound method + * class instance + +For more documentation see the functions + + * :func:`_register_obj` + * :func:`_register_class` + +See the examples in Module :mod:`plugs` how to use this decorator / function + +**Arguments**: + +- `plugin`: +- `name`: +- `package`: +- `replace`: +- `auto_tag`: + + + +#### tag + +```python +def tag(func: Callable) -> Callable +``` + +Method decorator for tagging a method as callable through the plugs interface + +Note that the instantiated class must still be registered as plugin object +(either with the class decorator or dynamically) + +**Arguments**: + +- `func`: function to decorate + +**Returns**: + +the function + + + +#### initialize + +```python +def initialize(func: Callable) -> Callable +``` + +Decorator for functions that shall be called by the plugs package directly after the module is loaded + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### finalize + +```python +def finalize(func: Callable) -> Callable +``` + +Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### atexit + +```python +def atexit(func: Callable[[int], Any]) -> Callable[[int], Any] +``` + +Decorator for functions that shall be called by the plugs package directly after at exit of program. + +.. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called + during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your + shutdown handler. + +The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) +It is intended for passing down the signal number that initiated the program termination + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### load + +```python +def load(package: str, + load_as: Optional[str] = None, + prefix: Optional[str] = None) +``` + +Loads a python package as plugin package + +Executes a regular python package load. That means a potentially existing __init__.py is executed. +Decorator @register can by used to register functions / classes / class istances as plugin callable +Decorator @initializer can be used to tag functions that shall be called after package loading +Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded +Instead of using @initializer, you may of course use __init__.py + +Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under +which they are loaded as plugin package also. + +**Arguments**: + +- `package`: Python package to load as plugin package +- `load_as`: Plugin package registration name. If None the name is the python's package simple name +- `prefix`: Prefix to python package to create fully qualified name. This is used only to locate the python package +and ignored otherwise. Useful if all the plugin module are in a dedicated folder + + + +#### load\_all\_named + +```python +def load_all_named(packages_named: Mapping[str, str], + prefix: Optional[str] = None, + ignore_errors=False) +``` + +Load all packages in packages_named with mapped names + +**Arguments**: + +- `packages_named`: Dict[load_as, package] + + + +#### load\_all\_unnamed + +```python +def load_all_unnamed(packages_unnamed: Iterable[str], + prefix: Optional[str] = None, + ignore_errors=False) +``` + +Load all packages in packages_unnamed with default names + + + +#### load\_all\_finalize + +```python +def load_all_finalize(ignore_errors=False) +``` + +Calls all functions registered with @finalize from all loaded modules in the order they were loaded + +This must be executed after the last plugin package is loaded + + + +#### close\_down + +```python +def close_down(**kwargs) -> Any +``` + +Calls all functions registered with @atexit from all loaded modules in reverse order of module load order + +Modules are processed in reverse order. Several at-exit tagged functions of a single module are processed +in the order of registration. + +Errors raised in functions are suppressed to ensure all plugins are processed + + + + +#### call + +```python +def call(package: str, + plugin: str, + method: Optional[str] = None, + *, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None) -> Any +``` + +Call a function/method from the loaded plugins + +If a plugin is a function or a callable instance of a class, this is equivalent to + +``package.plugin(*args, **kwargs)`` + +If plugin is a class instance from which a method is called, this is equivalent to the followig. +Also remember, that method must have the attribute ``plugin_callable = True`` + +``package.plugin.method(*args, **kwargs)`` + +Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. + +.. note:: + There is no logger in this function as they all belong up-level where the exceptions are handled. + If you want logger messages instead of exceptions, use :func:`call_ignore_errors` + +**Arguments**: + +- `package`: Name of the plugin package in which to look for function/class instance +- `plugin`: Function name or instance name of a class +- `method`: Method name when accessing a class instance' method. Leave at *None* if unneeded. +- `as_thread`: Run the callable in separate daemon thread. +There is no return value from the callable in this case! The return value is the thread object. +Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. +All threads are started as daemon threads with terminate upon main program termination. +There is not stop-thread mechanism. This is intended for short lived threads. +- `thread_name`: Name of the thread +- `args`: Arguments passed to callable +- `kwargs`: Keyword arguments passed to callable + +**Returns**: + +The return value from the called function, or, if started as thread the thread object + + + +#### call\_ignore\_errors + +```python +def call_ignore_errors(package: str, + plugin: str, + method: Optional[str] = None, + *, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None) -> Any +``` + +Call a function/method from the loaded plugins ignoring all raised Exceptions. + +Errors get logged. + +See :func:`call` for parameter documentation. + + + +#### exists + +```python +def exists(package: str, + plugin: Optional[str] = None, + method: Optional[str] = None) -> bool +``` + +Check if an object is registered within the plugs package + + + +#### get + +```python +def get(package: str, + plugin: Optional[str] = None, + method: Optional[str] = None) -> Any +``` + +Get a plugs-package registered object + +The return object depends on the number of parameters + +* 1 argument: Get the python module reference for the plugs *package* +* 2 arguments: Get the plugin reference for the plugs *package.plugin* +* 3 arguments: Get the plugin reference for the plugs *package.plugin.method* + + + +#### loaded\_as + +```python +def loaded_as(module_name: str) -> str +``` + +Return the plugin name a python module is loaded as + + + +#### delete + +```python +def delete(package: str, plugin: Optional[str] = None, ignore_errors=False) +``` + +Delete a plugin object from the registered plugs callables + +Note: This does not 'unload' the python module. It merely makes it un-callable via plugs! + + + +#### dump\_plugins + +```python +def dump_plugins(stream) +``` + +Write a human readable summary of all plugin callables to stream + + + +#### summarize + +```python +def summarize() +``` + +Create a reference summary of all plugin callables in dictionary format + + + +#### generate\_help\_rst + +```python +def generate_help_rst(stream) +``` + +Write a reference of all plugin callables in Restructured Text format + + + +#### get\_all\_loaded\_packages + +```python +def get_all_loaded_packages() -> Dict[str, str] +``` + +Report a short summary of all loaded packages + +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` + + + +#### get\_all\_failed\_packages + +```python +def get_all_failed_packages() -> Dict[str, str] +``` + +Report those packages that did not load error free + +.. note:: Package could fail to load + + 1. altogether: these package are not registered + 2. partially: during initializer, finalizer functions: The package is loaded, + but the function did not execute error-free + + Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED + +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` + + + +# jukebox.speaking\_text + +Text to Speech. Plugin to speak any given text via speaker + + + +# jukebox.multitimer + +Multitimer Module + + + +## MultiTimer Objects + +```python +class MultiTimer(threading.Thread) +``` + +Call a function after a specified number of seconds, repeat that iteration times + +May be cancelled during any of the wait times. +Function is called with keyword parameter 'iteration' (which decreases down to 0 for the last iteration) + +If iterations is negative, an endlessly repeating timer is created (which needs to be cancelled with cancel()) + +Initiates start and publishing by calling self.publish_callback + +Note: Inspired by threading.Timer and generally using the same API + + + +#### cancel + +```python +def cancel() +``` + +Stop the timer if it hasn't finished all iterations yet. + + + +## GenericTimerClass Objects + +```python +class GenericTimerClass() +``` + +Interface for plugin / RPC accessibility for a single event timer + + + +#### \_\_init\_\_ + +```python +def __init__(name, wait_seconds: float, function, args=None, kwargs=None) +``` + +**Arguments**: + +- `wait_seconds`: The time in seconds to wait before calling function +- `function`: The function to call with args and kwargs. +- `args`: Parameters for function call +- `kwargs`: Parameters for function call + + + +#### start + +```python +@plugin.tag +def start(wait_seconds=None) +``` + +Start the timer (with default or new parameters) + + + +#### cancel + +```python +@plugin.tag +def cancel() +``` + +Cancel the timer + + + +#### toggle + +```python +@plugin.tag +def toggle() +``` + +Toggle the activation of the timer + + + +#### trigger + +```python +@plugin.tag +def trigger() +``` + +Trigger the next target execution before the time is up + + + +#### is\_alive + +```python +@plugin.tag +def is_alive() +``` + +Check if timer is active + + + +#### get\_timeout + +```python +@plugin.tag +def get_timeout() +``` + +Get the configured time-out + +**Returns**: + +The total wait time. (Not the remaining wait time!) + + + +#### set\_timeout + +```python +@plugin.tag +def set_timeout(wait_seconds: float) +``` + +Set a new time-out in seconds. Re-starts the timer if already running! + + + +#### publish + +```python +@plugin.tag +def publish() +``` + +Publish the current state and config + + + +#### get\_state + +```python +@plugin.tag +def get_state() +``` + +Get the current state and config as dictionary + + + +## GenericEndlessTimerClass Objects + +```python +class GenericEndlessTimerClass(GenericTimerClass) +``` + +Interface for plugin / RPC accessibility for an event timer call function endlessly every m seconds + + + +## GenericMultiTimerClass Objects + +```python +class GenericMultiTimerClass(GenericTimerClass) +``` + +Interface for plugin / RPC accessibility for an event timer that performs an action n times every m seconds + + + +#### \_\_init\_\_ + +```python +def __init__(name, + iterations: int, + wait_seconds_per_iteration: float, + callee, + args=None, + kwargs=None) +``` + +**Arguments**: + +- `iterations`: Number of times callee is called +- `wait_seconds_per_iteration`: Wait in seconds before each iteration +- `callee`: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). +Then with every time out iteration __call__(*args, iteration=iteration, **kwargs) is called. +'iteration' is the current iteration count in decreasing order! +- `args`: +- `kwargs`: + + + +#### start + +```python +@plugin.tag +def start(iterations=None, wait_seconds_per_iteration=None) +``` + +Start the timer (with default or new parameters) + + + +# jukebox.utils + +Common utility functions + + + +#### decode\_rpc\_call + +```python +def decode_rpc_call(cfg_rpc_call: Dict) -> Optional[Dict] +``` + +Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. + +.. important: Leaves all other parameters in cfg_action untouched or later downstream processing! + +**Arguments**: + +- `cfg_rpc_call`: RPC command as configuration entry + +**Returns**: + +A fully populated deep copy of cfg_rpc_call + + + +#### decode\_rpc\_command + +```python +def decode_rpc_command(cfg_rpc_cmd: Dict, + logger: logging.Logger = log) -> Optional[Dict] +``` + +Decode an RPC Command from a config entry. + +This means + + * Decode RPC command alias (if present) + * Ensure all RPC call parameters have valid default values + +If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call +which emits a misuse warning when called +If an explicitly specified this is not done. However, it is ensured that the returned +dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling +for non-existing RPC commands and we get a clearer error message. + +**Arguments**: + +- `cfg_rpc_cmd`: RPC command as configuration entry +- `logger`: The logger to use + +**Returns**: + +A decoded, fully populated deep copy of cfg_rpc_cmd + + + +#### decode\_and\_call\_rpc\_command + +```python +def decode_and_call_rpc_command(rpc_cmd: Dict, logger: logging.Logger = log) +``` + +Convenience function combining decode_rpc_command and plugs.call_ignore_errors + + + +#### bind\_rpc\_command + +```python +def bind_rpc_command(cfg_rpc_cmd: Dict, + dereference=False, + logger: logging.Logger = log) +``` + +Decode an RPC command configuration entry and bind it to a function + +**Arguments**: + +- `dereference`: Dereference even the call to plugs.call(...) + ``. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with + all checks applied at bind time + ``. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with + all checks applied at bind time. + +Setting deference to True, circumvents the dynamic nature of the plugins: the function to call + must exist at bind time and cannot change. If False, the function to call must only exist at call time. + This can be important during the initialization where package ordering and initialization means that not all + classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls + is circumvented. Use with care! + +**Returns**: + +Callable function w/o parameters which directly runs the RPC command +using plugs.call_ignore_errors + + + +#### rpc\_call\_to\_str + +```python +def rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) -> str +``` + +Return a readable string of an RPC call config + +**Arguments**: + +- `cfg_rpc_call`: RPC call configuration entry +- `with_args`: Return string shall include the arguments of the function + + + +#### generate\_cmd\_alias\_rst + +```python +def generate_cmd_alias_rst(stream) +``` + +Write a reference of all rpc command aliases in Restructured Text format + + + +#### generate\_cmd\_alias\_reference + +```python +def generate_cmd_alias_reference(stream) +``` + +Write a reference of all rpc command aliases in text format + + + +#### get\_git\_state + +```python +def get_git_state() +``` + +Return git state information for the current branch + + + +# jukebox.rpc + + + +# jukebox.rpc.client + + + +# jukebox.rpc.server + +Remote Procedure Call Server (RPC) +************************************* + +Bind to tcp and/or websocket port and translates incoming requests to procedure calls. +Avaiable procedures to call are all functions registered with the plugin package. + +To protocol is loosely based on `jsonrpc `_ + +But with different elements directly relating to the plugin concept and Python function argument options + +.. code-block:: yaml + + { + 'package' : str # The plugin package loaded from python module + 'plugin' : str # The plugin object to be accessed from the package + # (i.e. function or class instance) + 'method' : str # (optional) The method of the class instance + 'args' : [ ] # (optional) Positional arguments as list + 'kwargs' : { } # (optional) Keyword arguments as dictionary + 'as_thread': bool # (optional) start call in separate thread + 'id' : Any # (optional) Round-trip id for response (may not be None) + 'tsp' : Any # (optional) measure and return total processing time for + # the call request (may not be None) + } + +**Response** + +A response will ALWAYS be send, independent of presence of 'id'. This is in difference to the +jsonrpc specification. But this is a ZeroMQB REQ/REP pattern requirement! + +If 'id' is omitted, the response will be 'None'! Unless an error occurred, then the error is returned. +The absence of 'id' indicates that the requester is not interested in the response. +If present, 'id' and 'tsp' may not be None. If they are None, there are treated as if non-existing. + +**Sockets** + +Three sockets are opened + +``. TCP (on a configurable port) +``. Websocket (on a configurable port) +``. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be + call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though + the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which + button triggers what action) + + + +## RpcServer Objects + +```python +class RpcServer() +``` + +The RPC Server Class + + + +#### \_\_init\_\_ + +```python +def __init__(context=None) +``` + +Initialize the connections and bind to the ports + + + +#### run + +```python +def run() +``` + +The main endless loop waiting for requests and forwarding the +call request to the plugin module + diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 07c0eee20..c00eb47e7 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -1,8 +1,12 @@ loaders: - type: python search_path: [./src/jukebox] +processors: + - type: filter + - type: smart + - type: crossref renderer: type: markdown render_toc: true - filename: ./documentation/developers/docstring-pydoc-markdown/docstring.md + filename: ./documentation/developers/docstring/docstring-api.md render_page_title: true diff --git a/requirements.txt b/requirements.txt index 6fe10e6a0..c4fd8a28d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,6 +34,4 @@ pytest mock # API docs generation -lazydocs -pydocstyle pydoc-markdown diff --git a/run_docstring2markdown.sh b/run_docstring2markdown.sh index 509716b1a..444324676 100755 --- a/run_docstring2markdown.sh +++ b/run_docstring2markdown.sh @@ -8,16 +8,8 @@ SOURCE=${BASH_SOURCE[0]} SCRIPT_DIR="$(dirname "$SOURCE")" cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && exit 1) -# Run lazydocs -lazydocs \ - --output-path="./documentation/developers/docstring-lazydocs" \ - --overview-file="docstring.md" \ - --src-base-url="https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/future3/develop/" \ - --ignored-modules=r"ruamel,pulsectl" \ - ./src/jukebox - # Run pydoc-markdown # make sure, directory exists -mkdir -p ./documentation/developers/docstring-pydoc-markdown +mkdir -p ./documentation/developers/docstring # expects pydoc-markdown.yml at working dir pydoc-markdown \ No newline at end of file From 922c1870f38b6a534ffde4fbc0a8d0401adfac2b Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 21:24:22 +0100 Subject: [PATCH 10/41] remove the test files from version control --- .../developers/docstring-lazydocs/.pages | 4 - .../components.battery_monitor.md | 16 - .../docstring-lazydocs/components.controls.md | 16 - .../docstring-lazydocs/components.md | 16 - .../docstring-lazydocs/components.rfid.md | 19 - .../components.rfid.readerbase.md | 83 - .../components.rpc_command_alias.md | 19 - .../components.synchronisation.md | 16 - .../components.synchronisation.syncutils.md | 75 - .../docstring-lazydocs/docstring.md | 94 - .../docstring-lazydocs/jukebox.NvManager.md | 136 - .../docstring-lazydocs/jukebox.callingback.md | 75 - .../developers/docstring-lazydocs/jukebox.md | 16 - .../jukebox.playlistgenerator.md | 238 - .../docstring-lazydocs/jukebox.plugs.md | 545 -- .../docstring-lazydocs/jukebox.rpc.md | 16 - .../docstring-lazydocs/jukebox.utils.md | 167 - .../docstring-lazydocs/jukebox.version.md | 49 - .../docstring-lazydocs/misc.inputminus.md | 69 - .../developers/docstring-lazydocs/misc.md | 58 - .../docstring-lazydocs/misc.simplecolors.md | 66 - .../docstring-pydoc-markdown/docstring.md | 5648 ----------------- 22 files changed, 7441 deletions(-) delete mode 100644 documentation/developers/docstring-lazydocs/.pages delete mode 100644 documentation/developers/docstring-lazydocs/components.battery_monitor.md delete mode 100644 documentation/developers/docstring-lazydocs/components.controls.md delete mode 100644 documentation/developers/docstring-lazydocs/components.md delete mode 100644 documentation/developers/docstring-lazydocs/components.rfid.md delete mode 100644 documentation/developers/docstring-lazydocs/components.rfid.readerbase.md delete mode 100644 documentation/developers/docstring-lazydocs/components.rpc_command_alias.md delete mode 100644 documentation/developers/docstring-lazydocs/components.synchronisation.md delete mode 100644 documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md delete mode 100644 documentation/developers/docstring-lazydocs/docstring.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.NvManager.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.callingback.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.plugs.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.rpc.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.utils.md delete mode 100644 documentation/developers/docstring-lazydocs/jukebox.version.md delete mode 100644 documentation/developers/docstring-lazydocs/misc.inputminus.md delete mode 100644 documentation/developers/docstring-lazydocs/misc.md delete mode 100644 documentation/developers/docstring-lazydocs/misc.simplecolors.md delete mode 100644 documentation/developers/docstring-pydoc-markdown/docstring.md diff --git a/documentation/developers/docstring-lazydocs/.pages b/documentation/developers/docstring-lazydocs/.pages deleted file mode 100644 index d09244c0d..000000000 --- a/documentation/developers/docstring-lazydocs/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: API Reference -nav: - - Overview: docstring.md - - ... diff --git a/documentation/developers/docstring-lazydocs/components.battery_monitor.md b/documentation/developers/docstring-lazydocs/components.battery_monitor.md deleted file mode 100644 index e54396848..000000000 --- a/documentation/developers/docstring-lazydocs/components.battery_monitor.md +++ /dev/null @@ -1,16 +0,0 @@ - - - - -# module `components.battery_monitor` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.controls.md b/documentation/developers/docstring-lazydocs/components.controls.md deleted file mode 100644 index 57b61e430..000000000 --- a/documentation/developers/docstring-lazydocs/components.controls.md +++ /dev/null @@ -1,16 +0,0 @@ - - - - -# module `components.controls` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.md b/documentation/developers/docstring-lazydocs/components.md deleted file mode 100644 index bef8bf650..000000000 --- a/documentation/developers/docstring-lazydocs/components.md +++ /dev/null @@ -1,16 +0,0 @@ - - - - -# module `components` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.rfid.md b/documentation/developers/docstring-lazydocs/components.rfid.md deleted file mode 100644 index 6dc5a428c..000000000 --- a/documentation/developers/docstring-lazydocs/components.rfid.md +++ /dev/null @@ -1,19 +0,0 @@ - - - - -# module `components.rfid` - - - - -**Global Variables** ---------------- -- **readerbase** - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md b/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md deleted file mode 100644 index 805dba289..000000000 --- a/documentation/developers/docstring-lazydocs/components.rfid.readerbase.md +++ /dev/null @@ -1,83 +0,0 @@ - - - - -# module `components.rfid.readerbase` - - - - - - ---- - - - -## class `ReaderBaseClass` -Abstract Base Class for all Reader Classes to ensure common API - -Look at template_new_reader.py for documentation how to integrate a new RFID reader - - - -### method `__init__` - -```python -__init__(reader_cfg_key: str, description: str, logger: Logger) -``` - - - - - - - - ---- - - - -### method `cleanup` - -```python -cleanup() -``` - - - - - ---- - - - -### method `read_card` - -```python -read_card() -``` - - - - - ---- - - - -### method `stop` - -```python -stop() -``` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md b/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md deleted file mode 100644 index c001e4cfe..000000000 --- a/documentation/developers/docstring-lazydocs/components.rpc_command_alias.md +++ /dev/null @@ -1,19 +0,0 @@ - - - - -# module `components.rpc_command_alias` -This file provides definitions for RPC command aliases - -See :ref:`userguide/rpc_commands` - -**Global Variables** ---------------- -- **cmd_alias_definitions** - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.synchronisation.md b/documentation/developers/docstring-lazydocs/components.synchronisation.md deleted file mode 100644 index 54a208d07..000000000 --- a/documentation/developers/docstring-lazydocs/components.synchronisation.md +++ /dev/null @@ -1,16 +0,0 @@ - - - - -# module `components.synchronisation` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md b/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md deleted file mode 100644 index 6e34601f3..000000000 --- a/documentation/developers/docstring-lazydocs/components.synchronisation.syncutils.md +++ /dev/null @@ -1,75 +0,0 @@ - - - - -# module `components.synchronisation.syncutils` - - - - - ---- - - - -## function `clean_foldername` - -```python -clean_foldername(lib_path: str, folder: str) → str -``` - - - - - - ---- - - - -## function `ensure_trailing_slash` - -```python -ensure_trailing_slash(path: str) → str -``` - - - - - - ---- - - - -## function `remove_trailing_slash` - -```python -remove_trailing_slash(path: str) → str -``` - - - - - - ---- - - - -## function `remove_leading_slash` - -```python -remove_leading_slash(path: str) → str -``` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/docstring.md b/documentation/developers/docstring-lazydocs/docstring.md deleted file mode 100644 index 2f7d2355b..000000000 --- a/documentation/developers/docstring-lazydocs/docstring.md +++ /dev/null @@ -1,94 +0,0 @@ - - -# API Overview - -## Modules - -- [`components`](./components.md#module-components) -- [`components.battery_monitor`](./components.battery_monitor.md#module-componentsbattery_monitor) -- [`components.controls`](./components.controls.md#module-componentscontrols) -- [`components.rfid`](./components.rfid.md#module-componentsrfid) -- [`components.rfid.readerbase`](./components.rfid.readerbase.md#module-componentsrfidreaderbase) -- [`components.rpc_command_alias`](./components.rpc_command_alias.md#module-componentsrpc_command_alias): This file provides definitions for RPC command aliases -- [`components.synchronisation`](./components.synchronisation.md#module-componentssynchronisation) -- [`components.synchronisation.syncutils`](./components.synchronisation.syncutils.md#module-componentssynchronisationsyncutils) -- [`jukebox`](./jukebox.md#module-jukebox) -- [`jukebox.NvManager`](./jukebox.NvManager.md#module-jukeboxnvmanager) -- [`jukebox.callingback`](./jukebox.callingback.md#module-jukeboxcallingback): Provides a generic callback handler -- [`jukebox.playlistgenerator`](./jukebox.playlistgenerator.md#module-jukeboxplaylistgenerator): Playlists are build from directory content in the following way: -- [`jukebox.plugs`](./jukebox.plugs.md#module-jukeboxplugs): A plugin package with some special functionality -- [`jukebox.rpc`](./jukebox.rpc.md#module-jukeboxrpc) -- [`jukebox.utils`](./jukebox.utils.md#module-jukeboxutils): Common utility functions -- [`jukebox.version`](./jukebox.version.md#module-jukeboxversion) -- [`misc`](./misc.md#module-misc) -- [`misc.inputminus`](./misc.inputminus.md#module-miscinputminus): Zero 3rd-party dependency module for user prompting -- [`misc.simplecolors`](./misc.simplecolors.md#module-miscsimplecolors): Zero 3rd-party dependency module to add colors to unix terminal output - -## Classes - -- [`readerbase.ReaderBaseClass`](./components.rfid.readerbase.md#class-readerbaseclass): Abstract Base Class for all Reader Classes to ensure common API -- [`NvManager.nv_dict`](./jukebox.NvManager.md#class-nv_dict) -- [`NvManager.nv_manager`](./jukebox.NvManager.md#class-nv_manager) -- [`callingback.CallbackHandler`](./jukebox.callingback.md#class-callbackhandler): Generic Callback Handler to collect callbacks functions through :func:`register` and execute them -- [`playlistgenerator.PlaylistCollector`](./jukebox.playlistgenerator.md#class-playlistcollector): Build a playlist from directory(s) -- [`playlistgenerator.PlaylistEntry`](./jukebox.playlistgenerator.md#class-playlistentry) -- [`plugs.PluginPackageClass`](./jukebox.plugs.md#class-pluginpackageclass): A local data class for holding all information about a loaded plugin package -- [`simplecolors.Colors`](./misc.simplecolors.md#class-colors): Container class for all the colors as constants - -## Functions - -- [`syncutils.clean_foldername`](./components.synchronisation.syncutils.md#function-clean_foldername) -- [`syncutils.ensure_trailing_slash`](./components.synchronisation.syncutils.md#function-ensure_trailing_slash) -- [`syncutils.remove_leading_slash`](./components.synchronisation.syncutils.md#function-remove_leading_slash) -- [`syncutils.remove_trailing_slash`](./components.synchronisation.syncutils.md#function-remove_trailing_slash) -- [`playlistgenerator.decode_livestream`](./jukebox.playlistgenerator.md#function-decode_livestream) -- [`playlistgenerator.decode_m3u`](./jukebox.playlistgenerator.md#function-decode_m3u) -- [`playlistgenerator.decode_musicfile`](./jukebox.playlistgenerator.md#function-decode_musicfile) -- [`playlistgenerator.decode_podcast`](./jukebox.playlistgenerator.md#function-decode_podcast) -- [`playlistgenerator.decode_podcast_core`](./jukebox.playlistgenerator.md#function-decode_podcast_core) -- [`plugs.atexit`](./jukebox.plugs.md#function-atexit): Decorator for functions that shall be called by the plugs package directly after at exit of program. -- [`plugs.call`](./jukebox.plugs.md#function-call): Call a function/method from the loaded plugins -- [`plugs.call_ignore_errors`](./jukebox.plugs.md#function-call_ignore_errors): Call a function/method from the loaded plugins ignoring all raised Exceptions. -- [`plugs.close_down`](./jukebox.plugs.md#function-close_down): Calls all functions registered with @atexit from all loaded modules in reverse order of module load order -- [`plugs.delete`](./jukebox.plugs.md#function-delete): Delete a plugin object from the registered plugs callables -- [`plugs.dereference`](./jukebox.plugs.md#function-dereference) -- [`plugs.dump_plugins`](./jukebox.plugs.md#function-dump_plugins): Write a human readable summary of all plugin callables to stream -- [`plugs.exists`](./jukebox.plugs.md#function-exists): Check if an object is registered within the plugs package -- [`plugs.finalize`](./jukebox.plugs.md#function-finalize): Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded -- [`plugs.generate_help_rst`](./jukebox.plugs.md#function-generate_help_rst): Write a reference of all plugin callables in Restructured Text format -- [`plugs.get`](./jukebox.plugs.md#function-get): Get a plugs-package registered object -- [`plugs.get_all_failed_packages`](./jukebox.plugs.md#function-get_all_failed_packages): Report those packages that did not load error free -- [`plugs.get_all_loaded_packages`](./jukebox.plugs.md#function-get_all_loaded_packages): Report a short summary of all loaded packages -- [`plugs.initialize`](./jukebox.plugs.md#function-initialize): Decorator for functions that shall be called by the plugs package directly after the module is loaded -- [`plugs.load`](./jukebox.plugs.md#function-load): Loads a python package as plugin package -- [`plugs.load_all_finalize`](./jukebox.plugs.md#function-load_all_finalize): Calls all functions registered with @finalize from all loaded modules in the order they were loaded -- [`plugs.load_all_named`](./jukebox.plugs.md#function-load_all_named): Load all packages in packages_named with mapped names -- [`plugs.load_all_unnamed`](./jukebox.plugs.md#function-load_all_unnamed): Load all packages in packages_unnamed with default names -- [`plugs.loaded_as`](./jukebox.plugs.md#function-loaded_as): Return the plugin name a python module is loaded as -- [`plugs.register`](./jukebox.plugs.md#function-register): A generic decorator / run-time function to register plugin module callables -- [`plugs.summarize`](./jukebox.plugs.md#function-summarize): Create a reference summary of all plugin callables in dictionary format -- [`plugs.tag`](./jukebox.plugs.md#function-tag): Method decorator for tagging a method as callable through the plugs interface -- [`utils.bind_rpc_command`](./jukebox.utils.md#function-bind_rpc_command): Decode an RPC command configuration entry and bind it to a function -- [`utils.decode_and_call_rpc_command`](./jukebox.utils.md#function-decode_and_call_rpc_command): Convenience function combining decode_rpc_command and plugs.call_ignore_errors -- [`utils.decode_rpc_call`](./jukebox.utils.md#function-decode_rpc_call): Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. -- [`utils.decode_rpc_command`](./jukebox.utils.md#function-decode_rpc_command): Decode an RPC Command from a config entry. -- [`utils.generate_cmd_alias_reference`](./jukebox.utils.md#function-generate_cmd_alias_reference): Write a reference of all rpc command aliases in text format -- [`utils.generate_cmd_alias_rst`](./jukebox.utils.md#function-generate_cmd_alias_rst): Write a reference of all rpc command aliases in Restructured Text format -- [`utils.get_git_state`](./jukebox.utils.md#function-get_git_state): Return git state information for the current branch -- [`utils.indent`](./jukebox.utils.md#function-indent) -- [`utils.rpc_call_to_str`](./jukebox.utils.md#function-rpc_call_to_str): Return a readable string of an RPC call config -- [`version.version`](./jukebox.version.md#function-version): Return the Jukebox version as a string -- [`version.version_info`](./jukebox.version.md#function-version_info): Return the Jukebox version as a tuple of three numbers -- [`misc.flatten`](./misc.md#function-flatten): Flatten all levels of hierarchy in nested iterables -- [`misc.getattr_hierarchical`](./misc.md#function-getattr_hierarchical): Like the builtin getattr, but descends though the hierarchy levels -- [`misc.recursive_chmod`](./misc.md#function-recursive_chmod): Recursively change folder and file permissions -- [`inputminus.input_int`](./misc.inputminus.md#function-input_int): Request an integer input from user -- [`inputminus.input_yesno`](./misc.inputminus.md#function-input_yesno): Request a yes / no choice from user -- [`inputminus.msg_highlight`](./misc.inputminus.md#function-msg_highlight) -- [`simplecolors.print`](./misc.simplecolors.md#function-print): Drop-in replacement for print with color choice and auto color reset for convenience -- [`simplecolors.resolve`](./misc.simplecolors.md#function-resolve): Resolve a color name into the respective color constant - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.NvManager.md b/documentation/developers/docstring-lazydocs/jukebox.NvManager.md deleted file mode 100644 index 6a79f2aab..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.NvManager.md +++ /dev/null @@ -1,136 +0,0 @@ - - - - -# module `jukebox.NvManager` - - - - - - ---- - - - -## class `nv_manager` - - - - - - -### method `__init__` - -```python -__init__() -``` - - - - - - - - ---- - - - -### method `load` - -```python -load(default_filename, type=None) -``` - - - - - ---- - - - -### method `save_all` - -```python -save_all() -``` - - - - - - ---- - - - -## class `nv_dict` - - - - - - -### method `__init__` - -```python -__init__(default_filename=None) -``` - - - - - - - - ---- - - - -### method `hash` - -```python -hash() -``` - - - - - ---- - - - -### method `init_from_json` - -```python -init_from_json(path=None, merge=False) -``` - - - - - ---- - - - -### method `save_to_json` - -```python -save_to_json(filename=None) -``` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.callingback.md b/documentation/developers/docstring-lazydocs/jukebox.callingback.md deleted file mode 100644 index 262e6f947..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.callingback.md +++ /dev/null @@ -1,75 +0,0 @@ - - - - -# module `jukebox.callingback` -Provides a generic callback handler - - - ---- - - - -## class `CallbackHandler` -Generic Callback Handler to collect callbacks functions through :func:`register` and execute them with :func:`run_callbacks` - -A lock is used to sequence registering of new functions and running callbacks. - -:param name: A name of this handler for usage in log messages :param logger: The logger instance to use for logging :param context: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created - - - -### method `__init__` - -```python -__init__(name: str, logger: Logger, context=None) -``` - - - - - - ---- - -#### property has_callbacks - -:data:`True` if there are any registered callbacks. Read-only property - - - ---- - - - -### method `register` - -```python -register(func: Optional[Callable[, NoneType]]) -``` - -Register a new function to be executed when the callback event happens - -:param func: The function to register. If set to :data:`None`, this register request is silently ignored. - ---- - - - -### method `run_callbacks` - -```python -run_callbacks(*args, **kwargs) -``` - -Run all registered callbacks. - -*ALL* exceptions from callback functions will be caught and logged only. Exceptions are not raised upwards! - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.md b/documentation/developers/docstring-lazydocs/jukebox.md deleted file mode 100644 index ebef95c0d..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.md +++ /dev/null @@ -1,16 +0,0 @@ - - - - -# module `jukebox` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md b/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md deleted file mode 100644 index 751374438..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.playlistgenerator.md +++ /dev/null @@ -1,238 +0,0 @@ - - - - -# module `jukebox.playlistgenerator` -Playlists are build from directory content in the following way: a directory is parsed and files are added to the playlist in the following way - -1. files are added in alphabetic order 2. files ending with ``*livestream.txt`` are unpacked and the containing URL(s) are added verbatim to the playlist 3. files ending with ``*podcast.txt`` are unpacked and the containing Podcast URL(s) are expanded and added to the playlist 4. files ending with ``*.m3u`` are treated as folder playlist. Regular folder processing is suspended and the playlist is build solely from the ``*.m3u`` content. Only the alphabetically first ``*.m3u`` is processed. URLs are added verbatim to the playlist except for ``*.xml`` and ``*.podcast`` URLS, which are expanded first - -An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. - -.. code-block:: bash - - 01-livestream.txt 02-livestream.txt music.mp3 podcast.txt - -All files are treated as music files and are added to the playlist, except those: - -* starting with ``.``, * not having a file ending, i.e. do not contain a ``.``, * ending with ``.txt``, * ending with ``.m3u``, * ending with one of the excluded file endings in :attr:`PlaylistCollector._exclude_endings` - -In recursive mode, the playlist is generated by concatenating all sub-folder playlists. Sub-folders are parsed in alphabetic order. Symbolic links are being followed. The above rules are enforced on a per-folder bases. This means, one ``*.m3u`` file per sub-folder is processed (if present). - -In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. - -**Global Variables** ---------------- -- **TYPE_FILE** -- **TYPE_DIR** -- **TYPE_STREAM** -- **TYPE_PODCAST** -- **TYPE_DECODE** - ---- - - - -## function `decode_podcast_core` - -```python -decode_podcast_core(url, playlist) -``` - - - - - - ---- - - - -## function `decode_podcast` - -```python -decode_podcast(filename: str, path, playlist) -``` - - - - - - ---- - - - -## function `decode_livestream` - -```python -decode_livestream(filename: DirEntry, path, playlist) -``` - - - - - - ---- - - - -## function `decode_musicfile` - -```python -decode_musicfile(filename: DirEntry, path, playlist) -``` - - - - - - ---- - - - -## function `decode_m3u` - -```python -decode_m3u(filename: DirEntry, path, playlist: List[PlaylistEntry]) -``` - - - - - - ---- - - - -## class `PlaylistEntry` - - - - - - -### method `__init__` - -```python -__init__(filetype: int, name: str, path: str) -``` - - - - - - ---- - -#### property filetype - - - - - ---- - -#### property name - - - - - ---- - -#### property path - - - - - - - - ---- - - - -## class `PlaylistCollector` -Build a playlist from directory(s) - -This class is intended to be used with an absolute path to the music library: -``` - - plc = PlaylistCollector('/home/chris/music') plc.parse('Traumfaenger') print(f"res = {plc}") - -But it can also be used with relative paths from current working directory: -``` - - plc = PlaylistCollector('.') plc.parse('../../../../music/Traumfaenger') print(f"res = {plc}") - -``` -The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. - - - -### method `__init__` - -```python -__init__(music_library_base_path='/') -``` - -Initialize the playlist generator with music_library_base_path - -:param music_library_base_path: Base path the the music library. This is used to locate the file in the disk but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir - - - - ---- - - - -### method `get_directory_content` - -```python -get_directory_content(path='.') -``` - -Parse the folder ``path`` and create a content list. Depth is always the current level - -:param path: Path to folder **relative** to ``music_library_base_path`` :return: [ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] where type is one of :attr:`TYPE_DECODE` - ---- - - - -### method `parse` - -```python -parse(path='.', recursive=False) -``` - -Parse the folder ``path`` and create a playlist from it's content - -:param path: Path to folder **relative** to ``music_library_base_path`` :param recursive: Parse folder recursivley, or stay in top-level folder - ---- - - - -### classmethod `set_exclusion_endings` - -```python -set_exclusion_endings(endings: List[str]) -``` - -Set the class-wide file ending exclusion list - -See :attr:`PlaylistCollector._exclude_endings` - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.plugs.md b/documentation/developers/docstring-lazydocs/jukebox.plugs.md deleted file mode 100644 index f97633dbe..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.plugs.md +++ /dev/null @@ -1,545 +0,0 @@ - - - - -# module `jukebox.plugs` -A plugin package with some special functionality - -Plugins packages are python packages that are dynamically loaded. From these packages only a subset of objects is exposed through the plugs.call interface. The python packages can use decorators or dynamic function call to register (callable) objects. - -The python package name may be different from the name the package is registered under in plugs. This allows to load different python packages for a specific feature based on a configuration file. Note: Python package are still loaded as regular python packages and can be accessed by normal means - -If you want to provide additional functionality to the same feature (probably even for run-time switching) you can implement a Factory Pattern using this package. Take a look at volume.py as an example. - -**Example:** Decorate a function for auto-registering under it's own name: -``` - - import jukebox.plugs as plugs @plugs.register def func1(param): pass - -**Example:** Decorate a function for auto-registering under a new name: -``` - - @plugs.register(name='better_name') def func2(param): pass - -**Example:** Register a function during run-time under it's own name: -``` - - def func3(param): pass plugs.register(func3) - -**Example:** Register a function during run-time under a new name: -``` - - def func4(param): pass plugs.register(func4, name='other_name', package='other_package') - -``` -**Example:** Decorate a class for auto registering during initialization, including all methods (see _register_class for more info): -``` - - @plugs.register(auto_tag=True) class MyClass1: pass - -**Example:** Register a class instance, from which only report is a callable method through the plugs interface: -``` - - class MyClass2: @plugs.tag def report(self): pass myinst2 = MyClass2() plugin.register(myinst2, name='myinst2') - -``` -Naming convention: - -package 1. Either a python package 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) - -plugin 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) 2. The string name to above object - -name The string name of the plugin object for registration - -method 1. In case the object is a class instance a bound method to call from the class instance 2. The string name to above object - -**Global Variables** ---------------- -- **ALLOW_DIRECT_IMPORTS** - ---- - - - -## function `register` - -```python -register( - plugin: Optional[Callable] = None, - name: Optional[str] = None, - package: Optional[str] = None, - replace: bool = False, - auto_tag: bool = False -) → Callable -``` - -A generic decorator / run-time function to register plugin module callables - -The functions comes in five distinct signatures for 5 use cases: - -1. ``@plugs.register``: decorator for a class w/o any arguments 2. ``@plugs.register``: decorator for a function w/o any arguments 3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments 4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments 5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of - - * function * bound method * class instance - -For more documentation see the functions - - * :func:`_register_obj` * :func:`_register_class` - -See the examples in Module :mod:`plugs` how to use this decorator / function - -:param plugin: :param name: :param package: :param replace: :param auto_tag: :return: - - ---- - - - -## function `tag` - -```python -tag(func: Callable) → Callable -``` - -Method decorator for tagging a method as callable through the plugs interface - -Note that the instantiated class must still be registered as plugin object (either with the class decorator or dynamically) - -:param func: function to decorate :return: the function - - ---- - - - -## function `initialize` - -```python -initialize(func: Callable) → Callable -``` - -Decorator for functions that shall be called by the plugs package directly after the module is loaded - -:param func: Function to decorate :return: The function itself - - ---- - - - -## function `finalize` - -```python -finalize(func: Callable) → Callable -``` - -Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded - -:param func: Function to decorate :return: The function itself - - ---- - - - -## function `atexit` - -```python -atexit(func: Callable[[int], Any]) → Callable[[int], Any] -``` - -Decorator for functions that shall be called by the plugs package directly after at exit of program. - -.. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your shutdown handler. - -The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) It is intended for passing down the signal number that initiated the program termination - -:param func: Function to decorate :return: The function itself - - ---- - - - -## function `load` - -```python -load(package: str, load_as: Optional[str] = None, prefix: Optional[str] = None) -``` - -Loads a python package as plugin package - -Executes a regular python package load. That means a potentially existing __init__.py is executed. Decorator @register can by used to register functions / classes / class istances as plugin callable Decorator @initializer can be used to tag functions that shall be called after package loading Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded Instead of using @initializer, you may of course use __init__.py - -Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under which they are loaded as plugin package also. - -:param package: Python package to load as plugin package :param load_as: Plugin package registration name. If None the name is the python's package simple name :param prefix: Prefix to python package to create fully qualified name. This is used only to locate the python package and ignored otherwise. Useful if all the plugin module are in a dedicated folder :return: - - ---- - - - -## function `load_all_named` - -```python -load_all_named( - packages_named: Mapping[str, str], - prefix: Optional[str] = None, - ignore_errors=False -) -``` - -Load all packages in packages_named with mapped names - -:param packages_named: Dict[load_as, package] - - ---- - - - -## function `load_all_unnamed` - -```python -load_all_unnamed( - packages_unnamed: Iterable[str], - prefix: Optional[str] = None, - ignore_errors=False -) -``` - -Load all packages in packages_unnamed with default names - - ---- - - - -## function `load_all_finalize` - -```python -load_all_finalize(ignore_errors=False) -``` - -Calls all functions registered with @finalize from all loaded modules in the order they were loaded - -This must be executed after the last plugin package is loaded - - ---- - - - -## function `close_down` - -```python -close_down(**kwargs) → Any -``` - -Calls all functions registered with @atexit from all loaded modules in reverse order of module load order - -Modules are processed in reverse order. Several at-exit tagged functions of a single module are processed in the order of registration. - -Errors raised in functions are suppressed to ensure all plugins are processed :return: - - ---- - - - -## function `dereference` - -```python -dereference( - package: str, - plugin: str, - method: Optional[str] = None, - args=None, - kwargs=None -) -``` - - - - - - ---- - - - -## function `call` - -```python -call( - package: str, - plugin: str, - method: Optional[str] = None, - args=(), - kwargs=None, - as_thread: bool = False, - thread_name: Optional[str] = None -) → Any -``` - -Call a function/method from the loaded plugins - -If a plugin is a function or a callable instance of a class, this is equivalent to - -``package.plugin(*args, **kwargs)`` - -If plugin is a class instance from which a method is called, this is equivalent to the followig. Also remember, that method must have the attribute ``plugin_callable = True`` - -``package.plugin.method(*args, **kwargs)`` - -Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. - -.. note: -``` There is no logger in this function as they all belong up-level where the exceptions are handled. If you want logger messages instead of exceptions, use :func:`call_ignore_errors` - -``` -:param package: Name of the plugin package in which to look for function/class instance :param plugin: Function name or instance name of a class :param method: Method name when accessing a class instance' method. Leave at *None* if unneeded. :param as_thread: Run the callable in separate daemon thread. There is no return value from the callable in this case! The return value is the thread object. Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. All threads are started as daemon threads with terminate upon main program termination. There is not stop-thread mechanism. This is intended for short lived threads. :param thread_name: Name of the thread :param args: Arguments passed to callable :param kwargs: Keyword arguments passed to callable :return: The return value from the called function, or, if started as thread the thread object - - ---- - - - -## function `call_ignore_errors` - -```python -call_ignore_errors( - package: str, - plugin: str, - method: Optional[str] = None, - args=(), - kwargs=None, - as_thread: bool = False, - thread_name: Optional[str] = None -) → Any -``` - -Call a function/method from the loaded plugins ignoring all raised Exceptions. - -Errors get logged. - -See :func:`call` for parameter documentation. - - ---- - - - -## function `exists` - -```python -exists( - package: str, - plugin: Optional[str] = None, - method: Optional[str] = None -) → bool -``` - -Check if an object is registered within the plugs package - - ---- - - - -## function `get` - -```python -get( - package: str, - plugin: Optional[str] = None, - method: Optional[str] = None -) → Any -``` - -Get a plugs-package registered object - -The return object depends on the number of parameters - -* 1 argument: Get the python module reference for the plugs *package* * 2 arguments: Get the plugin reference for the plugs *package.plugin* * 3 arguments: Get the plugin reference for the plugs *package.plugin.method* - - ---- - - - -## function `loaded_as` - -```python -loaded_as(module_name: str) → str -``` - -Return the plugin name a python module is loaded as - - ---- - - - -## function `delete` - -```python -delete(package: str, plugin: Optional[str] = None, ignore_errors=False) -``` - -Delete a plugin object from the registered plugs callables - -Note: This does not 'unload' the python module. It merely makes it un-callable via plugs! - - ---- - - - -## function `dump_plugins` - -```python -dump_plugins(stream) -``` - -Write a human readable summary of all plugin callables to stream - - ---- - - - -## function `summarize` - -```python -summarize() -``` - -Create a reference summary of all plugin callables in dictionary format - - ---- - - - -## function `generate_help_rst` - -```python -generate_help_rst(stream) -``` - -Write a reference of all plugin callables in Restructured Text format - - ---- - - - -## function `get_all_loaded_packages` - -```python -get_all_loaded_packages() → Dict[str, str] -``` - -Report a short summary of all loaded packages - -:return: Dictionary of the form `{loaded_as: loaded_from, ...}` - - ---- - - - -## function `get_all_failed_packages` - -```python -get_all_failed_packages() → Dict[str, str] -``` - -Report those packages that did not load error free - -.. note:: Package could fail to load - - 1. altogether: these package are not registered 2. partially: during initializer, finalizer functions: The package is loaded, but the function did not execute error-free - - Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED - -:return: Dictionary of the form `{loaded_as: loaded_from, ...}` - - ---- - - - -## class `PluginPackageClass` -A local data class for holding all information about a loaded plugin package - - - -### method `__init__` - -```python -__init__(loaded_from: str) -``` - - - - - - ---- - -#### property atexit - - - - - ---- - -#### property finalizer - - - - - ---- - -#### property initializer - - - - - ---- - -#### property loaded_from - - - - - ---- - -#### property module - - - - - ---- - -#### property plugins - - - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.rpc.md b/documentation/developers/docstring-lazydocs/jukebox.rpc.md deleted file mode 100644 index 2af4393ae..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.rpc.md +++ /dev/null @@ -1,16 +0,0 @@ - - - - -# module `jukebox.rpc` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.utils.md b/documentation/developers/docstring-lazydocs/jukebox.utils.md deleted file mode 100644 index 1bdbbfe40..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.utils.md +++ /dev/null @@ -1,167 +0,0 @@ - - - - -# module `jukebox.utils` -Common utility functions - -**Global Variables** ---------------- -- **cmd_alias_definitions** - ---- - - - -## function `decode_rpc_call` - -```python -decode_rpc_call(cfg_rpc_call: Dict) → Optional[Dict] -``` - -Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. - -.. important: Leaves all other parameters in cfg_action untouched or later downstream processing! - -:param cfg_rpc_call: RPC command as configuration entry :return: A fully populated deep copy of cfg_rpc_call - - ---- - - - -## function `decode_rpc_command` - -```python -decode_rpc_command( - cfg_rpc_cmd: Dict, - logger: Logger = -) → Optional[Dict] -``` - -Decode an RPC Command from a config entry. - -This means - - * Decode RPC command alias (if present) * Ensure all RPC call parameters have valid default values - -If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call which emits a misuse warning when called If an explicitly specified this is not done. However, it is ensured that the returned dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling for non-existing RPC commands and we get a clearer error message. - -:param cfg_rpc_cmd: RPC command as configuration entry :param logger: The logger to use :return: A decoded, fully populated deep copy of cfg_rpc_cmd - - ---- - - - -## function `decode_and_call_rpc_command` - -```python -decode_and_call_rpc_command( - rpc_cmd: Dict, - logger: Logger = -) -``` - -Convenience function combining decode_rpc_command and plugs.call_ignore_errors - - ---- - - - -## function `bind_rpc_command` - -```python -bind_rpc_command( - cfg_rpc_cmd: Dict, - dereference=False, - logger: Logger = -) -``` - -Decode an RPC command configuration entry and bind it to a function - -:param dereference: Dereference even the call to plugs.call(...) - - #. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with all checks applied at bind time #. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with all checks applied at bind time. - - Setting deference to True, circumvents the dynamic nature of the plugins: the function to call must exist at bind time and cannot change. If False, the function to call must only exist at call time. This can be important during the initialization where package ordering and initialization means that not all classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls is circumvented. Use with care! - -:return: Callable function w/o parameters which directly runs the RPC command using plugs.call_ignore_errors - - ---- - - - -## function `rpc_call_to_str` - -```python -rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) → str -``` - -Return a readable string of an RPC call config - -:param cfg_rpc_call: RPC call configuration entry :param with_args: Return string shall include the arguments of the function - - ---- - - - -## function `indent` - -```python -indent(doc, spaces=4) -``` - - - - - - ---- - - - -## function `generate_cmd_alias_rst` - -```python -generate_cmd_alias_rst(stream) -``` - -Write a reference of all rpc command aliases in Restructured Text format - - ---- - - - -## function `generate_cmd_alias_reference` - -```python -generate_cmd_alias_reference(stream) -``` - -Write a reference of all rpc command aliases in text format - - ---- - - - -## function `get_git_state` - -```python -get_git_state() -``` - -Return git state information for the current branch - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/jukebox.version.md b/documentation/developers/docstring-lazydocs/jukebox.version.md deleted file mode 100644 index 6fc69503f..000000000 --- a/documentation/developers/docstring-lazydocs/jukebox.version.md +++ /dev/null @@ -1,49 +0,0 @@ - - - - -# module `jukebox.version` - - - - -**Global Variables** ---------------- -- **VERSION_MAJOR** -- **VERSION_MINOR** -- **VERSION_PATCH** -- **VERSION_EXTRA** - ---- - - - -## function `version` - -```python -version() -``` - -Return the Jukebox version as a string - - ---- - - - -## function `version_info` - -```python -version_info() -``` - -Return the Jukebox version as a tuple of three numbers - -If this is a development version, an identifier string will be appended after the third integer. - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/misc.inputminus.md b/documentation/developers/docstring-lazydocs/misc.inputminus.md deleted file mode 100644 index 1250ec8d7..000000000 --- a/documentation/developers/docstring-lazydocs/misc.inputminus.md +++ /dev/null @@ -1,69 +0,0 @@ - - - - -# module `misc.inputminus` -Zero 3rd-party dependency module for user prompting - -Yes, there are modules out there to do the same and they have more features. However, this is low-complexity and has zero dependencies - - ---- - - - -## function `input_int` - -```python -input_int( - prompt, - blank=None, - min=None, - max=None, - prompt_color=None, - prompt_hint=False -) → int -``` - -Request an integer input from user - -:param prompt: The prompt to display :param blank: Value to return when user just hits enter. Leave at None, if blank is invalid :param min: Minimum valid integer value (None disables this check) :param max: Maximum valid integer value (None disables this check) :param prompt_color: Color of the prompt. Color will be reset at end of prompt :param prompt_hint: Append a 'hint' with [min...max, default=xx] to end of prompt :return: integer value read from user input - - ---- - - - -## function `input_yesno` - -```python -input_yesno(prompt, blank=None, prompt_color=None, prompt_hint=False) → bool -``` - -Request a yes / no choice from user - -Accepts multiple input for true/false and is case insensitive - -:param prompt: The prompt to display :param blank: Value to return when user just hits enter. Leave at None, if blank is invalid :param prompt_color: Color of the prompt. Color will be reset at end of prompt :param prompt_hint: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized :return: boolean value read from user input - - ---- - - - -## function `msg_highlight` - -```python -msg_highlight(msg, color='\x1b[94m', deliminator_length=79) -``` - - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/misc.md b/documentation/developers/docstring-lazydocs/misc.md deleted file mode 100644 index 45acafd14..000000000 --- a/documentation/developers/docstring-lazydocs/misc.md +++ /dev/null @@ -1,58 +0,0 @@ - - - - -# module `misc` - - - - - ---- - - - -## function `recursive_chmod` - -```python -recursive_chmod(path, mode_files, mode_dirs) -``` - -Recursively change folder and file permissions - -mode_files/mode dirs can be given in octal notation e.g. 0o777 flags from the stats module. - -Reference: https://docs.python.org/3/library/os.html#os.chmod - - ---- - - - -## function `flatten` - -```python -flatten(iterable) -``` - -Flatten all levels of hierarchy in nested iterables - - ---- - - - -## function `getattr_hierarchical` - -```python -getattr_hierarchical(obj: Any, name: str) → Any -``` - -Like the builtin getattr, but descends though the hierarchy levels - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-lazydocs/misc.simplecolors.md b/documentation/developers/docstring-lazydocs/misc.simplecolors.md deleted file mode 100644 index 7a2352caa..000000000 --- a/documentation/developers/docstring-lazydocs/misc.simplecolors.md +++ /dev/null @@ -1,66 +0,0 @@ - - - - -# module `misc.simplecolors` -Zero 3rd-party dependency module to add colors to unix terminal output - -Yes, there are modules out there to do the same and they have more features. However, this is low-complexity and has zero dependencies - -**Global Variables** ---------------- -- **COLORS** - ---- - - - -## function `resolve` - -```python -resolve(color_name: str) -``` - -Resolve a color name into the respective color constant - -:param color_name: Name of the color :return: color constant - - ---- - - - -## function `print` - -```python -print( - color: Colors, - *values, - sep=' ', - end='\n', - file=<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>, - flush=False -) -``` - -Drop-in replacement for print with color choice and auto color reset for convenience - -Use just as a regular print function, but with first parameter as color - - ---- - - - -## class `Colors` -Container class for all the colors as constants - - - - - - - ---- - -_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/documentation/developers/docstring-pydoc-markdown/docstring.md b/documentation/developers/docstring-pydoc-markdown/docstring.md deleted file mode 100644 index b2184d060..000000000 --- a/documentation/developers/docstring-pydoc-markdown/docstring.md +++ /dev/null @@ -1,5648 +0,0 @@ -# None - -## Table of Contents - -* [run\_jukebox](#run_jukebox) -* [\_\_init\_\_](#__init__) -* [run\_register\_rfid\_reader](#run_register_rfid_reader) -* [run\_rpc\_tool](#run_rpc_tool) - * [get\_common\_beginning](#run_rpc_tool.get_common_beginning) -* [run\_configure\_audio](#run_configure_audio) -* [run\_publicity\_sniffer](#run_publicity_sniffer) -* [misc](#misc) - * [recursive\_chmod](#misc.recursive_chmod) - * [flatten](#misc.flatten) - * [getattr\_hierarchical](#misc.getattr_hierarchical) -* [misc.inputminus](#misc.inputminus) - * [input\_int](#misc.inputminus.input_int) - * [input\_yesno](#misc.inputminus.input_yesno) -* [misc.loggingext](#misc.loggingext) - * [ColorFilter](#misc.loggingext.ColorFilter) - * [\_\_init\_\_](#misc.loggingext.ColorFilter.__init__) - * [PubStream](#misc.loggingext.PubStream) - * [PubStreamHandler](#misc.loggingext.PubStreamHandler) -* [misc.simplecolors](#misc.simplecolors) - * [Colors](#misc.simplecolors.Colors) - * [resolve](#misc.simplecolors.resolve) - * [print](#misc.simplecolors.print) -* [components](#components) -* [components.playermpd.playcontentcallback](#components.playermpd.playcontentcallback) - * [PlayContentCallbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks) - * [register](#components.playermpd.playcontentcallback.PlayContentCallbacks.register) - * [run\_callbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks.run_callbacks) -* [components.playermpd](#components.playermpd) - * [PlayerMPD](#components.playermpd.PlayerMPD) - * [mpd\_retry\_with\_mutex](#components.playermpd.PlayerMPD.mpd_retry_with_mutex) - * [pause](#components.playermpd.PlayerMPD.pause) - * [next](#components.playermpd.PlayerMPD.next) - * [rewind](#components.playermpd.PlayerMPD.rewind) - * [replay](#components.playermpd.PlayerMPD.replay) - * [toggle](#components.playermpd.PlayerMPD.toggle) - * [replay\_if\_stopped](#components.playermpd.PlayerMPD.replay_if_stopped) - * [play\_card](#components.playermpd.PlayerMPD.play_card) - * [get\_folder\_content](#components.playermpd.PlayerMPD.get_folder_content) - * [play\_folder](#components.playermpd.PlayerMPD.play_folder) - * [play\_album](#components.playermpd.PlayerMPD.play_album) - * [get\_volume](#components.playermpd.PlayerMPD.get_volume) - * [set\_volume](#components.playermpd.PlayerMPD.set_volume) - * [play\_card\_callbacks](#components.playermpd.play_card_callbacks) -* [components.rpc\_command\_alias](#components.rpc_command_alias) -* [components.synchronisation.rfidcards](#components.synchronisation.rfidcards) - * [SyncRfidcards](#components.synchronisation.rfidcards.SyncRfidcards) - * [sync\_change\_on\_rfid\_scan](#components.synchronisation.rfidcards.SyncRfidcards.sync_change_on_rfid_scan) - * [sync\_all](#components.synchronisation.rfidcards.SyncRfidcards.sync_all) - * [sync\_card\_database](#components.synchronisation.rfidcards.SyncRfidcards.sync_card_database) - * [sync\_folder](#components.synchronisation.rfidcards.SyncRfidcards.sync_folder) -* [components.synchronisation](#components.synchronisation) -* [components.synchronisation.syncutils](#components.synchronisation.syncutils) -* [components.volume](#components.volume) - * [PulseMonitor](#components.volume.PulseMonitor) - * [SoundCardConnectCallbacks](#components.volume.PulseMonitor.SoundCardConnectCallbacks) - * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) - * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) - * [stop](#components.volume.PulseMonitor.stop) - * [run](#components.volume.PulseMonitor.run) - * [PulseVolumeControl](#components.volume.PulseVolumeControl) - * [OutputChangeCallbackHandler](#components.volume.PulseVolumeControl.OutputChangeCallbackHandler) - * [OutputVolumeCallbackHandler](#components.volume.PulseVolumeControl.OutputVolumeCallbackHandler) - * [toggle\_output](#components.volume.PulseVolumeControl.toggle_output) - * [get\_outputs](#components.volume.PulseVolumeControl.get_outputs) - * [publish\_volume](#components.volume.PulseVolumeControl.publish_volume) - * [publish\_outputs](#components.volume.PulseVolumeControl.publish_outputs) - * [set\_volume](#components.volume.PulseVolumeControl.set_volume) - * [get\_volume](#components.volume.PulseVolumeControl.get_volume) - * [change\_volume](#components.volume.PulseVolumeControl.change_volume) - * [get\_mute](#components.volume.PulseVolumeControl.get_mute) - * [mute](#components.volume.PulseVolumeControl.mute) - * [set\_output](#components.volume.PulseVolumeControl.set_output) - * [set\_soft\_max\_volume](#components.volume.PulseVolumeControl.set_soft_max_volume) - * [get\_soft\_max\_volume](#components.volume.PulseVolumeControl.get_soft_max_volume) - * [card\_list](#components.volume.PulseVolumeControl.card_list) -* [components.rfid](#components.rfid) -* [components.rfid.reader](#components.rfid.reader) - * [RfidCardDetectCallbacks](#components.rfid.reader.RfidCardDetectCallbacks) - * [register](#components.rfid.reader.RfidCardDetectCallbacks.register) - * [run\_callbacks](#components.rfid.reader.RfidCardDetectCallbacks.run_callbacks) - * [rfid\_card\_detect\_callbacks](#components.rfid.reader.rfid_card_detect_callbacks) - * [CardRemovalTimerClass](#components.rfid.reader.CardRemovalTimerClass) - * [\_\_init\_\_](#components.rfid.reader.CardRemovalTimerClass.__init__) -* [components.rfid.configure](#components.rfid.configure) - * [reader\_install\_dependencies](#components.rfid.configure.reader_install_dependencies) - * [reader\_load\_module](#components.rfid.configure.reader_load_module) - * [query\_user\_for\_reader](#components.rfid.configure.query_user_for_reader) - * [write\_config](#components.rfid.configure.write_config) -* [components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui](#components.rfid.hardware.fake_reader_gui.fake_reader_gui) -* [components.rfid.hardware.fake\_reader\_gui.description](#components.rfid.hardware.fake_reader_gui.description) -* [components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon) - * [create\_inputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_inputs) - * [set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.set_state) - * [que\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_state) - * [fix\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.fix_state) - * [pbox\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.pbox_set_state) - * [que\_set\_pbox](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_pbox) - * [create\_outputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_outputs) -* [components.rfid.hardware.generic\_usb.description](#components.rfid.hardware.generic_usb.description) -* [components.rfid.hardware.generic\_usb.generic\_usb](#components.rfid.hardware.generic_usb.generic_usb) -* [components.rfid.hardware.rc522\_spi.description](#components.rfid.hardware.rc522_spi.description) -* [components.rfid.hardware.rc522\_spi.rc522\_spi](#components.rfid.hardware.rc522_spi.rc522_spi) -* [components.rfid.hardware.pn532\_i2c\_py532.description](#components.rfid.hardware.pn532_i2c_py532.description) -* [components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532](#components.rfid.hardware.pn532_i2c_py532.pn532_i2c_py532) -* [components.rfid.hardware.rdm6300\_serial.rdm6300\_serial](#components.rfid.hardware.rdm6300_serial.rdm6300_serial) - * [decode](#components.rfid.hardware.rdm6300_serial.rdm6300_serial.decode) -* [components.rfid.hardware.rdm6300\_serial.description](#components.rfid.hardware.rdm6300_serial.description) -* [components.rfid.hardware.template\_new\_reader.description](#components.rfid.hardware.template_new_reader.description) -* [components.rfid.hardware.template\_new\_reader.template\_new\_reader](#components.rfid.hardware.template_new_reader.template_new_reader) - * [query\_customization](#components.rfid.hardware.template_new_reader.template_new_reader.query_customization) - * [ReaderClass](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass) - * [\_\_init\_\_](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.__init__) - * [cleanup](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.cleanup) - * [stop](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.stop) - * [read\_card](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.read_card) -* [components.rfid.readerbase](#components.rfid.readerbase) - * [ReaderBaseClass](#components.rfid.readerbase.ReaderBaseClass) -* [components.rfid.cards](#components.rfid.cards) - * [list\_cards](#components.rfid.cards.list_cards) - * [delete\_card](#components.rfid.cards.delete_card) - * [register\_card](#components.rfid.cards.register_card) - * [register\_card\_custom](#components.rfid.cards.register_card_custom) - * [save\_card\_database](#components.rfid.cards.save_card_database) -* [components.rfid.cardutils](#components.rfid.cardutils) - * [decode\_card\_command](#components.rfid.cardutils.decode_card_command) - * [card\_command\_to\_str](#components.rfid.cardutils.card_command_to_str) - * [card\_to\_str](#components.rfid.cardutils.card_to_str) -* [components.publishing](#components.publishing) - * [republish](#components.publishing.republish) -* [components.player](#components.player) - * [MusicLibPath](#components.player.MusicLibPath) - * [get\_music\_library\_path](#components.player.get_music_library_path) -* [components.jingle](#components.jingle) - * [JingleFactory](#components.jingle.JingleFactory) - * [list](#components.jingle.JingleFactory.list) - * [play](#components.jingle.play) - * [play\_startup](#components.jingle.play_startup) - * [play\_shutdown](#components.jingle.play_shutdown) -* [components.jingle.alsawave](#components.jingle.alsawave) - * [AlsaWave](#components.jingle.alsawave.AlsaWave) - * [play](#components.jingle.alsawave.AlsaWave.play) - * [AlsaWaveBuilder](#components.jingle.alsawave.AlsaWaveBuilder) - * [\_\_init\_\_](#components.jingle.alsawave.AlsaWaveBuilder.__init__) -* [components.jingle.jinglemp3](#components.jingle.jinglemp3) - * [JingleMp3Play](#components.jingle.jinglemp3.JingleMp3Play) - * [play](#components.jingle.jinglemp3.JingleMp3Play.play) - * [JingleMp3PlayBuilder](#components.jingle.jinglemp3.JingleMp3PlayBuilder) - * [\_\_init\_\_](#components.jingle.jinglemp3.JingleMp3PlayBuilder.__init__) -* [components.hostif.linux](#components.hostif.linux) - * [shutdown](#components.hostif.linux.shutdown) - * [reboot](#components.hostif.linux.reboot) - * [jukebox\_is\_service](#components.hostif.linux.jukebox_is_service) - * [is\_any\_jukebox\_service\_active](#components.hostif.linux.is_any_jukebox_service_active) - * [restart\_service](#components.hostif.linux.restart_service) - * [get\_disk\_usage](#components.hostif.linux.get_disk_usage) - * [get\_cpu\_temperature](#components.hostif.linux.get_cpu_temperature) - * [get\_ip\_address](#components.hostif.linux.get_ip_address) - * [wlan\_disable\_power\_down](#components.hostif.linux.wlan_disable_power_down) - * [get\_autohotspot\_status](#components.hostif.linux.get_autohotspot_status) - * [stop\_autohotspot](#components.hostif.linux.stop_autohotspot) - * [start\_autohotspot](#components.hostif.linux.start_autohotspot) -* [components.misc](#components.misc) - * [rpc\_cmd\_help](#components.misc.rpc_cmd_help) - * [get\_all\_loaded\_packages](#components.misc.get_all_loaded_packages) - * [get\_all\_failed\_packages](#components.misc.get_all_failed_packages) - * [get\_start\_time](#components.misc.get_start_time) - * [get\_log](#components.misc.get_log) - * [get\_log\_debug](#components.misc.get_log_debug) - * [get\_log\_error](#components.misc.get_log_error) - * [get\_git\_state](#components.misc.get_git_state) - * [empty\_rpc\_call](#components.misc.empty_rpc_call) -* [components.controls](#components.controls) -* [components.controls.bluetooth\_audio\_buttons](#components.controls.bluetooth_audio_buttons) -* [components.controls.common.evdev\_listener](#components.controls.common.evdev_listener) - * [find\_device](#components.controls.common.evdev_listener.find_device) - * [EvDevKeyListener](#components.controls.common.evdev_listener.EvDevKeyListener) - * [\_\_init\_\_](#components.controls.common.evdev_listener.EvDevKeyListener.__init__) - * [run](#components.controls.common.evdev_listener.EvDevKeyListener.run) - * [start](#components.controls.common.evdev_listener.EvDevKeyListener.start) -* [components.music\_cover\_art](#components.music_cover_art) - * [MusicCoverArt](#components.music_cover_art.MusicCoverArt) - * [get\_by\_filename\_as\_base64](#components.music_cover_art.MusicCoverArt.get_by_filename_as_base64) -* [components.battery\_monitor](#components.battery_monitor) -* [components.battery\_monitor.BatteryMonitorBase](#components.battery_monitor.BatteryMonitorBase) - * [pt1\_frac](#components.battery_monitor.BatteryMonitorBase.pt1_frac) - * [BattmonBase](#components.battery_monitor.BatteryMonitorBase.BattmonBase) -* [components.battery\_monitor.batt\_mon\_simulator](#components.battery_monitor.batt_mon_simulator) - * [battmon\_simulator](#components.battery_monitor.batt_mon_simulator.battmon_simulator) -* [components.battery\_monitor.batt\_mon\_i2c\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015) - * [battmon\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015.battmon_ads1015) -* [components.gpio.gpioz.plugin](#components.gpio.gpioz.plugin) - * [output\_devices](#components.gpio.gpioz.plugin.output_devices) - * [input\_devices](#components.gpio.gpioz.plugin.input_devices) - * [factory](#components.gpio.gpioz.plugin.factory) - * [IS\_ENABLED](#components.gpio.gpioz.plugin.IS_ENABLED) - * [IS\_MOCKED](#components.gpio.gpioz.plugin.IS_MOCKED) - * [CONFIG\_FILE](#components.gpio.gpioz.plugin.CONFIG_FILE) - * [ServiceIsRunningCallbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks) - * [register](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.register) - * [run\_callbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.run_callbacks) - * [service\_is\_running\_callbacks](#components.gpio.gpioz.plugin.service_is_running_callbacks) - * [build\_output\_device](#components.gpio.gpioz.plugin.build_output_device) - * [build\_input\_device](#components.gpio.gpioz.plugin.build_input_device) - * [get\_output](#components.gpio.gpioz.plugin.get_output) - * [on](#components.gpio.gpioz.plugin.on) - * [off](#components.gpio.gpioz.plugin.off) - * [set\_value](#components.gpio.gpioz.plugin.set_value) - * [flash](#components.gpio.gpioz.plugin.flash) -* [components.gpio.gpioz.plugin.connectivity](#components.gpio.gpioz.plugin.connectivity) - * [BUZZ\_TONE](#components.gpio.gpioz.plugin.connectivity.BUZZ_TONE) - * [register\_rfid\_callback](#components.gpio.gpioz.plugin.connectivity.register_rfid_callback) - * [register\_status\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_led_callback) - * [register\_status\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_buzzer_callback) - * [register\_status\_tonalbuzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_tonalbuzzer_callback) - * [register\_audio\_sink\_change\_callback](#components.gpio.gpioz.plugin.connectivity.register_audio_sink_change_callback) - * [register\_volume\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_led_callback) - * [register\_volume\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_buzzer_callback) - * [register\_volume\_rgbled\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_rgbled_callback) -* [components.gpio.gpioz.core.converter](#components.gpio.gpioz.core.converter) - * [ColorProperty](#components.gpio.gpioz.core.converter.ColorProperty) - * [VolumeToRGB](#components.gpio.gpioz.core.converter.VolumeToRGB) - * [\_\_call\_\_](#components.gpio.gpioz.core.converter.VolumeToRGB.__call__) - * [luminize](#components.gpio.gpioz.core.converter.VolumeToRGB.luminize) -* [components.gpio.gpioz.core.mock](#components.gpio.gpioz.core.mock) - * [patch\_mock\_outputs\_with\_callback](#components.gpio.gpioz.core.mock.patch_mock_outputs_with_callback) -* [components.gpio.gpioz.core.input\_devices](#components.gpio.gpioz.core.input_devices) - * [NameMixin](#components.gpio.gpioz.core.input_devices.NameMixin) - * [set\_rpc\_actions](#components.gpio.gpioz.core.input_devices.NameMixin.set_rpc_actions) - * [EventProperty](#components.gpio.gpioz.core.input_devices.EventProperty) - * [ButtonBase](#components.gpio.gpioz.core.input_devices.ButtonBase) - * [value](#components.gpio.gpioz.core.input_devices.ButtonBase.value) - * [pin](#components.gpio.gpioz.core.input_devices.ButtonBase.pin) - * [pull\_up](#components.gpio.gpioz.core.input_devices.ButtonBase.pull_up) - * [close](#components.gpio.gpioz.core.input_devices.ButtonBase.close) - * [Button](#components.gpio.gpioz.core.input_devices.Button) - * [on\_press](#components.gpio.gpioz.core.input_devices.Button.on_press) - * [LongPressButton](#components.gpio.gpioz.core.input_devices.LongPressButton) - * [on\_press](#components.gpio.gpioz.core.input_devices.LongPressButton.on_press) - * [ShortLongPressButton](#components.gpio.gpioz.core.input_devices.ShortLongPressButton) - * [RotaryEncoder](#components.gpio.gpioz.core.input_devices.RotaryEncoder) - * [pin\_a](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_a) - * [pin\_b](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_b) - * [on\_rotate\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_clockwise) - * [on\_rotate\_counter\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_counter_clockwise) - * [close](#components.gpio.gpioz.core.input_devices.RotaryEncoder.close) - * [TwinButton](#components.gpio.gpioz.core.input_devices.TwinButton) - * [StateVar](#components.gpio.gpioz.core.input_devices.TwinButton.StateVar) - * [close](#components.gpio.gpioz.core.input_devices.TwinButton.close) - * [value](#components.gpio.gpioz.core.input_devices.TwinButton.value) - * [is\_active](#components.gpio.gpioz.core.input_devices.TwinButton.is_active) -* [components.gpio.gpioz.core.output\_devices](#components.gpio.gpioz.core.output_devices) - * [LED](#components.gpio.gpioz.core.output_devices.LED) - * [flash](#components.gpio.gpioz.core.output_devices.LED.flash) - * [Buzzer](#components.gpio.gpioz.core.output_devices.Buzzer) - * [flash](#components.gpio.gpioz.core.output_devices.Buzzer.flash) - * [PWMLED](#components.gpio.gpioz.core.output_devices.PWMLED) - * [flash](#components.gpio.gpioz.core.output_devices.PWMLED.flash) - * [RGBLED](#components.gpio.gpioz.core.output_devices.RGBLED) - * [flash](#components.gpio.gpioz.core.output_devices.RGBLED.flash) - * [TonalBuzzer](#components.gpio.gpioz.core.output_devices.TonalBuzzer) - * [flash](#components.gpio.gpioz.core.output_devices.TonalBuzzer.flash) - * [melody](#components.gpio.gpioz.core.output_devices.TonalBuzzer.melody) -* [components.timers](#components.timers) -* [jukebox](#jukebox) -* [jukebox.callingback](#jukebox.callingback) - * [CallbackHandler](#jukebox.callingback.CallbackHandler) - * [register](#jukebox.callingback.CallbackHandler.register) - * [run\_callbacks](#jukebox.callingback.CallbackHandler.run_callbacks) - * [has\_callbacks](#jukebox.callingback.CallbackHandler.has_callbacks) -* [jukebox.version](#jukebox.version) - * [version](#jukebox.version.version) - * [version\_info](#jukebox.version.version_info) -* [jukebox.cfghandler](#jukebox.cfghandler) - * [ConfigHandler](#jukebox.cfghandler.ConfigHandler) - * [loaded\_from](#jukebox.cfghandler.ConfigHandler.loaded_from) - * [get](#jukebox.cfghandler.ConfigHandler.get) - * [setdefault](#jukebox.cfghandler.ConfigHandler.setdefault) - * [getn](#jukebox.cfghandler.ConfigHandler.getn) - * [setn](#jukebox.cfghandler.ConfigHandler.setn) - * [setndefault](#jukebox.cfghandler.ConfigHandler.setndefault) - * [config\_dict](#jukebox.cfghandler.ConfigHandler.config_dict) - * [is\_modified](#jukebox.cfghandler.ConfigHandler.is_modified) - * [clear\_modified](#jukebox.cfghandler.ConfigHandler.clear_modified) - * [save](#jukebox.cfghandler.ConfigHandler.save) - * [load](#jukebox.cfghandler.ConfigHandler.load) - * [get\_handler](#jukebox.cfghandler.get_handler) - * [load\_yaml](#jukebox.cfghandler.load_yaml) - * [write\_yaml](#jukebox.cfghandler.write_yaml) -* [jukebox.playlistgenerator](#jukebox.playlistgenerator) - * [TYPE\_DECODE](#jukebox.playlistgenerator.TYPE_DECODE) - * [PlaylistCollector](#jukebox.playlistgenerator.PlaylistCollector) - * [\_\_init\_\_](#jukebox.playlistgenerator.PlaylistCollector.__init__) - * [set\_exclusion\_endings](#jukebox.playlistgenerator.PlaylistCollector.set_exclusion_endings) - * [get\_directory\_content](#jukebox.playlistgenerator.PlaylistCollector.get_directory_content) - * [parse](#jukebox.playlistgenerator.PlaylistCollector.parse) -* [jukebox.NvManager](#jukebox.NvManager) -* [jukebox.publishing](#jukebox.publishing) - * [get\_publisher](#jukebox.publishing.get_publisher) -* [jukebox.publishing.subscriber](#jukebox.publishing.subscriber) -* [jukebox.publishing.server](#jukebox.publishing.server) - * [PublishServer](#jukebox.publishing.server.PublishServer) - * [run](#jukebox.publishing.server.PublishServer.run) - * [handle\_message](#jukebox.publishing.server.PublishServer.handle_message) - * [handle\_subscription](#jukebox.publishing.server.PublishServer.handle_subscription) - * [Publisher](#jukebox.publishing.server.Publisher) - * [\_\_init\_\_](#jukebox.publishing.server.Publisher.__init__) - * [send](#jukebox.publishing.server.Publisher.send) - * [revoke](#jukebox.publishing.server.Publisher.revoke) - * [resend](#jukebox.publishing.server.Publisher.resend) - * [close\_server](#jukebox.publishing.server.Publisher.close_server) -* [jukebox.daemon](#jukebox.daemon) - * [log\_active\_threads](#jukebox.daemon.log_active_threads) - * [JukeBox](#jukebox.daemon.JukeBox) - * [signal\_handler](#jukebox.daemon.JukeBox.signal_handler) -* [jukebox.plugs](#jukebox.plugs) - * [PluginPackageClass](#jukebox.plugs.PluginPackageClass) - * [register](#jukebox.plugs.register) - * [register](#jukebox.plugs.register) - * [register](#jukebox.plugs.register) - * [register](#jukebox.plugs.register) - * [register](#jukebox.plugs.register) - * [register](#jukebox.plugs.register) - * [tag](#jukebox.plugs.tag) - * [initialize](#jukebox.plugs.initialize) - * [finalize](#jukebox.plugs.finalize) - * [atexit](#jukebox.plugs.atexit) - * [load](#jukebox.plugs.load) - * [load\_all\_named](#jukebox.plugs.load_all_named) - * [load\_all\_unnamed](#jukebox.plugs.load_all_unnamed) - * [load\_all\_finalize](#jukebox.plugs.load_all_finalize) - * [close\_down](#jukebox.plugs.close_down) - * [call](#jukebox.plugs.call) - * [call\_ignore\_errors](#jukebox.plugs.call_ignore_errors) - * [exists](#jukebox.plugs.exists) - * [get](#jukebox.plugs.get) - * [loaded\_as](#jukebox.plugs.loaded_as) - * [delete](#jukebox.plugs.delete) - * [dump\_plugins](#jukebox.plugs.dump_plugins) - * [summarize](#jukebox.plugs.summarize) - * [generate\_help\_rst](#jukebox.plugs.generate_help_rst) - * [get\_all\_loaded\_packages](#jukebox.plugs.get_all_loaded_packages) - * [get\_all\_failed\_packages](#jukebox.plugs.get_all_failed_packages) -* [jukebox.speaking\_text](#jukebox.speaking_text) -* [jukebox.multitimer](#jukebox.multitimer) - * [MultiTimer](#jukebox.multitimer.MultiTimer) - * [cancel](#jukebox.multitimer.MultiTimer.cancel) - * [GenericTimerClass](#jukebox.multitimer.GenericTimerClass) - * [\_\_init\_\_](#jukebox.multitimer.GenericTimerClass.__init__) - * [start](#jukebox.multitimer.GenericTimerClass.start) - * [cancel](#jukebox.multitimer.GenericTimerClass.cancel) - * [toggle](#jukebox.multitimer.GenericTimerClass.toggle) - * [trigger](#jukebox.multitimer.GenericTimerClass.trigger) - * [is\_alive](#jukebox.multitimer.GenericTimerClass.is_alive) - * [get\_timeout](#jukebox.multitimer.GenericTimerClass.get_timeout) - * [set\_timeout](#jukebox.multitimer.GenericTimerClass.set_timeout) - * [publish](#jukebox.multitimer.GenericTimerClass.publish) - * [get\_state](#jukebox.multitimer.GenericTimerClass.get_state) - * [GenericEndlessTimerClass](#jukebox.multitimer.GenericEndlessTimerClass) - * [GenericMultiTimerClass](#jukebox.multitimer.GenericMultiTimerClass) - * [\_\_init\_\_](#jukebox.multitimer.GenericMultiTimerClass.__init__) - * [start](#jukebox.multitimer.GenericMultiTimerClass.start) -* [jukebox.utils](#jukebox.utils) - * [decode\_rpc\_call](#jukebox.utils.decode_rpc_call) - * [decode\_rpc\_command](#jukebox.utils.decode_rpc_command) - * [decode\_and\_call\_rpc\_command](#jukebox.utils.decode_and_call_rpc_command) - * [bind\_rpc\_command](#jukebox.utils.bind_rpc_command) - * [rpc\_call\_to\_str](#jukebox.utils.rpc_call_to_str) - * [generate\_cmd\_alias\_rst](#jukebox.utils.generate_cmd_alias_rst) - * [generate\_cmd\_alias\_reference](#jukebox.utils.generate_cmd_alias_reference) - * [get\_git\_state](#jukebox.utils.get_git_state) -* [jukebox.rpc](#jukebox.rpc) -* [jukebox.rpc.client](#jukebox.rpc.client) -* [jukebox.rpc.server](#jukebox.rpc.server) - * [RpcServer](#jukebox.rpc.server.RpcServer) - * [\_\_init\_\_](#jukebox.rpc.server.RpcServer.__init__) - * [run](#jukebox.rpc.server.RpcServer.run) - - - -# run\_jukebox - -This is the main app and starts the Jukebox Core. - -Usually this runs as a service, which is started automatically after boot-up. At times, it may be necessary to restart -the service. -For example after a configuration change. Not all configuration changes can be applied on-the-fly. -See :ref:`userguide/configuration:Jukebox Configuration`. - -For debugging, it is usually desirable to run the Jukebox directly from the console rather than -as service. This gives direct logging info in the console and allows changing command line parameters. -See :ref:`userguide/troubleshooting:Troubleshooting`. - - - -# \_\_init\_\_ - - - -# run\_register\_rfid\_reader - -Setup tool to configure the RFID Readers. - -Run this once to register and configure the RFID readers with the Jukebox. Can be re-run at any time to change -the settings. For more information see :ref:`rfid/rfid:RFID Readers`. - -.. note:: This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). - Any manual modifications to the settings will have to be re-applied - - - -# run\_rpc\_tool - -Command Line Interface to the Jukebox RPC Server - -A command line tool for sending RPC commands to the running jukebox app. -This uses the same interface as the WebUI. Can be used for additional control -or for debugging. - -The tool features auto-completion and command history. - -The list of available commands is fetched from the running Jukebox service. - -.. todo: - - kwargs support - - - -#### get\_common\_beginning - -```python -def get_common_beginning(strings) -``` - -Return the strings that are common to the beginning of each string in the strings list. - - - -# run\_configure\_audio - -Setup tool to register the PulseAudio sinks as primary and secondary audio outputs. - -Will also setup equalizer and mono down mixer in the pulseaudio config file. - -Run this once after installation. Can be re-run at any time to change the settings. -For more information see :ref:`userguide/audio:Audio Configuration`. - - - -# run\_publicity\_sniffer - -A command line tool that monitors all messages being sent out from the -Jukebox via the publishing interface. Received messages are printed in the console. -Mainly used for debugging. - - - -# misc - - - -#### recursive\_chmod - -```python -def recursive_chmod(path, mode_files, mode_dirs) -``` - -Recursively change folder and file permissions - -mode_files/mode dirs can be given in octal notation e.g. 0o777 -flags from the stats module. - -Reference: https://docs.python.org/3/library/os.html#os.chmod - - - -#### flatten - -```python -def flatten(iterable) -``` - -Flatten all levels of hierarchy in nested iterables - - - -#### getattr\_hierarchical - -```python -def getattr_hierarchical(obj: Any, name: str) -> Any -``` - -Like the builtin getattr, but descends though the hierarchy levels - - - -# misc.inputminus - -Zero 3rd-party dependency module for user prompting - -Yes, there are modules out there to do the same and they have more features. -However, this is low-complexity and has zero dependencies - - - -#### input\_int - -```python -def input_int(prompt, - blank=None, - min=None, - max=None, - prompt_color=None, - prompt_hint=False) -> int -``` - -Request an integer input from user - -**Arguments**: - -- `prompt`: The prompt to display -- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid -- `min`: Minimum valid integer value (None disables this check) -- `max`: Maximum valid integer value (None disables this check) -- `prompt_color`: Color of the prompt. Color will be reset at end of prompt -- `prompt_hint`: Append a 'hint' with [min...max, default=xx] to end of prompt - -**Returns**: - -integer value read from user input - - - -#### input\_yesno - -```python -def input_yesno(prompt, - blank=None, - prompt_color=None, - prompt_hint=False) -> bool -``` - -Request a yes / no choice from user - -Accepts multiple input for true/false and is case insensitive - -**Arguments**: - -- `prompt`: The prompt to display -- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid -- `prompt_color`: Color of the prompt. Color will be reset at end of prompt -- `prompt_hint`: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized - -**Returns**: - -boolean value read from user input - - - -# misc.loggingext - -############## -Logger -############## -We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. - -The top-level logger is called 'jb' (to make it short). In any module you may simple create a child-logger at any hierarchy -level below 'jb'. It will inherit settings from it's parent logger unless otherwise configured in the yaml file. -Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be -created on the spot. - -:Example: How to get logger and log away at your heart's content: ->>> import logging ->>> logger = logging.getLogger('jb.awesome_module') ->>> logger.info('Started general awesomeness aura') - -Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: -`` -loggers: -jb: -level: WARNING -handlers: [console, debug_file_handler, error_file_handler] -propagate: no -jb.awesome_module: -level: DEBUG -`` - -.. note:: -The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense) -There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output - - - -## ColorFilter Objects - -```python -class ColorFilter(logging.Filter) -``` - -This filter adds colors to the logger - -It adds all colors from simplecolors by using the color name as new keyword, -i.e. use %(colorname)c or {colorname} in the formatter string - -It also adds the keyword {levelnameColored} which is an auto-colored drop-in replacement -for the levelname depending on severity. - -Don't forget to {reset} the color settings at the end of the string. - - - -#### \_\_init\_\_ - -```python -def __init__(enable=True, color_levelname=True) -``` - -**Arguments**: - -- `enable`: Enable the coloring -- `color_levelname`: Enable auto-coloring when using the levelname keyword - - - -## PubStream Objects - -```python -class PubStream() -``` - -" -Stream handler wrapper around the publisher for logging.StreamHandler - -Allows logging to send all log information (based on logging configuration) -to the Publisher. - -ATTENTION: This can lead to recursions! - -Recursions come up when -(a) Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, - which causes a send, ..... -(b) Publisher initialization emits logs, which need a Publisher instance to send logs - -IMPORTANT: To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the -functions in the send-function stack! - - - -## PubStreamHandler Objects - -```python -class PubStreamHandler(logging.StreamHandler) -``` - -Wrapper for logging.StreamHandler with stream = PubStream - -This serves one purpose: In logger.yaml custom handlers -can be configured (which are automatically instantiated). -Using this Handler, we can output to PubStream whithout -support code to instantiate PubStream keeping this file generic - - - -# misc.simplecolors - -Zero 3rd-party dependency module to add colors to unix terminal output - -Yes, there are modules out there to do the same and they have more features. -However, this is low-complexity and has zero dependencies - - - -## Colors Objects - -```python -class Colors() -``` - -Container class for all the colors as constants - - - -#### resolve - -```python -def resolve(color_name: str) -``` - -Resolve a color name into the respective color constant - -**Arguments**: - -- `color_name`: Name of the color - -**Returns**: - -color constant - - - -#### print - -```python -def print(color: Colors, - *values, - sep=' ', - end='\n', - file=sys.stdout, - flush=False) -``` - -Drop-in replacement for print with color choice and auto color reset for convenience - -Use just as a regular print function, but with first parameter as color - - - -# components - - - -# components.playermpd.playcontentcallback - - - -## PlayContentCallbacks Objects - -```python -class PlayContentCallbacks(Generic[STATE], CallbackHandler) -``` - -Callbacks are executed in various play functions - - - -#### register - -```python -def register(func: Callable[[str, STATE], None]) -``` - -Add a new callback function :attr:`func`. - -Callback signature is - -.. py:function:: func(folder: str, state: STATE) - :noindex: - - :param folder: relativ path to folder to play - :param state: indicator of the state inside the calling - - - - -#### run\_callbacks - -```python -def run_callbacks(folder: str, state: STATE) -``` - -:meta private: - - - -# components.playermpd - -Package for interfacing with the MPD Music Player Daemon - -Status information in three topics -1) Player Status: published only on change -This is a subset of the MPD status (and not the full MPD status) ?? -- folder -- song -- volume (volume is published only via player status, and not separatly to avoid too many Threads) -- ... -2) Elapsed time: published every 250 ms, unless constant -- elapsed -3) Folder Config: published only on change -This belongs to the folder being played -Publish: -- random, resume, single, loop -On save store this information: -Contains the information for resume functionality of each folder -- random, resume, single, loop -- if resume: -- current song, elapsed -- what is PLAYSTATUS for? -When to save -- on stop -Angstsave: -- on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) -- on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) -Load checks: -- if resume, but no song, elapsed -> log error and start from the beginning - -Status storing: -- Folder config for each folder (see above) -- Information to restart last folder playback, which is: -- last_folder -> folder_on_close -- song, elapsed -- random, resume, single, loop -- if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! -on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card - -Internal status -- last played folder: Needed to detect second swipe - - -Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, -'audio_folder_status': -{'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, -'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} - -**References**: - - https://github.com/Mic92/python-mpd2 - https://python-mpd2.readthedocs.io/en/latest/topics/commands.html - https://mpd.readthedocs.io/en/latest/protocol.html - - sudo -u mpd speaker-test -t wav -c 2 - - - -## PlayerMPD Objects - -```python -class PlayerMPD() -``` - -Interface to MPD Music Player Daemon - - - -#### mpd\_retry\_with\_mutex - -```python -def mpd_retry_with_mutex(mpd_cmd, *args) -``` - -This method adds thread saftey for acceses to mpd via a mutex lock, -it shall be used for each access to mpd to ensure thread safety -In case of a communication error the connection will be reestablished and the pending command will be repeated 2 times - -I think this should be refactored to a decorator - - - -#### pause - -```python -@plugs.tag -def pause(state: int = 1) -``` - -Enforce pause to state (1: pause, 0: resume) - -This is what you want as card removal action: pause the playback, so it can be resumed when card is placed -on the reader again. What happens on re-placement depends on configured second swipe option - - - -#### next - -```python -@plugs.tag -def next() -``` - -Play next track in current playlist - - - -#### rewind - -```python -@plugs.tag -def rewind() -``` - -Re-start current playlist from first track - -Note: Will not re-read folder config, but leave settings untouched - - - -#### replay - -```python -@plugs.tag -def replay() -``` - -Re-start playing the last-played folder - -Will reset settings to folder config - - - -#### toggle - -```python -@plugs.tag -def toggle() -``` - -Toggle pause state, i.e. do a pause / resume depending on current state - - - -#### replay\_if\_stopped - -```python -@plugs.tag -def replay_if_stopped() -``` - -Re-start playing the last-played folder unless playlist is still playing - -.. note:: To me this seems much like the behaviour of play, - but we keep it as it is specifically implemented in box 2.X - - - -#### play\_card - -```python -@plugs.tag -def play_card(folder: str, recursive: bool = False) -``` - -Main entry point for trigger music playing from RFID reader. Decodes second swipe options before playing folder content - -Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action -accordingly. - -**Arguments**: - -- `folder`: Folder path relative to music library path -- `recursive`: Add folder recursively - - - -#### get\_folder\_content - -```python -@plugs.tag -def get_folder_content(folder: str) -``` - -Get the folder content as content list with meta-information. Depth is always 1. - -Call repeatedly to descend in hierarchy - -**Arguments**: - -- `folder`: Folder path relative to music library path - - - -#### play\_folder - -```python -@plugs.tag -def play_folder(folder: str, recursive: bool = False) -> None -``` - -Playback a music folder. - -Folder content is added to the playlist as described by :mod:`jukebox.playlistgenerator`. -The playlist is cleared first. - -**Arguments**: - -- `folder`: Folder path relative to music library path -- `recursive`: Add folder recursively - - - -#### play\_album - -```python -@plugs.tag -def play_album(albumartist: str, album: str) -``` - -Playback a album found in MPD database. - -All album songs are added to the playlist -The playlist is cleared first. - -**Arguments**: - -- `albumartist`: Artist of the Album provided by MPD database -- `album`: Album name provided by MPD database - - - -#### get\_volume - -```python -def get_volume() -``` - -Get the current volume - -For volume control do not use directly, but use through the plugin 'volume', -as the user may have configured a volume control manager other than MPD - - - -#### set\_volume - -```python -def set_volume(volume) -``` - -Set the volume - -For volume control do not use directly, but use through the plugin 'volume', -as the user may have configured a volume control manager other than MPD - - - -#### play\_card\_callbacks - -Callback handler instance for play_card events. -- is executed when play_card function is called -States: -- See :class:`PlayCardState` -See :class:`PlayContentCallbacks` - - - -# components.rpc\_command\_alias - -This file provides definitions for RPC command aliases - -See :ref:`userguide/rpc_commands` - - - -# components.synchronisation.rfidcards - -Handles the synchronisation of RFID cards (audiofolder and card database entries). - -sync-all -> all card entries and audiofolders are synced from remote including deletions -sync-on-scan -> only the entry and audiofolder for the cardId will be synced from remote. - Deletions are only performed on files and subfolder inside the audiofolder. - A deletion of the audiofolder itself on remote side will not be propagated. - -card database: -On synchronisation the remote file will not be synced with the original cards database, but rather a local copy. -If a full sync is performed, the state is written back to the original file. -If a single card sync is performed, only the state of the specific cardId is updated in the original file. -This is done to allow to play audio offline. -Otherwise we would also update other cardIds where the audiofolders have not been synced yet. -The local copy is kept to reduce unnecessary syncing. - - - -## SyncRfidcards Objects - -```python -class SyncRfidcards() -``` - -Control class for sync RFID cards functionality - - - -#### sync\_change\_on\_rfid\_scan - -```python -@plugs.tag -def sync_change_on_rfid_scan(option: str = 'toggle') -> None -``` - -Change activation of 'on_rfid_scan_enabled' - -**Arguments**: - -- `option`: Must be one of 'enable', 'disable', 'toggle' - - - -#### sync\_all - -```python -@plugs.tag -def sync_all() -> bool -``` - -Sync all audiofolder and cardids from the remote server. -Removes local entries not existing at the remote server. - - - -#### sync\_card\_database - -```python -@plugs.tag -def sync_card_database(card_id: str) -> bool -``` - -Sync the card database from the remote server, if existing. - -If card_id is provided only this entry is updated. - -**Arguments**: - -- `card_id`: The cardid to update - - - -#### sync\_folder - -```python -@plugs.tag -def sync_folder(folder: str) -> bool -``` - -Sync the folder from the remote server, if existing - -**Arguments**: - -- `folder`: Folder path relative to music library path - - - -# components.synchronisation - - - -# components.synchronisation.syncutils - - - -# components.volume - -PulseAudio Volume Control Plugin Package - -Features - - * Volume Control - * Two outputs - * Watcher thread on volume / output change - -Publishes - - * volume.level - * volume.sink - -PulseAudio References - -https://brokkr.net/2018/05/24/down-the-drain-the-elusive-default-pulseaudio-sink/ - -Check fallback device (on device de-connect): -$ pacmd list-sinks | grep -e 'name:' -e 'index' - - -Integration - -Pulse Audio runs as a user process. Processes who want to communicate / stream to it -must also run as a user process. - -This means must also run as user process, as described in :ref:`userguide/system:Music Player Daemon (MPD)` - -Misc - -PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module -with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration -in ``/usr/pulse/default.pa``. So, we don't need to worry about it. -If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs -from the Jukebox. Remove it from the configuration! - -.. code-block:: text - - ### Use hot-plugged devices like Bluetooth or USB automatically (LP: `1702794`) - ### not available on PI? - .ifexists module-switch-on-connect.so - load-module module-switch-on-connect - .endif - -Why PulseAudio? - -The audio configuration of the system is one of those topics, -which has a myriad of options and possibilities. Every system is different and PulseAudio unifies these and -makes our life easier. Besides, it is only option to support Bluetooth at the moment. - -Callbacks: - -The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): - - ``. :func:`add_on_connect_callback` - ``. :func:`add_on_output_change_callbacks` - ``. :func:`add_on_volume_change_callback` - - - -## PulseMonitor Objects - -```python -class PulseMonitor(threading.Thread) -``` - -A thread for monitoring and interacting with the Pulse Lib via pulsectrl - -Whenever we want to access pulsectl, we need to exit the event listen loop. -This is handled by the context manager. It stops the event loop and returns -the pulsectl instance to be used (it does no return the monitor thread itself!) - -The context manager also locks the module to ensure proper thread sequencing, -as only a single thread may work with pulsectl at any time. Currently, an RLock is -used, even if it may not be necessary - - - -## SoundCardConnectCallbacks Objects - -```python -class SoundCardConnectCallbacks(CallbackHandler) -``` - -Callbacks are executed when - - * new sound card gets connected - - - -#### register - -```python -def register(func: Callable[[str, str], None]) -``` - -Add a new callback function :attr:`func`. - -Callback signature is - -.. py:function:: func(card_driver: str, device_name: str) - :noindex: - - :param card_driver: The PulseAudio card driver module, - e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` - :param device_name: The sound card device name as reported - in device properties - - - - -#### run\_callbacks - -```python -def run_callbacks(sink_name, alias, sink_index, error_state) -``` - -:meta private: - - - -#### toggle\_on\_connect - -```python -@property -def toggle_on_connect() -``` - -Returns :data:`True` if the sound card shall be changed when a new card connects/disconnects. Setting this -property changes the behavior. - -.. note:: A new card is always assumed to be the secondary device from the audio configuration. - At the moment there is no check it actually is the configured device. This means any new - device connection will initiate the toggle. This, however, is no real issue as the RPi's audio - system will be relatively stable once setup - - - -#### toggle\_on\_connect - -```python -@toggle_on_connect.setter -def toggle_on_connect(state=True) -``` - -Toggle Doc 2 - - - -#### stop - -```python -def stop() -``` - -Stop the pulse monitor thread - - - -#### run - -```python -def run() -> None -``` - -Starts the pulse monitor thread - - - -## PulseVolumeControl Objects - -```python -class PulseVolumeControl() -``` - -Volume control manager for PulseAudio - -When accessing the pulse library, it needs to be put into a special -state. Which is ensured by the context manager - -.. code-block: python - - with pulse_monitor as pulse ... - - -All private functions starting with `_function_name` assume that this is ensured by -the calling function. All user functions acquire proper context! - - - -## OutputChangeCallbackHandler Objects - -```python -class OutputChangeCallbackHandler(CallbackHandler) -``` - -Callbacks are executed when - - * audio sink is changed - - - -#### register - -```python -def register(func: Callable[[str, str, int, int], None]) -``` - -Add a new callback function :attr:`func`. - -Parameters always give the valid audio sink. That means, if an error -occurred, all parameters are valid. - -Callback signature is - -.. py:function:: func(sink_name: str, alias: str, sink_index: int, error_state: int) - :noindex: - - :param sink_name: PulseAudio's sink name - :param alias: The alias for :attr:`sink_name` - :param sink_index: The index of the sink in the configuration list - :param error_state: 1 if there was an attempt to change the output - but an error occurred. Above parameters always give the now valid sink! - If a sink change is successful, it is 0. - - - - -#### run\_callbacks - -```python -def run_callbacks(sink_name, alias, sink_index, error_state) -``` - -:meta private: - - - -## OutputVolumeCallbackHandler Objects - -```python -class OutputVolumeCallbackHandler(CallbackHandler) -``` - -Callbacks are executed when - - * audio volume level is changed - - - -#### register - -```python -def register(func: Callable[[int, bool, bool], None]) -``` - -Add a new callback function :attr:`func`. - -Callback signature is - -.. py:function:: func(volume: int, is_min: bool, is_max: bool) - :noindex: - - :param volume: Volume level - :param is_min: 1, if volume level is minimum, else 0 - :param is_max: 1, if volume level is maximum, else 0 - - - - -#### run\_callbacks - -```python -def run_callbacks(sink_name, alias, sink_index, error_state) -``` - -:meta private: - - - -#### toggle\_output - -```python -@plugin.tag -def toggle_output() -``` - -Toggle the audio output sink - - - -#### get\_outputs - -```python -@plugin.tag -def get_outputs() -``` - -Get current output and list of outputs - - - -#### publish\_volume - -```python -@plugin.tag -def publish_volume() -``` - -Publish (volume, mute) - - - -#### publish\_outputs - -```python -@plugin.tag -def publish_outputs() -``` - -Publish current output and list of outputs - - - -#### set\_volume - -```python -@plugin.tag -def set_volume(volume: int) -``` - -Set the volume (0-100) for the currently active output - - - -#### get\_volume - -```python -@plugin.tag -def get_volume() -``` - -Get the volume - - - -#### change\_volume - -```python -@plugin.tag -def change_volume(step: int) -``` - -Increase/decrease the volume by step for the currently active output - - - -#### get\_mute - -```python -@plugin.tag -def get_mute() -``` - -Return mute status for the currently active output - - - -#### mute - -```python -@plugin.tag -def mute(mute=True) -``` - -Set mute status for the currently active output - - - -#### set\_output - -```python -@plugin.tag -def set_output(sink_index: int) -``` - -Set the active output (sink_index = 0: primary, 1: secondary) - - - -#### set\_soft\_max\_volume - -```python -@plugin.tag -def set_soft_max_volume(max_volume: int) -``` - -Limit the maximum volume to max_volume for the currently active output - - - -#### get\_soft\_max\_volume - -```python -@plugin.tag -def get_soft_max_volume() -``` - -Return the maximum volume limit for the currently active output - - - -#### card\_list - -```python -def card_list() -> List[pulsectl.PulseCardInfo] -``` - -Return the list of present sound card - - - -# components.rfid - - - -# components.rfid.reader - - - -## RfidCardDetectCallbacks Objects - -```python -class RfidCardDetectCallbacks(CallbackHandler) -``` - -Callbacks are executed if rfid card is detected - - - -#### register - -```python -def register(func: Callable[[str, RfidCardDetectState], None]) -``` - -Add a new callback function :attr:`func`. - -Callback signature is - -.. py:function:: func(card_id: str, state: int) - :noindex: - - :param card_id: Card ID - :param state: See :class:`RfidCardDetectState` - - - - -#### run\_callbacks - -```python -def run_callbacks(card_id: str, state: RfidCardDetectState) -``` - -:meta private: - - - -#### rfid\_card\_detect\_callbacks - -Callback handler instance for rfid_card_detect_callbacks events. -See :class:`RfidCardDetectCallbacks` - - - -## CardRemovalTimerClass Objects - -```python -class CardRemovalTimerClass(threading.Thread) -``` - -A timer watchdog thread that calls timeout_action on time-out - - - -#### \_\_init\_\_ - -```python -def __init__(on_timeout_callback, logger: logging.Logger = None) -``` - -**Arguments**: - -- `on_timeout_callback`: The function to execute on time-out - - - -# components.rfid.configure - - - -#### reader\_install\_dependencies - -```python -def reader_install_dependencies(reader_path: str, - dependency_install: str) -> None -``` - -Install dependencies for the selected reader module - -**Arguments**: - -- `reader_path`: Path to the reader module -- `dependency_install`: how to handle installing of dependencies -'query': query user (default) -'auto': automatically -'no': don't install dependencies - - - -#### reader\_load\_module - -```python -def reader_load_module(reader_name) -``` - -Load the module for the reader_name - -A ModuleNotFoundError is unrecoverable, but we at least want to give some hint how to resolve that to the user -All other errors will NOT be handled. Modules that do not load due to compile errors have other problems - -**Arguments**: - -- `reader_name`: Name of the reader to load the module for - -**Returns**: - -module - - - -#### query\_user\_for\_reader - -```python -def query_user_for_reader(dependency_install='query') -> dict -``` - -Ask the user to select a RFID reader and prompt for the reader's configuration - -This function performs the following steps, to find and present all available readers to the user - -- search for available reader subpackages -- dynamically load the description module for each reader subpackage -- queries user for selection -- if no_dep_install=False, install dependencies as given by requirements.txt and execute setup.inc.sh of subpackage -- dynamically load the actual reader module from the reader subpackage -- if selected reader has customization options query user for that now -- return configuration - -There are checks to make sure we have the right reader modules and they are what we expect. -The are as few requirements towards the reader module as possible and everything else is optional -(see reader_template for these requirements) -However, there is no error handling w.r.t to user input and reader's query_config. Firstly, in this script -we cannot gracefully handle an exception that occurs on reader level, and secondly the exception will simply -exit the script w/o writing the config to file. No harm done. - -This script expects to reside in the directory with all the reader subpackages, i.e it is part of the rfid-reader package. -Otherwise you'll need to adjust sys.path - -**Arguments**: - -- `dependency_install`: how to handle installing of dependencies -'query': query user (default) -'auto': automatically -'no': don't install dependencies - -**Returns**: - -`dict as {section: {parameter: value}}`: nested dict with entire configuration that can be read into ConfigParser - - - -#### write\_config - -```python -def write_config(config_file: str, - config_dict: dict, - force_overwrite=False) -> None -``` - -Write configuration to config_file - -**Arguments**: - -- `config_file`: relative or absolute path to config file -- `config_dict`: nested dict with configuration parameters for ConfigParser consumption -- `force_overwrite`: overwrite existing configuration file without asking - - - -# components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui - - - -# components.rfid.hardware.fake\_reader\_gui.description - - - -# components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon - -Add GPIO input devices and output devices to the RFID Mock Reader GUI - - - -#### create\_inputs - -```python -def create_inputs(frame, default_btn_width, default_padx, default_pady) -``` - -Add all input devies to the GUI - -**Arguments**: - -- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to - -**Returns**: - -List of all added GUI buttons - - - -#### set\_state - -```python -def set_state(value, box_state_var) -``` - -Change the value of a checkbox state variable - - - -#### que\_set\_state - -```python -def que_set_state(value, box_state_var) -``` - -Queue the action to change a checkbox state variable to the TK GUI main thread - - - -#### fix\_state - -```python -def fix_state(box_state_var) -``` - -Prevent a checkbox state variable to change on checkbox mouse press - - - -#### pbox\_set\_state - -```python -def pbox_set_state(value, pbox_state_var, label_var) -``` - -Update progress bar state and related state label - - - -#### que\_set\_pbox - -```python -def que_set_pbox(value, pbox_state_var, label_var) -``` - -Queue the action to change the progress bar state to the TK GUI main thread - - - -#### create\_outputs - -```python -def create_outputs(frame, default_btn_width, default_padx, default_pady) -``` - -Add all output devices to the GUI - -**Arguments**: - -- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to - -**Returns**: - -List of all added GUI objects - - - -# components.rfid.hardware.generic\_usb.description - - - -# components.rfid.hardware.generic\_usb.generic\_usb - - - -# components.rfid.hardware.rc522\_spi.description - - - -# components.rfid.hardware.rc522\_spi.rc522\_spi - - - -# components.rfid.hardware.pn532\_i2c\_py532.description - - - -# components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532 - - - -# components.rfid.hardware.rdm6300\_serial.rdm6300\_serial - - - -#### decode - -```python -def decode(raw_card_id: bytearray, number_format: int) -> str -``` - -Decode the RDM6300 data format into actual card ID - - - -# components.rfid.hardware.rdm6300\_serial.description - - - -# components.rfid.hardware.template\_new\_reader.description - -Provide a short title for this reader. -This is what that user will see when asked for selecting his RFID reader -So, be precise but readable. Precise means 40 characters or less - - - -# components.rfid.hardware.template\_new\_reader.template\_new\_reader - - - -#### query\_customization - -```python -def query_customization() -> dict -``` - -Query the user for reader parameter customization - -This function will be called during the configuration/setup phase when the user selects this reader module. -It must return all configuration parameters that are necessary to later use the Reader class. -You can ask the user for selections and choices. And/or provide default values. -If your reader requires absolutely no configuration return {} - - - -## ReaderClass Objects - -```python -class ReaderClass(ReaderBaseClass) -``` - -The actual reader class that is used to read RFID cards. - -It will be instantiated once and then read_card() is called in an endless loop. - -It will be used in a manner - with Reader(reader_cfg_key) as reader: - for card_id in reader: - ... -which ensures proper resource de-allocation. For this to work derive this class from ReaderBaseClass. -All the required interfaces are implemented there. - -Put your code into these functions (see below for more information) - - __init__ - - read_card - - cleanup - - stop - - - -#### \_\_init\_\_ - -```python -def __init__(reader_cfg_key) -``` - -In the constructor, you will get the `reader_cfg_key` with which you can access the configuration data - -As you are dealing directly with potentially user-manipulated config information, it is -advisable to do some sanity checks and give useful error messages. Even if you cannot recover gracefully, -a good error message helps :-) - - - -#### cleanup - -```python -def cleanup() -``` - -The cleanup function: free and release all resources used by this card reader (if any). - -Put all your cleanup code here, e.g. if you are using the serial bus or GPIO pins. -Will be called implicitly via the __exit__ function -This function must exist! If there is nothing to do, just leave the pass statement in place below - - - -#### stop - -```python -def stop() -``` - -This function is called to tell the reader to exist it's reading function. - -This function is called before cleanup is called. - -.. note: This is usually called from a different thread than the reader's thread! And this is the reason for the - two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt - to read a card. Once called, the function read_card will not be called again. When the reader thread exits - cleanup is called from the reader thread itself. - - - -#### read\_card - -```python -def read_card() -> str -``` - -Blocking or non-blocking function that waits for a new card to appear and return the card's UID as string - -This is were your main code goes :-) -This function must return a string with the card id -In case of error, it may return None or an empty string - -The function should break and return with an empty string, once stop() is called - - - -# components.rfid.readerbase - - - -## ReaderBaseClass Objects - -```python -class ReaderBaseClass(ABC) -``` - -Abstract Base Class for all Reader Classes to ensure common API - -Look at template_new_reader.py for documentation how to integrate a new RFID reader - - - -# components.rfid.cards - -Handling the RFID card database - -A few considerations: -- Changing the Card DB influences to current state - - rfid.reader: Does not care, as it always freshly looks into the DB when a new card is triggered - - fake_reader_gui: Initializes the Drop-down menu once on start --> Will get out of date! - -Do we need a notifier? Or a callback for modules to get notified? -Do we want to publish the information about a card DB update? -TODO: Add callback for on_database_change - -TODO: check card id type (if int, convert to str) -TODO: check if args is really a list (convert if not?) - - - -#### list\_cards - -```python -@plugs.register -def list_cards() -``` - -Provide a summarized, decoded list of all card actions - -This is intended as basis for a formatter function - -Format: 'id': {decoded_function_call, ignore_same_id_delay, ignore_card_removal_action, description, from_alias} - - - -#### delete\_card - -```python -@plugs.register -def delete_card(card_id: str, auto_save: bool = True) -``` - -**Arguments**: - -- `auto_save`: -- `card_id`: - - - -#### register\_card - -```python -@plugs.register -def register_card(card_id: str, - cmd_alias: str, - args: Optional[List] = None, - kwargs: Optional[Dict] = None, - ignore_card_removal_action: Optional[bool] = None, - ignore_same_id_delay: Optional[bool] = None, - overwrite: bool = False, - auto_save: bool = True) -``` - -Register a new card based on quick-selection - -If you are going to call this through the RPC it will get a little verbose - -**Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume -(*here: 15*) and custom *ignore_same_id_delay value*:: - -plugin.call_ignore_errors('cards', 'register_card', -args=['0009', 'inc_volume'], -kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) - - - -#### register\_card\_custom - -```python -@plugs.register -def register_card_custom() -``` - -Register a new card with full RPC call specification (Not implemented yet) - - - -#### save\_card\_database - -```python -@plugs.register -def save_card_database(filename=None, *, only_if_changed=True) -``` - -Store the current card database. If filename is None, it is saved back to the file it was loaded from - - - -# components.rfid.cardutils - -Common card decoding functions - -TODO: Thread safety when accessing the card DB! - - - -#### decode\_card\_command - -```python -def decode_card_command(cfg_rpc_cmd: Mapping, logger: logging.Logger = log) -``` - -Extension of utils.decode_action with card-specific parameters - - - -#### card\_command\_to\_str - -```python -def card_command_to_str(cfg_rpc_cmd: Mapping, long=False) -> List[str] -``` - -Returns a list of strings with [card_action, ignore_same_id_delay, ignore_card_removal_action] - -The last two parameters are only present, if *long* is True and if they are present in the cfg_rpc_cmd - - - -#### card\_to\_str - -```python -def card_to_str(card_id: str, long=False) -> List[str] -``` - -Returns a list of strings from card entry command in the format of :func:`card_command_to_str` - - - -# components.publishing - -Plugin interface for Jukebox Publisher - -Thin wrapper around jukebox.publishing to benefit from the plugin loading / exit handling / function handling - -This is the first package to be loaded and the last to be closed: put Hello and Goodbye publish messages here. - - - -#### republish - -```python -@plugin.register -def republish(topic=None) -``` - -Re-publish the topic tree 'topic' to all subscribers - -**Arguments**: - -- `topic`: Topic tree to republish. None = resend all - - - -# components.player - - - -## MusicLibPath Objects - -```python -class MusicLibPath() -``` - -Extract the music directory from the mpd.conf file - - - -#### get\_music\_library\_path - -```python -def get_music_library_path() -``` - -Get the music library path - - - -# components.jingle - -Jingle Playback Factory for extensible run-time support of various file types - - - -## JingleFactory Objects - -```python -class JingleFactory() -``` - -Jingle Factory - - - -#### list - -```python -def list() -``` - -List the available volume services - - - -#### play - -```python -@plugin.register -def play(filename) -``` - -Play the jingle using the configured jingle service - -Note: This runs in a separate thread. And this may cause troubles -when changing the volume level before -and after the sound playback: There is nothing to prevent another -thread from changing the volume and sink while playback happens -and afterwards we change the volume back to where it was before! - -There is no way around this dilemma except for not running the jingle as a -separate thread. Currently (as thread) even the RPC is started before the sound -is finished and the volume is reset to normal... - -However: Volume plugin is loaded before jingle and sets the default -volume. No interference here. It can now only happen -if (a) through the RPC or (b) some other plugin the volume is changed. Okay, now -(a) let's hope that there is enough delay in the user requesting a volume change -(b) let's hope no other plugin wants to do that -(c) no bluetooth device connects during this time (and pulseaudio control is set to toggle_on_connect) -and take our changes with the threaded approach. - - - -#### play\_startup - -```python -@plugin.register -def play_startup() -``` - -Play the startup sound (using jingle.play) - - - -#### play\_shutdown - -```python -@plugin.register -def play_shutdown() -``` - -Play the shutdown sound (using jingle.play) - - - -# components.jingle.alsawave - -ALSA wave jingle Service for jingle.JingleFactory - - - -## AlsaWave Objects - -```python -@plugin.register -class AlsaWave() -``` - -Jingle Service for playing wave files directly from Python through ALSA - - - -#### play - -```python -@plugin.tag -def play(filename) -``` - -Play the wave file - - - -## AlsaWaveBuilder Objects - -```python -class AlsaWaveBuilder() -``` - - - -#### \_\_init\_\_ - -```python -def __init__() -``` - -Builder instantiates AlsaWave during init and not during first call because -we want AlsaWave registers as plugin function in any case if this plugin is loaded -(and not only on first use!) - - - -# components.jingle.jinglemp3 - -Generic MP3 jingle Service for jingle.JingleFactory - - - -## JingleMp3Play Objects - -```python -@plugin.register(auto_tag=True) -class JingleMp3Play() -``` - -Jingle Service for playing MP3 files - - - -#### play - -```python -def play(filename) -``` - -Play the MP3 file - - - -## JingleMp3PlayBuilder Objects - -```python -class JingleMp3PlayBuilder() -``` - - - -#### \_\_init\_\_ - -```python -def __init__() -``` - -Builder instantiates JingleMp3Play during init and not during first call because -we want JingleMp3Play registers as plugin function in any case if this plugin is loaded -(and not only on first use!) - - - -# components.hostif.linux - - - -#### shutdown - -```python -@plugin.register -def shutdown() -``` - -Shutdown the host machine - - - -#### reboot - -```python -@plugin.register -def reboot() -``` - -Reboot the host machine - - - -#### jukebox\_is\_service - -```python -@plugin.register -def jukebox_is_service() -``` - -Check if current Jukebox process is running as a service - - - -#### is\_any\_jukebox\_service\_active - -```python -@plugin.register -def is_any_jukebox_service_active() -``` - -Check if a Jukebox service is running - -.. note:: Does not have the be the current app, that is running as a service! - - - -#### restart\_service - -```python -@plugin.register -def restart_service() -``` - -Restart Jukebox App if running as a service - - - -#### get\_disk\_usage - -```python -@plugin.register() -def get_disk_usage(path='/') -``` - -Return the disk usage in Megabytes as dictionary for RPC export - - - -#### get\_cpu\_temperature - -```python -@plugin.register -def get_cpu_temperature() -``` - -Get the CPU temperature with single decimal point - -No error handling: this is expected to take place up-level! - - - -#### get\_ip\_address - -```python -@plugin.register -def get_ip_address() -``` - -Get the IP address - - - -#### wlan\_disable\_power\_down - -```python -@plugin.register() -def wlan_disable_power_down(card=None) -``` - -Turn off power management of wlan. Keep RPi reachable via WLAN - -This must be done after every reboot -card=None takes card from configuration file - - - -#### get\_autohotspot\_status - -```python -@plugin.register -def get_autohotspot_status() -``` - -Get the status of the auto hotspot feature - - - -#### stop\_autohotspot - -```python -@plugin.register() -def stop_autohotspot() -``` - -Stop auto hotspot functionality - -Basically disabling the cronjob and running the script one last time manually - - - -#### start\_autohotspot - -```python -@plugin.register() -def start_autohotspot() -``` - -start auto hotspot functionality - -Basically enabling the cronjob and running the script one time manually - - - -# components.misc - -Miscellaneous function package - - - -#### rpc\_cmd\_help - -```python -@plugin.register -def rpc_cmd_help() -``` - -Return all commands for RPC - - - -#### get\_all\_loaded\_packages - -```python -@plugin.register -def get_all_loaded_packages() -``` - -Get all successfully loaded plugins - - - -#### get\_all\_failed\_packages - -```python -@plugin.register -def get_all_failed_packages() -``` - -Get all plugins with error during load or initialization - - - -#### get\_start\_time - -```python -@plugin.register -def get_start_time() -``` - -Time when JukeBox has been started - - - -#### get\_log - -```python -def get_log(handler_name: str) -``` - -Get the log file from the loggers (debug_file_handler, error_file_handler) - - - -#### get\_log\_debug - -```python -@plugin.register -def get_log_debug() -``` - -Get the log file (from the debug_file_handler) - - - -#### get\_log\_error - -```python -@plugin.register -def get_log_error() -``` - -Get the log file (from the error_file_handler) - - - -#### get\_git\_state - -```python -@plugin.register -def get_git_state() -``` - -Return git state information for the current branch - - - -#### empty\_rpc\_call - -```python -@plugin.register -def empty_rpc_call(msg: str = '') -``` - -This function does nothing. - -The RPC command alias 'none' is mapped to this function. - -This is also used when configuration errors lead to non existing RPC command alias definitions. -When the alias definition is void, we still want to return a valid function to simplify error handling -up the module call stack. - -**Arguments**: - -- `msg`: If present, this message is send to the logger with severity warning - - - -# components.controls - - - -# components.controls.bluetooth\_audio\_buttons - -Plugin to attempt to automatically listen to it's buttons (play, next, ...) -when a bluetooth sound device (headphone, speakers) connects - -This effectively does: - - * register a callback with components.volume to get notified when a new sound card connects - * if that is a bluetooth device, try opening an input device with similar name using - * button listeners are run each in its own thread - - - -# components.controls.common.evdev\_listener - -Generalized listener for ``dev/input`` devices - - - -#### find\_device - -```python -def find_device(device_name: str, - exact_name: bool = True, - mandatory_keys: Optional[Set[int]] = None) -> str -``` - -Find an input device with device_name and mandatory keys. - -Raises - - ``. FileNotFoundError, if no device is found. - ``. AttributeError, if device does not have the mandatory keys - -If multiple devices match, the first match is returned - -**Arguments**: - -- `device_name`: See :func:`_filter_by_device_name` -- `exact_name`: See :func:`_filter_by_device_name` -- `mandatory_keys`: See :func:`_filter_by_mandatory_keys` - -**Returns**: - -The path to the device - - - -## EvDevKeyListener Objects - -```python -class EvDevKeyListener(threading.Thread) -``` - -Opens and event input device from ``/dev/inputs``, and runs callbacks upon the button presses. -Input devices could be .e.g. Keyboard, Bluetooth audio buttons, USB buttons - -Runs as a separate thread. When device disconnects or disappears, thread exists. A new thread must be started -when device re-connects. - -Assign callbacks to :attr:`EvDevKeyListener.button_callbacks` - - - -#### \_\_init\_\_ - -```python -def __init__(device_name_request: str, exact_name: bool, thread_name: str) -``` - -**Arguments**: - -- `device_name_request`: The device name to look for -- `exact_name`: If true, device_name must mach exactly, else a match is returned if device_name is a substring of -the reported device name -- `thread_name`: Name of the listener thread - - - -#### run - -```python -def run() -``` - - - - - -#### start - -```python -def start() -> None -``` - -Start the tread and start listening - - - -# components.music\_cover\_art - -Read all cover art from music save it to a cache for the UI to load - -.. note:: Not implemented. This is a feature planned for a future release. - - - -## MusicCoverArt Objects - -```python -class MusicCoverArt() -``` - - - -#### get\_by\_filename\_as\_base64 - -```python -@plugin.tag -def get_by_filename_as_base64(audio_src: str) -``` - -Not implemented. This is a feature planned for a future release. - - - -# components.battery\_monitor - - - -# components.battery\_monitor.BatteryMonitorBase - - - -## pt1\_frac Objects - -```python -class pt1_frac() -``` - -fixed point first order filter, fractional format: 2^16,2^16 - - - -## BattmonBase Objects - -```python -class BattmonBase() -``` - -Battery Monitor base class - - - -# components.battery\_monitor.batt\_mon\_simulator - - - -## battmon\_simulator Objects - -```python -class battmon_simulator(BatteryMonitorBase.BattmonBase) -``` - -Battery Monitor Simulator - - - -# components.battery\_monitor.batt\_mon\_i2c\_ads1015 - - - -## battmon\_ads1015 Objects - -```python -class battmon_ads1015(BatteryMonitorBase.BattmonBase) -``` - -Battery Monitor based on a ADS1015 - -CAUTION - WARNING -======================================================================== -Lithium and other batteries are dangerous and must be treated with care. -Rechargeable Lithium Ion batteries are potentially hazardous and can -present a serious FIRE HAZARD if damaged, defective or improperly used. -Do not use this circuit to a lithium ion battery without expertise and -training in handling and use of batteries of this type. -Use appropriate test equipment and safety protocols during development. - -There is no warranty, this may not work as expected or at all! -========================================================================= - -This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: - - 3.3V - + - | - .----o----. - ___ | | SDA - .--------|___|---o----o---------o AIN0 o------ - | 2MΩ | | | | SCL - | .-. | | ADS1015 o------ - --- | | --- | | - Battery - 1.5MΩ| | ---100nF '----o----' - 2.9V-4.2V| '-' | | - | | | | - === === === === - -Attention: - - the circuit is constantly draining the battery! (leak current up to: 2.1µA) - - the time between sample needs to be a minimum 1sec with this high impedance voltage divider - don't use the continuous conversion method! - - - -# components.gpio.gpioz.plugin - -The GPIOZ plugin interface build all input and output devices from the configuration file and connects -the actions and callbacks. It also provides a very restricted, but common API for the output devices to the RPC. -That API is mainly used for testing. All the relevant output state changes are usually made through callbacks directly -using the output device's API. - - - -#### output\_devices - -List of all created output devices - - - -#### input\_devices - -List of all created input devices - - - -#### factory - -The global pin factory used in this module -Using different pin factories for different devices is not supported - - - -#### IS\_ENABLED - -Indicates that the GPIOZ module is enabled and loaded w/o errors - - - -#### IS\_MOCKED - -Indicates that the pin factory is a mock factory - - - -#### CONFIG\_FILE - -The path of the config file the GPIOZ configuration was loaded from - - - -## ServiceIsRunningCallbacks Objects - -```python -class ServiceIsRunningCallbacks(CallbackHandler) -``` - -Callbacks are executed when - - * Jukebox app started - * Jukebox shuts down - -This is intended to e.g. signal an LED to change state. -This is integrated into this module because: - - * we need the GPIO to control a LED (it must be available when the status callback comes) - * the plugin callback functions provide all the functionality to control the status of the LED - * which means no need to adapt other modules - - - -#### register - -```python -def register(func: Callable[[int], None]) -``` - -Add a new callback function :attr:`func`. - -Callback signature is - -.. py:function:: func(status: int) - :noindex: - - :param status: 1 if app started, 0 if app shuts down - - - - -#### run\_callbacks - -```python -def run_callbacks(status: int) -``` - -:meta private: - - - -#### service\_is\_running\_callbacks - -Callback handler instance for service_is_running_callbacks events. -See :class:`ServiceIsRunningCallbacks` - - - -#### build\_output\_device - -```python -def build_output_device(name: str, config: Dict) -``` - -Construct and register a new output device - -In principal all supported GPIOZero output devices can be used. -For all devices a custom functions need to be written to control the state of the outputs - - - -#### build\_input\_device - -```python -def build_input_device(name: str, config) -``` - -Construct and connect a new input device - -Supported input devices are those from gpio.gpioz.core.input_devices - - - -#### get\_output - -```python -def get_output(name: str) -``` - -Get the output device instance based on the configured name - -**Arguments**: - -- `name`: The alias name output device instance - - - -#### on - -```python -@plugin.register -def on(name: str) -``` - -Turn an output device on - -**Arguments**: - -- `name`: The alias name output device instance - - - -#### off - -```python -@plugin.register -def off(name: str) -``` - -Turn an output device off - -**Arguments**: - -- `name`: The alias name output device instance - - - -#### set\_value - -```python -@plugin.register -def set_value(name: str, value: Any) -``` - -Set the output device to :attr:`value` - -**Arguments**: - -- `name`: The alias name output device instance -- `value`: Value to set the device to - - - -#### flash - -```python -@plugin.register -def flash(name, - on_time=1, - off_time=1, - n=1, - *, - fade_in_time=0, - fade_out_time=0, - tone=None, - color=(1, 1, 1)) -``` - -Flash (blink or beep) an output device - -This is a generic function for all types of output devices. Parameters not applicable to an -specific output device are silently ignored - -**Arguments**: - -- `name`: The alias name output device instance -- `on_time`: Time in seconds in state ``ON`` -- `off_time`: Time in seconds in state ``OFF`` -- `n`: Number of flash cycles -- `tone`: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. -- `color`: The RGB color *only for PWMLED*. -- `fade_in_time`: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* -- `fade_out_time`: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* - - - -# components.gpio.gpioz.plugin.connectivity - -Provide connector functions to hook up to some kind of Jukebox functionality and change the output device's state -accordingly. - -Connector functions can often be used for various output devices. Some connector functions are specific to -an output device type. - - - -#### BUZZ\_TONE - -The tone to be used as buzz tone when the buzzer is an active buzzer - - - -#### register\_rfid\_callback - -```python -def register_rfid_callback(device) -``` - -Flash the output device once on successful RFID card detection and thrice if card ID is unknown - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` - - - -#### register\_status\_led\_callback - -```python -def register_status_led_callback(device) -``` - -Turn LED on when Jukebox App has started - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` - - - -#### register\_status\_buzzer\_callback - -```python -def register_status_buzzer_callback(device) -``` - -Buzz once when Jukebox App has started, twice when closing down - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` - - - -#### register\_status\_tonalbuzzer\_callback - -```python -def register_status_tonalbuzzer_callback(device) -``` - -Buzz a multi-note melody when Jukebox App has started and when closing down - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` - - - -#### register\_audio\_sink\_change\_callback - -```python -def register_audio_sink_change_callback(device) -``` - -Turn LED on if secondary audio output is selected. If audio output change -fails, blink thrice - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` - - - -#### register\_volume\_led\_callback - -```python -def register_volume_led_callback(device) -``` - -Have a PWMLED change it's brightness according to current volume. LED flashes when minimum or maximum volume -is reached. Minimum value is still a very dimly turned on LED (i.e. LED is never off). - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - - -#### register\_volume\_buzzer\_callback - -```python -def register_volume_buzzer_callback(device) -``` - -Sound a buzzer once when minimum or maximum value is reached - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` - - - -#### register\_volume\_rgbled\_callback - -```python -def register_volume_rgbled_callback(device) -``` - -Have a :class:`RGBLED` change it's color according to current volume. LED flashes when minimum or maximum volume -is reached. - -Compatible devices: - - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` - - - -# components.gpio.gpioz.core.converter - -Provides converter functions/classes for various Jukebox parameters to -values that can be assigned to GPIO output devices - - - -## ColorProperty Objects - -```python -class ColorProperty() -``` - -Color descriptor ensuring valid weight ranges - -:meta private: - - - -## VolumeToRGB Objects - -```python -class VolumeToRGB() -``` - -Converts linear volume level to an RGB color value running through the color spectrum - -**Arguments**: - -- `max_input`: Maximum input value of linear input data -- `offset`: Offset in degrees in the color circle. Color circle -traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) -- `section`: The section of the full color circle to use in degrees -Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 - -.. code-block:: python - - conv = VolumeToRGB(100, offset=120, section=180) - (r, g, b) = conv(50) - -The three components of an RGB LEDs do not have the same luminosity. -Weight factors are used to get a balanced color output - - - -#### \_\_call\_\_ - -```python -def __call__(volume) -> Tuple[float, float, float] -``` - -Perform conversion for single volume level - -**Returns**: - -Tuple(red, green, blue) - - - -#### luminize - -```python -def luminize(r, g, b) -``` - -Apply the color weight factors to the input color values - - - -# components.gpio.gpioz.core.mock - -Changes to the GPIOZero devices for using with the Mock RFID Reader - - - -#### patch\_mock\_outputs\_with\_callback - -```python -def patch_mock_outputs_with_callback() -``` - -Monkey Patch LED + Buzzer to get a callback when state changes - -This targets to represent the state in the TK GUI. -Other output devices cannot be represented in the GUI and are silently ignored. - -..note:: Only for developing purposes! - - - -# components.gpio.gpioz.core.input\_devices - -Provides all supported input devices for the GPIOZ plugin. - -Input devices are based on GPIOZero devices. So for certain configuration parameters, you should -their documentation. - -All callback handlers are replaced by GPIOZ callback handlers. These are usually configured -by using the :func:`set_rpc_actions` each input device exhibits. - -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Input devices` - - - -## NameMixin Objects - -```python -class NameMixin(ABC) -``` - -Provides name property and RPC decode function - -:meta private: - - - -#### set\_rpc\_actions - -```python -@abstractmethod -def set_rpc_actions(action_config) -> None -``` - -Set all input device callbacks from :attr:`action_config` - -**Arguments**: - -- `action_config`: Dictionary with one -:ref:`RPC Command ` definition entry for every device callback - - - -## EventProperty Objects - -```python -class EventProperty() -``` - -Event callback property - -:meta private: - - - -## ButtonBase Objects - -```python -class ButtonBase(ABC) -``` - -Common stuff for single button devices - -:meta private: - - - -#### value - -```python -@property -def value() -``` - -Returns 1 if the button is currently pressed, and 0 if it is not. - - - -#### pin - -```python -@property -def pin() -``` - -Returns the underlying pin class from GPIOZero. - - - -#### pull\_up - -```python -@property -def pull_up() -``` - -If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. - - - -#### close - -```python -def close() -``` - -Close the device and release the pin - - - -## Button Objects - -```python -class Button(NameMixin, ButtonBase) -``` - -A basic Button that runs a single actions on button press - -**Arguments**: - -- `pull_up` (`bool`): If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. -If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external -resistor must be used and the :attr:`active_state` must be set. -- `active_state` (`bool or None`): If :data:`True`, when the hardware pin state is ``HIGH``, the software -pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when -the hardware pin state is ``HIGH``, the software pin state is ``LOW``. -Use this parameter to set the active state of the underlying pin when -configuring it as not pulled (when *pull_up* is :data:`None`). When -*pull_up* is :data:`True` or :data:`False`, the active state is -automatically set to the proper value. -- `bounce_time` (`float or None`): Specifies the length of time (in seconds) that the component will -ignore changes in state after an initial change. This defaults to -:data:`None` which indicates that no bounce compensation will be -performed. -- `hold_repeat` (`bool`): If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action -is run only once independent of the length of time the button is pressed for. -- `hold_time` (`float`): Time in seconds to wait between invocations of :attr:`on_press`. -- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file -- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly -through the configuration file - -.. copied from GPIOZero's documentation: active_state, bounce_time -.. Copyright Ben Nuttall / SPDX-License-Identifier: BSD-3-Clause - - - -#### on\_press - -```python -@property -def on_press() -``` - -The function to run when the device has been pressed - - - -## LongPressButton Objects - -```python -class LongPressButton(NameMixin, ButtonBase) -``` - -A Button that runs a single actions only when the button is pressed long enough - -**Arguments**: - -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ -- `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action -is run only once independent of the length of time the button is pressed for. -- `hold_time`: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. -Also the time in seconds to wait between invocations of :attr:`on_press`. - - - -#### on\_press - -```python -@on_press.setter -def on_press(func) -``` - -The function to run when the device has been pressed for longer than :attr:`hold_time` - - - -## ShortLongPressButton Objects - -```python -class ShortLongPressButton(NameMixin, ButtonBase) -``` - -A single button that runs two different actions depending if the button is pressed for a short or long time. - -The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press -can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. -But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release -event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run -in this case! - -**Arguments**: - -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ -- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before -this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the -short press action is ignored -- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press -action -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ - - - -## RotaryEncoder Objects - -```python -class RotaryEncoder(NameMixin) -``` - -A rotary encoder to run one of two actions depending on the rotation direction. - -**Arguments**: - -- `bounce_time`: See `Button`_ -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ - - - -#### pin\_a - -```python -@property -def pin_a() -``` - -Returns the underlying pin A - - - -#### pin\_b - -```python -@property -def pin_b() -``` - -Returns the underlying pin B - - - -#### on\_rotate\_clockwise - -```python -@property -def on_rotate_clockwise() -``` - -The function to run when the encoder is rotated clockwise - - - -#### on\_rotate\_counter\_clockwise - -```python -@property -def on_rotate_counter_clockwise() -``` - -The function to run when the encoder is rotated counter clockwise - - - -#### close - -```python -def close() -``` - -Close the device and release the pin - - - -## TwinButton Objects - -```python -class TwinButton(NameMixin) -``` - -A two-button device which can run up to six different actions, a.k.a the six function beast. - -Per user press "input" of the TwinButton, only a single callback is executed (but this callback -may be executed several times). -The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press -can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. -But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release -event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run -in this case! - -It is not necessary to configure all actions. - -**Arguments**: - -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ -- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before -this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the -short press action is ignored. -- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press -action. A long dual press is never repeated independent of this setting -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ - - - -## StateVar Objects - -```python -class StateVar(Enum) -``` - -State encoding of the Mealy FSM - -:meta private: - - - -#### close - -```python -def close() -``` - -Close the device and release the pins - - - -#### value - -```python -@property -def value() -``` - -2 bit integer indicating if and which button is currently pressed. Button A is the LSB. - - - -#### is\_active - -```python -@property -def is_active() -``` - -:data:`True` if one or both buttons are currently pressed - - - -# components.gpio.gpioz.core.output\_devices - -Provides all supported output devices for the GPIOZ plugin. - -For each device all constructor parameters can be set via the configuration file. Only exceptions -are the :attr:`name` and :attr:`pin_factory` which are set by internal mechanisms. - -The devices a are a relatively thin wrapper around the GPIOZero devices with the same name. -We add a name property to be used for error log message and similar and a :func:`flash` function -to all devices. This function provides a unified API to all devices. This means it can be called for every device -with parameters for this device and optional parameters from another device. Unused/unsupported parameters -are silently ignored. This is done to reduce the amount of coding required for connectivity functions. - -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Output devices` - - - -## LED Objects - -```python -class LED(NameMixin, gpiozero.LED) -``` - -A binary LED - -**Arguments**: - -- `pin`: The GPIO pin which the LED is connected -- `active_high`: If :data:`true` the output pin will have a high logic level when the device is turned on. -- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file -- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly -through the configuration file - - - -#### flash - -```python -def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) -``` - -Exactly like :func:`blink` but restores the original state after flashing the device - -**Arguments**: - -- `on_time` (`float`): Number of seconds on. Defaults to 1 second. -- `off_time` (`float`): Number of seconds off. Defaults to 1 second. -- `n`: Number of times to blink; :data:`None` means forever. -- `background` (`bool`): If :data:`True` (the default), start a background thread to -continue blinking and return immediately. If :data:`False`, only -return when the blink is finished -- `ignored_kwargs`: Ignore all other keywords so this function can be called with identical -parameters also for all other output devices - - - -## Buzzer Objects - -```python -class Buzzer(NameMixin, gpiozero.Buzzer) -``` - - - -#### flash - -```python -def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) -``` - -Flash the device and restore the previous value afterwards - - - -## PWMLED Objects - -```python -class PWMLED(NameMixin, gpiozero.PWMLED) -``` - - - -#### flash - -```python -def flash(on_time=1, - off_time=1, - n=1, - *, - fade_in_time=0, - fade_out_time=0, - background=True, - **ignored_kwargs) -``` - -Flash the LED and restore the previous value afterwards - - - -## RGBLED Objects - -```python -class RGBLED(NameMixin, gpiozero.RGBLED) -``` - - - -#### flash - -```python -def flash(on_time=1, - off_time=1, - *, - fade_in_time=0, - fade_out_time=0, - on_color=(1, 1, 1), - off_color=(0, 0, 0), - n=None, - background=True, - **igorned_kwargs) -``` - -Flash the LED with :attr:`on_color` and restore the previous value afterwards - - - -## TonalBuzzer Objects - -```python -class TonalBuzzer(NameMixin, gpiozero.TonalBuzzer) -``` - - - -#### flash - -```python -def flash(on_time=1, - off_time=1, - n=1, - *, - tone=None, - background=True, - **ignored_kwargs) -``` - -Play the tone :data:`tone` for :attr:`n` times - - - -#### melody - -```python -def melody(on_time=0.2, - off_time=0.05, - *, - tone: Optional[List[Tone]] = None, - background=True) -``` - -Play a melody from the list of tones in :attr:`tone` - - - -# components.timers - - - -# jukebox - - - -# jukebox.callingback - -Provides a generic callback handler - - - -## CallbackHandler Objects - -```python -class CallbackHandler() -``` - -Generic Callback Handler to collect callbacks functions through :func:`register` and execute them - -with :func:`run_callbacks` - -A lock is used to sequence registering of new functions and running callbacks. - -**Arguments**: - -- `name`: A name of this handler for usage in log messages -- `logger`: The logger instance to use for logging -- `context`: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created - - - -#### register - -```python -def register(func: Optional[Callable[..., None]]) -``` - -Register a new function to be executed when the callback event happens - -**Arguments**: - -- `func`: The function to register. If set to :data:`None`, this register request is silently ignored. - - - -#### run\_callbacks - -```python -def run_callbacks(*args, **kwargs) -``` - -Run all registered callbacks. - -*ALL* exceptions from callback functions will be caught and logged only. -Exceptions are not raised upwards! - - - -#### has\_callbacks - -```python -@property -def has_callbacks() -``` - -:data:`True` if there are any registered callbacks. Read-only property - - - -# jukebox.version - - - -#### version - -```python -def version() -``` - -Return the Jukebox version as a string - - - -#### version\_info - -```python -def version_info() -``` - -Return the Jukebox version as a tuple of three numbers - -If this is a development version, an identifier string will be appended after the third integer. - - - -# jukebox.cfghandler - -This module handles global and local configuration data - -The concept is that config handler is created and initialized once in the main thread:: - - cfg = get_handler('global') - load_yaml(cfg, 'filename.yaml') - -In all other modules (in potentially different threads) the same handler is obtained and used by:: - - cfg = get_handler('global') - -This eliminates the need to pass an effectively global configuration handler by parameters across the entire design. -Handlers are identified by their name (in the above example *global*) - -The function :func:`get_handler` is the main entry point to obtain a new or existing handler. - - - -## ConfigHandler Objects - -```python -class ConfigHandler() -``` - -The configuration handler class - -Don't instantiate directly. Always use :func:`get_handler`! - -**Threads:** - -All threads can read and write to the configuration data. -**Proper thread-safeness must be ensured** by the the thread modifying the data by acquiring the lock -Easiest and best way is to use the context handler:: - - with cfg: - cfg['key'] = 66 - cfg.setndefault('hello', value='world') - -For a single function call, this is done implicitly. In this case, there is no need -to explicitly acquire the lock. - -Alternatively, you can lock and release manually by using :func:`acquire` and :func:`release` -But be very sure to release the lock even in cases of errors an exceptions! -Else we have a deadlock. - -Reading may be done without acquiring a lock. But be aware that when reading multiple values without locking, another -thread may intervene and modify some values in between! So, locking is still recommended. - - - -#### loaded\_from - -```python -@property -def loaded_from() -> Optional[str] -``` - -Property to store filename from which the config was loaded - - - -#### get - -```python -def get(key, *, default=None) -``` - -Enforce keyword on default to avoid accidental misuse when actually getn is wanted - - - -#### setdefault - -```python -def setdefault(key, *, value) -``` - -Enforce keyword on default to avoid accidental misuse when actually setndefault is wanted - - - -#### getn - -```python -def getn(*keys, default=None) -``` - -Get the value at arbitrary hierarchy depth. Return ``default`` if key not present - -The *default* value is returned no matter at which hierarchy level the path aborts. -A hierarchy is considered as any type with a :func:`get` method. - - - -#### setn - -```python -def setn(*keys, value, hierarchy_type=None) -> None -``` - -Set the ``key: value`` pair at arbitrary hierarchy depth - -All non-existing hierarchy levels are created. - -**Arguments**: - -- `keys`: Key hierarchy path through the nested levels -- `value`: The value to set -- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type -is used - - - -#### setndefault - -```python -def setndefault(*keys, value, hierarchy_type=None) -``` - -Set the ``key: value`` pair at arbitrary hierarchy depth unless the key already exists - -All non-existing hierarchy levels are created. - -**Arguments**: - -- `keys`: Key hierarchy path through the nested levels -- `value`: The default value to set -- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type -is used - -**Returns**: - -The actual value or or the default value if key does not exit - - - -#### config\_dict - -```python -def config_dict(data) -``` - -Initialize configuration data from dict-like data structure - -**Arguments**: - -- `data`: configuration data - - - -#### is\_modified - -```python -def is_modified() -> bool -``` - -Check if the data has changed since the last load/store - -.. note: This relies on the *__str__* representation of the underlying data structure - In case of ruamel, this ignores comments and only looks at the data - - - -#### clear\_modified - -```python -def clear_modified() -> None -``` - -Sets the current state as new baseline, clearing the is_modified state - - - -#### save - -```python -def save(only_if_changed: bool = False) -> None -``` - -Save config back to the file it was loaded from - -If you want to save to a different file, use :func:`write_yaml`. - - - -#### load - -```python -def load(filename: str) -> None -``` - -Load YAML config file into memory - - - -#### get\_handler - -```python -def get_handler(name: str) -> ConfigHandler -``` - -Get a configuration data handler with the specified name, creating it - -if it doesn't yet exit. If created, it is always created empty. - -This is the main entry point for obtaining an configuration handler - -**Arguments**: - -- `name`: Name of the config handler - -**Returns**: - -`ConfigHandler`: The configuration data handler for *name* - - - -#### load\_yaml - -```python -def load_yaml(cfg: ConfigHandler, filename: str) -> None -``` - -Load a yaml file into a ConfigHandler - -**Arguments**: - -- `cfg`: ConfigHandler instance -- `filename`: filename to yaml file - -**Returns**: - -None - - - -#### write\_yaml - -```python -def write_yaml(cfg: ConfigHandler, - filename: str, - only_if_changed: bool = False, - *args, - **kwargs) -> None -``` - -Writes ConfigHandler data to yaml file / sys.stdout - -**Arguments**: - -- `cfg`: ConfigHandler instance -- `filename`: filename to output file. If *sys.stdout*, output is written to console -- `only_if_changed`: Write file only, if ConfigHandler.is_modified() -- `args`: passed on to yaml.dump(...) -- `kwargs`: passed on to yaml.dump(...) - -**Returns**: - -None - - - -# jukebox.playlistgenerator - -Playlists are build from directory content in the following way: -a directory is parsed and files are added to the playlist in the following way - -1. files are added in alphabetic order -2. files ending with ``*livestream.txt`` are unpacked and the containing URL(s) are added verbatim to the playlist -3. files ending with ``*podcast.txt`` are unpacked and the containing Podcast URL(s) are expanded and added to the playlist -4. files ending with ``*.m3u`` are treated as folder playlist. Regular folder processing is suspended and the playlist - is build solely from the ``*.m3u`` content. Only the alphabetically first ``*.m3u`` is processed. URLs are added verbatim - to the playlist except for ``*.xml`` and ``*.podcast`` URLS, which are expanded first - -An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. - -.. code-block:: bash - - 01-livestream.txt - 02-livestream.txt - music.mp3 - podcast.txt - -All files are treated as music files and are added to the playlist, except those: - - * starting with ``.``, - * not having a file ending, i.e. do not contain a ``.``, - * ending with ``.txt``, - * ending with ``.m3u``, - * ending with one of the excluded file endings in :attr:`PlaylistCollector._exclude_endings` - -In recursive mode, the playlist is generated by concatenating all sub-folder playlists. Sub-folders are parsed -in alphabetic order. Symbolic links are being followed. The above rules are enforced on a per-folder bases. -This means, one ``*.m3u`` file per sub-folder is processed (if present). - -In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. - - - -#### TYPE\_DECODE - -Types if file entires in parsed directory - - - -## PlaylistCollector Objects - -```python -class PlaylistCollector() -``` - -Build a playlist from directory(s) - -This class is intended to be used with an absolute path to the music library:: - - plc = PlaylistCollector('/home/chris/music') - plc.parse('Traumfaenger') - print(f"res = {plc}") - -But it can also be used with relative paths from current working directory:: - - plc = PlaylistCollector('.') - plc.parse('../../../../music/Traumfaenger') - print(f"res = {plc}") - -The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. -If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. - - - -#### \_\_init\_\_ - -```python -def __init__(music_library_base_path='/') -``` - -Initialize the playlist generator with music_library_base_path - -**Arguments**: - -- `music_library_base_path`: Base path the the music library. This is used to locate the file in the disk -but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir - - - -#### set\_exclusion\_endings - -```python -@classmethod -def set_exclusion_endings(cls, endings: List[str]) -``` - -Set the class-wide file ending exclusion list - -See :attr:`PlaylistCollector._exclude_endings` - - - -#### get\_directory\_content - -```python -def get_directory_content(path='.') -``` - -Parse the folder ``path`` and create a content list. Depth is always the current level - -**Arguments**: - -- `path`: Path to folder **relative** to ``music_library_base_path`` - -**Returns**: - -[ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] -where type is one of :attr:`TYPE_DECODE` - - - -#### parse - -```python -def parse(path='.', recursive=False) -``` - -Parse the folder ``path`` and create a playlist from it's content - -**Arguments**: - -- `path`: Path to folder **relative** to ``music_library_base_path`` -- `recursive`: Parse folder recursivley, or stay in top-level folder - - - -# jukebox.NvManager - - - -# jukebox.publishing - - - -#### get\_publisher - -```python -def get_publisher() -``` - -Return the publisher instance for this thread - -Per thread, only one publisher instance is required to connect to the inproc socket. -A new instance is created if it does not already exist. - -If there is a remote-chance that your function publishing something may be called form -different threads, always make a fresh call to ``get_publisher()`` to get the correct instance for the current thread. - -Example:: - -import jukebox.publishing as publishing - -class MyClass: -def __init__(self): -pass - -def say_hello(name): -publishing.get_publisher().send('hello', f'Hi {name}, howya?') - -To stress what **NOT** to do: don't get a publisher instance in the constructor and save it to ``self._pub``. -If you do and ``say_hello`` gets called from different threads, the publisher of the thread which instantiated the class -will be used. - -If you need your very own private Publisher Instance, you'll need to instantiate it yourself. -But: the use cases are very rare for that. I cannot think of one at the moment. - -**Remember**: Don’t share ZeroMQ sockets between threads. - - - -# jukebox.publishing.subscriber - - - -# jukebox.publishing.server - -Publishing Server -******************** - -The common publishing server for the entire Jukebox using ZeroMQ - -Structure ----------------- - -.. code-block:: text - -+-----------------------+ -| functional interface | Publisher -| | - functional interface for single Thread -| PUB | - sends data to publisher (and thus across threads) -+-----------------------+ -| (1) -v -+-----------------------+ -| SUB (bind) | PublishServer -| | - Last Value (LV) Cache -| XPUB (bind) | - Subscriber notification and LV resend -+-----------------------+ - independent thread -| (2) -v - -Connection (1): Internal connection -Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) - -Protocol: Multi-part message - -Part 1: Topic (in topic tree format) -E.g. player.status.elapsed - -Part 2: Payload or Message in json serialization -If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same - -Part 3: Command -Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer -and the message is not forwarded to the outside. This third part of the message is never forwarded - -Connection (2): External connection -Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! -Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will -also get you all the branch topics. To get everything, subscribe to ``b''`` - -Protocol: Multi-part message - -Part 1: Topic (in topic tree format) -E.g. player.status.elapsed - -Part 2: Payload or Message in json serialization -If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) - -Why? Why? -------------- - -Check out the `ZeroMQ Documentation `_ -for why you need a proxy in a good design. - -For use case, we made a few simplifications - -Design Rationales -------------------- - -* "If you need `millions of messages per second `_ -sent to thousands of points, -you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." -* "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then -the `XSUB and XPUB `_" -* "Let’s imagine `our feed has an average of 100,000 100-byte messages a second -`_ [...]. -While 100K messages a second is easy for a ZeroMQ application, ..." - -**But we have:** - -* few dozen subscribers --> Check! -* limited number of topics --> Check! -* max ~10 messages per second --> Check! -* small common state information --> Check! -* only the server updates the state --> Check! - -This means, we can use less complex patters than used for these high-speed, high code count, high data rate networks :-) - -* XPUB / XSUB to detect new subscriber -* Cache the entire state in the publisher -* Re-send the entire state on-demand (and then even to every subscriber) -* Using the same channel: sends state to every subscriber - -**Reliability considerations** - -* Late joining client (or drop-off and re-join): get full state update -* Server crash etc: No special handling necessary, we are simple -and don't need recovery in this case. Server will publish initial state -after re-start -* Subscriber too slow: Subscribers problem (TODO: Do we need to do anything about it?) - -**Start-up sequence:** - -* Publisher plugin is first plugin to be loaded -* Due to Publisher - PublisherServer structure no further sequencing required - -Plugin interactions and usage ------------------------------- - -RPC can trigger through function call in components/publishing plugin that - -* entire state is re-published (from the cache) -* a specific topic tree is re-published (from the cache) - -Plugins publishing state information should publish initial state at @plugin.finalize - -.. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is -required per thread. But the publisher instance **must** be thread-local! -Always go through :func:`publishing.get_publisher()`. - -**Sockets** - -Three sockets are opened: - -``. TCP (on a configurable port) -``. Websocket (on a configurable port) -``. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules -that want to know about the current state on event based updates. - -**Further ZeroMQ References:** - -* `Working with Messages `_ -* `Multiple Threads `_ - - - -## PublishServer Objects - -```python -class PublishServer(threading.Thread) -``` - -The publish proxy server that collects and caches messages from all internal publishers and -forwards them to the outside world - -Handles new subscriptions by sending out the entire cached state to **all** subscribers - -The code is structures using a `Reactor Pattern `_ - - - -#### run - -```python -def run() -``` - -Thread's activity - - - -#### handle\_message - -```python -def handle_message(msg) -``` - -Handle incoming messages - - - -#### handle\_subscription - -```python -def handle_subscription(msg) -``` - -Handle new subscribers - - - -## Publisher Objects - -```python -class Publisher() -``` - -The publisher that provides the functional interface to the application - -.. note:: - * An instance must not be shared across threads! - * One instance per thread is enough - - - -#### \_\_init\_\_ - -```python -def __init__(check_thread_owner=True) -``` - -**Arguments**: - -- `check_thread_owner`: Check if send() is always called from the correct thread. This is debug feature -and is intended to expose the situation before it leads to real trouble. Leave it on! - - - -#### send - -```python -def send(topic: str, payload) -``` - -Send out a message for topic - - - -#### revoke - -```python -def revoke(topic: str) -``` - -Revoke a single topic element (not a topic tree!) - - - -#### resend - -```python -def resend(topic: Optional[str] = None) -``` - -Instructs the PublishServer to resend current status to all subscribers - -Not necessary to call after incremental updates or new subscriptions - that will happen automatically! - - - -#### close\_server - -```python -def close_server() -``` - -Instructs the PublishServer to close itself down - - - -# jukebox.daemon - - - -#### log\_active\_threads - -```python -@atexit.register -def log_active_threads() -``` - -This functions is registered with atexit very early, meaning it will be run very late. It is the best guess to -evaluate which Threads are still running (and probably shouldn't be) - -This function is registered before all the plugins and their dependencies are loaded - - - -## JukeBox Objects - -```python -class JukeBox() -``` - - - -#### signal\_handler - -```python -def signal_handler(esignal, frame) -``` - -Signal handler for orderly shutdown - -On first Ctrl-C (or SIGTERM) orderly shutdown procedure is embarked upon. It gets allocated a time-out! -On third Ctrl-C (or SIGTERM), this is interrupted and there will be a hard exit! - - - -# jukebox.plugs - -A plugin package with some special functionality - -Plugins packages are python packages that are dynamically loaded. From these packages only a subset of objects is exposed -through the plugs.call interface. The python packages can use decorators or dynamic function call to register (callable) -objects. - -The python package name may be different from the name the package is registered under in plugs. This allows to load different -python packages for a specific feature based on a configuration file. Note: Python package are still loaded as regular -python packages and can be accessed by normal means - -If you want to provide additional functionality to the same feature (probably even for run-time switching) -you can implement a Factory Pattern using this package. Take a look at volume.py as an example. - -**Example:** Decorate a function for auto-registering under it's own name:: - -import jukebox.plugs as plugs -@plugs.register -def func1(param): -pass - -**Example:** Decorate a function for auto-registering under a new name:: - -@plugs.register(name='better_name') -def func2(param): -pass - -**Example:** Register a function during run-time under it's own name:: - -def func3(param): -pass -plugs.register(func3) - -**Example:** Register a function during run-time under a new name:: - -def func4(param): -pass -plugs.register(func4, name='other_name', package='other_package') - -**Example:** Decorate a class for auto registering during initialization, -including all methods (see _register_class for more info):: - -@plugs.register(auto_tag=True) -class MyClass1: -pass - -**Example:** Register a class instance, from which only report is a callable method through the plugs interface:: - -class MyClass2: -@plugs.tag -def report(self): -pass -myinst2 = MyClass2() -plugin.register(myinst2, name='myinst2') - -Naming convention: - -package -1. Either a python package -2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) - -plugin -1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) -2. The string name to above object - -name -The string name of the plugin object for registration - -method -1. In case the object is a class instance a bound method to call from the class instance -2. The string name to above object - - - -## PluginPackageClass Objects - -```python -class PluginPackageClass() -``` - -A local data class for holding all information about a loaded plugin package - - - -#### register - -```python -@overload -def register(plugin: Callable) -> Callable -``` - -1-level decorator around a function - - - -#### register - -```python -@overload -def register(plugin: Type) -> Any -``` - -Signature: 1-level decorator around a class - - - -#### register - -```python -@overload -def register(*, name: str, package: Optional[str] = None) -> Callable -``` - -Signature: 2-level decorator around a function - - - -#### register - -```python -@overload -def register(*, auto_tag: bool = False, package: Optional[str] = None) -> Type -``` - -Signature: 2-level decorator around a class - - - -#### register - -```python -@overload -def register(plugin: Callable[..., Any] = None, - *, - name: Optional[str] = None, - package: Optional[str] = None, - replace: bool = False) -> Callable -``` - -Signature: Run-time registration of function / class instance / bound method - - - -#### register - -```python -def register(plugin: Optional[Callable] = None, - *, - name: Optional[str] = None, - package: Optional[str] = None, - replace: bool = False, - auto_tag: bool = False) -> Callable -``` - -A generic decorator / run-time function to register plugin module callables - -The functions comes in five distinct signatures for 5 use cases: - -1. ``@plugs.register``: decorator for a class w/o any arguments -2. ``@plugs.register``: decorator for a function w/o any arguments -3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments -4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments -5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of - - * function - * bound method - * class instance - -For more documentation see the functions - - * :func:`_register_obj` - * :func:`_register_class` - -See the examples in Module :mod:`plugs` how to use this decorator / function - -**Arguments**: - -- `plugin`: -- `name`: -- `package`: -- `replace`: -- `auto_tag`: - - - -#### tag - -```python -def tag(func: Callable) -> Callable -``` - -Method decorator for tagging a method as callable through the plugs interface - -Note that the instantiated class must still be registered as plugin object -(either with the class decorator or dynamically) - -**Arguments**: - -- `func`: function to decorate - -**Returns**: - -the function - - - -#### initialize - -```python -def initialize(func: Callable) -> Callable -``` - -Decorator for functions that shall be called by the plugs package directly after the module is loaded - -**Arguments**: - -- `func`: Function to decorate - -**Returns**: - -The function itself - - - -#### finalize - -```python -def finalize(func: Callable) -> Callable -``` - -Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded - -**Arguments**: - -- `func`: Function to decorate - -**Returns**: - -The function itself - - - -#### atexit - -```python -def atexit(func: Callable[[int], Any]) -> Callable[[int], Any] -``` - -Decorator for functions that shall be called by the plugs package directly after at exit of program. - -.. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called - during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your - shutdown handler. - -The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) -It is intended for passing down the signal number that initiated the program termination - -**Arguments**: - -- `func`: Function to decorate - -**Returns**: - -The function itself - - - -#### load - -```python -def load(package: str, - load_as: Optional[str] = None, - prefix: Optional[str] = None) -``` - -Loads a python package as plugin package - -Executes a regular python package load. That means a potentially existing __init__.py is executed. -Decorator @register can by used to register functions / classes / class istances as plugin callable -Decorator @initializer can be used to tag functions that shall be called after package loading -Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded -Instead of using @initializer, you may of course use __init__.py - -Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under -which they are loaded as plugin package also. - -**Arguments**: - -- `package`: Python package to load as plugin package -- `load_as`: Plugin package registration name. If None the name is the python's package simple name -- `prefix`: Prefix to python package to create fully qualified name. This is used only to locate the python package -and ignored otherwise. Useful if all the plugin module are in a dedicated folder - - - -#### load\_all\_named - -```python -def load_all_named(packages_named: Mapping[str, str], - prefix: Optional[str] = None, - ignore_errors=False) -``` - -Load all packages in packages_named with mapped names - -**Arguments**: - -- `packages_named`: Dict[load_as, package] - - - -#### load\_all\_unnamed - -```python -def load_all_unnamed(packages_unnamed: Iterable[str], - prefix: Optional[str] = None, - ignore_errors=False) -``` - -Load all packages in packages_unnamed with default names - - - -#### load\_all\_finalize - -```python -def load_all_finalize(ignore_errors=False) -``` - -Calls all functions registered with @finalize from all loaded modules in the order they were loaded - -This must be executed after the last plugin package is loaded - - - -#### close\_down - -```python -def close_down(**kwargs) -> Any -``` - -Calls all functions registered with @atexit from all loaded modules in reverse order of module load order - -Modules are processed in reverse order. Several at-exit tagged functions of a single module are processed -in the order of registration. - -Errors raised in functions are suppressed to ensure all plugins are processed - - - - -#### call - -```python -def call(package: str, - plugin: str, - method: Optional[str] = None, - *, - args=(), - kwargs=None, - as_thread: bool = False, - thread_name: Optional[str] = None) -> Any -``` - -Call a function/method from the loaded plugins - -If a plugin is a function or a callable instance of a class, this is equivalent to - -``package.plugin(*args, **kwargs)`` - -If plugin is a class instance from which a method is called, this is equivalent to the followig. -Also remember, that method must have the attribute ``plugin_callable = True`` - -``package.plugin.method(*args, **kwargs)`` - -Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. - -.. note:: - There is no logger in this function as they all belong up-level where the exceptions are handled. - If you want logger messages instead of exceptions, use :func:`call_ignore_errors` - -**Arguments**: - -- `package`: Name of the plugin package in which to look for function/class instance -- `plugin`: Function name or instance name of a class -- `method`: Method name when accessing a class instance' method. Leave at *None* if unneeded. -- `as_thread`: Run the callable in separate daemon thread. -There is no return value from the callable in this case! The return value is the thread object. -Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. -All threads are started as daemon threads with terminate upon main program termination. -There is not stop-thread mechanism. This is intended for short lived threads. -- `thread_name`: Name of the thread -- `args`: Arguments passed to callable -- `kwargs`: Keyword arguments passed to callable - -**Returns**: - -The return value from the called function, or, if started as thread the thread object - - - -#### call\_ignore\_errors - -```python -def call_ignore_errors(package: str, - plugin: str, - method: Optional[str] = None, - *, - args=(), - kwargs=None, - as_thread: bool = False, - thread_name: Optional[str] = None) -> Any -``` - -Call a function/method from the loaded plugins ignoring all raised Exceptions. - -Errors get logged. - -See :func:`call` for parameter documentation. - - - -#### exists - -```python -def exists(package: str, - plugin: Optional[str] = None, - method: Optional[str] = None) -> bool -``` - -Check if an object is registered within the plugs package - - - -#### get - -```python -def get(package: str, - plugin: Optional[str] = None, - method: Optional[str] = None) -> Any -``` - -Get a plugs-package registered object - -The return object depends on the number of parameters - -* 1 argument: Get the python module reference for the plugs *package* -* 2 arguments: Get the plugin reference for the plugs *package.plugin* -* 3 arguments: Get the plugin reference for the plugs *package.plugin.method* - - - -#### loaded\_as - -```python -def loaded_as(module_name: str) -> str -``` - -Return the plugin name a python module is loaded as - - - -#### delete - -```python -def delete(package: str, plugin: Optional[str] = None, ignore_errors=False) -``` - -Delete a plugin object from the registered plugs callables - -Note: This does not 'unload' the python module. It merely makes it un-callable via plugs! - - - -#### dump\_plugins - -```python -def dump_plugins(stream) -``` - -Write a human readable summary of all plugin callables to stream - - - -#### summarize - -```python -def summarize() -``` - -Create a reference summary of all plugin callables in dictionary format - - - -#### generate\_help\_rst - -```python -def generate_help_rst(stream) -``` - -Write a reference of all plugin callables in Restructured Text format - - - -#### get\_all\_loaded\_packages - -```python -def get_all_loaded_packages() -> Dict[str, str] -``` - -Report a short summary of all loaded packages - -**Returns**: - -Dictionary of the form `{loaded_as: loaded_from, ...}` - - - -#### get\_all\_failed\_packages - -```python -def get_all_failed_packages() -> Dict[str, str] -``` - -Report those packages that did not load error free - -.. note:: Package could fail to load - - 1. altogether: these package are not registered - 2. partially: during initializer, finalizer functions: The package is loaded, - but the function did not execute error-free - - Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED - -**Returns**: - -Dictionary of the form `{loaded_as: loaded_from, ...}` - - - -# jukebox.speaking\_text - -Text to Speech. Plugin to speak any given text via speaker - - - -# jukebox.multitimer - -Multitimer Module - - - -## MultiTimer Objects - -```python -class MultiTimer(threading.Thread) -``` - -Call a function after a specified number of seconds, repeat that iteration times - -May be cancelled during any of the wait times. -Function is called with keyword parameter 'iteration' (which decreases down to 0 for the last iteration) - -If iterations is negative, an endlessly repeating timer is created (which needs to be cancelled with cancel()) - -Initiates start and publishing by calling self.publish_callback - -Note: Inspired by threading.Timer and generally using the same API - - - -#### cancel - -```python -def cancel() -``` - -Stop the timer if it hasn't finished all iterations yet. - - - -## GenericTimerClass Objects - -```python -class GenericTimerClass() -``` - -Interface for plugin / RPC accessibility for a single event timer - - - -#### \_\_init\_\_ - -```python -def __init__(name, wait_seconds: float, function, args=None, kwargs=None) -``` - -**Arguments**: - -- `wait_seconds`: The time in seconds to wait before calling function -- `function`: The function to call with args and kwargs. -- `args`: Parameters for function call -- `kwargs`: Parameters for function call - - - -#### start - -```python -@plugin.tag -def start(wait_seconds=None) -``` - -Start the timer (with default or new parameters) - - - -#### cancel - -```python -@plugin.tag -def cancel() -``` - -Cancel the timer - - - -#### toggle - -```python -@plugin.tag -def toggle() -``` - -Toggle the activation of the timer - - - -#### trigger - -```python -@plugin.tag -def trigger() -``` - -Trigger the next target execution before the time is up - - - -#### is\_alive - -```python -@plugin.tag -def is_alive() -``` - -Check if timer is active - - - -#### get\_timeout - -```python -@plugin.tag -def get_timeout() -``` - -Get the configured time-out - -**Returns**: - -The total wait time. (Not the remaining wait time!) - - - -#### set\_timeout - -```python -@plugin.tag -def set_timeout(wait_seconds: float) -``` - -Set a new time-out in seconds. Re-starts the timer if already running! - - - -#### publish - -```python -@plugin.tag -def publish() -``` - -Publish the current state and config - - - -#### get\_state - -```python -@plugin.tag -def get_state() -``` - -Get the current state and config as dictionary - - - -## GenericEndlessTimerClass Objects - -```python -class GenericEndlessTimerClass(GenericTimerClass) -``` - -Interface for plugin / RPC accessibility for an event timer call function endlessly every m seconds - - - -## GenericMultiTimerClass Objects - -```python -class GenericMultiTimerClass(GenericTimerClass) -``` - -Interface for plugin / RPC accessibility for an event timer that performs an action n times every m seconds - - - -#### \_\_init\_\_ - -```python -def __init__(name, - iterations: int, - wait_seconds_per_iteration: float, - callee, - args=None, - kwargs=None) -``` - -**Arguments**: - -- `iterations`: Number of times callee is called -- `wait_seconds_per_iteration`: Wait in seconds before each iteration -- `callee`: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). -Then with every time out iteration __call__(*args, iteration=iteration, **kwargs) is called. -'iteration' is the current iteration count in decreasing order! -- `args`: -- `kwargs`: - - - -#### start - -```python -@plugin.tag -def start(iterations=None, wait_seconds_per_iteration=None) -``` - -Start the timer (with default or new parameters) - - - -# jukebox.utils - -Common utility functions - - - -#### decode\_rpc\_call - -```python -def decode_rpc_call(cfg_rpc_call: Dict) -> Optional[Dict] -``` - -Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. - -.. important: Leaves all other parameters in cfg_action untouched or later downstream processing! - -**Arguments**: - -- `cfg_rpc_call`: RPC command as configuration entry - -**Returns**: - -A fully populated deep copy of cfg_rpc_call - - - -#### decode\_rpc\_command - -```python -def decode_rpc_command(cfg_rpc_cmd: Dict, - logger: logging.Logger = log) -> Optional[Dict] -``` - -Decode an RPC Command from a config entry. - -This means - - * Decode RPC command alias (if present) - * Ensure all RPC call parameters have valid default values - -If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call -which emits a misuse warning when called -If an explicitly specified this is not done. However, it is ensured that the returned -dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling -for non-existing RPC commands and we get a clearer error message. - -**Arguments**: - -- `cfg_rpc_cmd`: RPC command as configuration entry -- `logger`: The logger to use - -**Returns**: - -A decoded, fully populated deep copy of cfg_rpc_cmd - - - -#### decode\_and\_call\_rpc\_command - -```python -def decode_and_call_rpc_command(rpc_cmd: Dict, logger: logging.Logger = log) -``` - -Convenience function combining decode_rpc_command and plugs.call_ignore_errors - - - -#### bind\_rpc\_command - -```python -def bind_rpc_command(cfg_rpc_cmd: Dict, - dereference=False, - logger: logging.Logger = log) -``` - -Decode an RPC command configuration entry and bind it to a function - -**Arguments**: - -- `dereference`: Dereference even the call to plugs.call(...) - ``. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with - all checks applied at bind time - ``. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with - all checks applied at bind time. - -Setting deference to True, circumvents the dynamic nature of the plugins: the function to call - must exist at bind time and cannot change. If False, the function to call must only exist at call time. - This can be important during the initialization where package ordering and initialization means that not all - classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls - is circumvented. Use with care! - -**Returns**: - -Callable function w/o parameters which directly runs the RPC command -using plugs.call_ignore_errors - - - -#### rpc\_call\_to\_str - -```python -def rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) -> str -``` - -Return a readable string of an RPC call config - -**Arguments**: - -- `cfg_rpc_call`: RPC call configuration entry -- `with_args`: Return string shall include the arguments of the function - - - -#### generate\_cmd\_alias\_rst - -```python -def generate_cmd_alias_rst(stream) -``` - -Write a reference of all rpc command aliases in Restructured Text format - - - -#### generate\_cmd\_alias\_reference - -```python -def generate_cmd_alias_reference(stream) -``` - -Write a reference of all rpc command aliases in text format - - - -#### get\_git\_state - -```python -def get_git_state() -``` - -Return git state information for the current branch - - - -# jukebox.rpc - - - -# jukebox.rpc.client - - - -# jukebox.rpc.server - -Remote Procedure Call Server (RPC) -************************************* - -Bind to tcp and/or websocket port and translates incoming requests to procedure calls. -Avaiable procedures to call are all functions registered with the plugin package. - -To protocol is loosely based on `jsonrpc `_ - -But with different elements directly relating to the plugin concept and Python function argument options - -.. code-block:: yaml - - { - 'package' : str # The plugin package loaded from python module - 'plugin' : str # The plugin object to be accessed from the package - # (i.e. function or class instance) - 'method' : str # (optional) The method of the class instance - 'args' : [ ] # (optional) Positional arguments as list - 'kwargs' : { } # (optional) Keyword arguments as dictionary - 'as_thread': bool # (optional) start call in separate thread - 'id' : Any # (optional) Round-trip id for response (may not be None) - 'tsp' : Any # (optional) measure and return total processing time for - # the call request (may not be None) - } - -**Response** - -A response will ALWAYS be send, independent of presence of 'id'. This is in difference to the -jsonrpc specification. But this is a ZeroMQB REQ/REP pattern requirement! - -If 'id' is omitted, the response will be 'None'! Unless an error occurred, then the error is returned. -The absence of 'id' indicates that the requester is not interested in the response. -If present, 'id' and 'tsp' may not be None. If they are None, there are treated as if non-existing. - -**Sockets** - -Three sockets are opened - -``. TCP (on a configurable port) -``. Websocket (on a configurable port) -``. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be - call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though - the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which - button triggers what action) - - - -## RpcServer Objects - -```python -class RpcServer() -``` - -The RPC Server Class - - - -#### \_\_init\_\_ - -```python -def __init__(context=None) -``` - -Initialize the connections and bind to the ports - - - -#### run - -```python -def run() -``` - -The main endless loop waiting for requests and forwarding the -call request to the plugin module - From 6048444bd2eb9daa7b624b264d2cd2cc9b9d7f81 Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 21:25:04 +0100 Subject: [PATCH 11/41] rename script and add to pre-commit hook --- .githooks/pre-commit | 14 +++++++++++++- run_docstring2markdown.sh => run_docgeneration.sh | 0 2 files changed, 13 insertions(+), 1 deletion(-) rename run_docstring2markdown.sh => run_docgeneration.sh (100%) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 95901bcda..b1b0c0348 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -7,7 +7,7 @@ # Checks # - flake8 on staged python files # Note: This only checks the modified files -# - docs build of if any python file or any doc file is staged +# - docs build of if any python file is staged # Note: This builds the entire documentation if a changed file goes into the documentation # # If there are problem with this script, commit may still be done with @@ -28,6 +28,18 @@ fi code=$(( flake8_code )) +doc_code=0 +if [[ -n $PY_FILES ]]; then + echo -e "\n**************************************************************" + echo -e "Modified Python source files. Generation markdown docs from docstring ... \n" + echo -e "**************************************************************\n" + ./run_docgeneration.sh -c + doc_code=$? + echo "pydoc_markdown return code: $doc_code" +fi + +code=$(( flake8_code + doc_code )) + if [[ code -gt 0 ]]; then echo -e "\n**************************************************************" echo -e "ERROR(s) during pre-commit checks. Aborting commit!" diff --git a/run_docstring2markdown.sh b/run_docgeneration.sh similarity index 100% rename from run_docstring2markdown.sh rename to run_docgeneration.sh From 37131751b34bf3e55057ab6de7575a461ace9edf Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 21:26:06 +0100 Subject: [PATCH 12/41] change py file to test pre commit hook --- src/jukebox/jukebox/plugs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index afad5da28..a3418fe04 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -72,6 +72,7 @@ def report(self): 1. In case the object is a class instance a bound method to call from the class instance 2. The string name to above object + """ import importlib From 70ff0890ce5a2afbcc16bce44768c4441653faa0 Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 21:28:55 +0100 Subject: [PATCH 13/41] modify py file again --- src/jukebox/jukebox/plugs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index a3418fe04..afad5da28 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -72,7 +72,6 @@ def report(self): 1. In case the object is a class instance a bound method to call from the class instance 2. The string name to above object - """ import importlib From 75205145c1b184fbe4da4d7c1c62d044ae02336c Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 21:43:19 +0100 Subject: [PATCH 14/41] test markdown formatting --- run_docgeneration.sh | 2 +- src/jukebox/jukebox/plugs.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/run_docgeneration.sh b/run_docgeneration.sh index 444324676..4c2324d07 100755 --- a/run_docgeneration.sh +++ b/run_docgeneration.sh @@ -12,4 +12,4 @@ cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && e # make sure, directory exists mkdir -p ./documentation/developers/docstring # expects pydoc-markdown.yml at working dir -pydoc-markdown \ No newline at end of file +pydoc-markdown diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index afad5da28..bc3c3e441 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -16,10 +16,12 @@ **Example:** Decorate a function for auto-registering under it's own name:: - import jukebox.plugs as plugs - @plugs.register - def func1(param): - pass +```python +import jukebox.plugs as plugs +@plugs.register +def func1(param): + pass +``` **Example:** Decorate a function for auto-registering under a new name:: From bf5448056b7c35838817e7e6fd9c08484579ac6c Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 21:44:46 +0100 Subject: [PATCH 15/41] updated docstring --- documentation/developers/docstring/docstring-api.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index b2184d060..f553f1f27 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -4670,10 +4670,6 @@ you can implement a Factory Pattern using this package. Take a look at volume.py **Example:** Decorate a function for auto-registering under it's own name:: -import jukebox.plugs as plugs -@plugs.register -def func1(param): -pass **Example:** Decorate a function for auto-registering under a new name:: @@ -4725,6 +4721,12 @@ The string name of the plugin object for registration method 1. In case the object is a class instance a bound method to call from the class instance 2. The string name to above object +```python +import jukebox.plugs as plugs +@plugs.register +def func1(param): + pass +``` From 535b0a76fc6f1013c4c3c8fbe05c96b2d38b7c8e Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 22:43:19 +0100 Subject: [PATCH 16/41] use sphinx renderer --- .../developers/docstring/docstring-api.md | 559 +++++++++++++----- pydoc-markdown.yml | 2 +- src/jukebox/jukebox/plugs.py | 12 +- 3 files changed, 411 insertions(+), 162 deletions(-) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index f553f1f27..52e632bc0 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -395,6 +395,7 @@ For debugging, it is usually desirable to run the Jukebox directly from the cons as service. This gives direct logging info in the console and allows changing command line parameters. See :ref:`userguide/troubleshooting:Troubleshooting`. + # \_\_init\_\_ @@ -411,6 +412,7 @@ the settings. For more information see :ref:`rfid/rfid:RFID Readers`. .. note:: This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). Any manual modifications to the settings will have to be re-applied + # run\_rpc\_tool @@ -428,6 +430,7 @@ The list of available commands is fetched from the running Jukebox service. .. todo: - kwargs support + #### get\_common\_beginning @@ -438,6 +441,7 @@ def get_common_beginning(strings) Return the strings that are common to the beginning of each string in the strings list. + # run\_configure\_audio @@ -449,14 +453,17 @@ Will also setup equalizer and mono down mixer in the pulseaudio config file. Run this once after installation. Can be re-run at any time to change the settings. For more information see :ref:`userguide/audio:Audio Configuration`. + # run\_publicity\_sniffer A command line tool that monitors all messages being sent out from the + Jukebox via the publishing interface. Received messages are printed in the console. Mainly used for debugging. + # misc @@ -476,6 +483,7 @@ flags from the stats module. Reference: https://docs.python.org/3/library/os.html#os.chmod + #### flatten @@ -486,6 +494,7 @@ def flatten(iterable) Flatten all levels of hierarchy in nested iterables + #### getattr\_hierarchical @@ -496,6 +505,7 @@ def getattr_hierarchical(obj: Any, name: str) -> Any Like the builtin getattr, but descends though the hierarchy levels + # misc.inputminus @@ -505,6 +515,7 @@ Zero 3rd-party dependency module for user prompting Yes, there are modules out there to do the same and they have more features. However, this is low-complexity and has zero dependencies + #### input\_int @@ -564,6 +575,7 @@ boolean value read from user input # misc.loggingext ############## + Logger ############## We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. @@ -573,25 +585,6 @@ level below 'jb'. It will inherit settings from it's parent logger unless otherw Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be created on the spot. -:Example: How to get logger and log away at your heart's content: ->>> import logging ->>> logger = logging.getLogger('jb.awesome_module') ->>> logger.info('Started general awesomeness aura') - -Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: -`` -loggers: -jb: -level: WARNING -handlers: [console, debug_file_handler, error_file_handler] -propagate: no -jb.awesome_module: -level: DEBUG -`` - -.. note:: -The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense) -There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output @@ -611,6 +604,7 @@ for the levelname depending on severity. Don't forget to {reset} the color settings at the end of the string. + #### \_\_init\_\_ @@ -633,6 +627,7 @@ class PubStream() ``` " + Stream handler wrapper around the publisher for logging.StreamHandler Allows logging to send all log information (based on logging configuration) @@ -648,6 +643,7 @@ Recursions come up when IMPORTANT: To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the functions in the send-function stack! + ## PubStreamHandler Objects @@ -663,6 +659,7 @@ can be configured (which are automatically instantiated). Using this Handler, we can output to PubStream whithout support code to instantiate PubStream keeping this file generic + # misc.simplecolors @@ -672,6 +669,7 @@ Zero 3rd-party dependency module to add colors to unix terminal output Yes, there are modules out there to do the same and they have more features. However, this is low-complexity and has zero dependencies + ## Colors Objects @@ -682,6 +680,7 @@ class Colors() Container class for all the colors as constants + #### resolve @@ -717,6 +716,7 @@ Drop-in replacement for print with color choice and auto color reset for conveni Use just as a regular print function, but with first parameter as color + # components @@ -735,6 +735,7 @@ class PlayContentCallbacks(Generic[STATE], CallbackHandler) Callbacks are executed in various play functions + #### register @@ -762,7 +763,7 @@ Callback signature is def run_callbacks(folder: str, state: STATE) ``` -:meta private: + @@ -772,42 +773,42 @@ Package for interfacing with the MPD Music Player Daemon Status information in three topics 1) Player Status: published only on change -This is a subset of the MPD status (and not the full MPD status) ?? -- folder -- song -- volume (volume is published only via player status, and not separatly to avoid too many Threads) -- ... + This is a subset of the MPD status (and not the full MPD status) ?? + - folder + - song + - volume (volume is published only via player status, and not separatly to avoid too many Threads) + - ... 2) Elapsed time: published every 250 ms, unless constant -- elapsed + - elapsed 3) Folder Config: published only on change -This belongs to the folder being played -Publish: -- random, resume, single, loop -On save store this information: -Contains the information for resume functionality of each folder -- random, resume, single, loop -- if resume: -- current song, elapsed -- what is PLAYSTATUS for? -When to save -- on stop -Angstsave: -- on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) -- on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) -Load checks: -- if resume, but no song, elapsed -> log error and start from the beginning + This belongs to the folder being played + Publish: + - random, resume, single, loop + On save store this information: + Contains the information for resume functionality of each folder + - random, resume, single, loop + - if resume: + - current song, elapsed + - what is PLAYSTATUS for? + When to save + - on stop + Angstsave: + - on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) + - on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) + Load checks: + - if resume, but no song, elapsed -> log error and start from the beginning Status storing: -- Folder config for each folder (see above) -- Information to restart last folder playback, which is: -- last_folder -> folder_on_close -- song, elapsed -- random, resume, single, loop -- if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! -on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card + - Folder config for each folder (see above) + - Information to restart last folder playback, which is: + - last_folder -> folder_on_close + - song, elapsed + - random, resume, single, loop + - if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! + on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card Internal status -- last played folder: Needed to detect second swipe + - last played folder: Needed to detect second swipe Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, @@ -815,13 +816,13 @@ Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CUR {'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, 'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} -**References**: +References: +https://github.com/Mic92/python-mpd2 +https://python-mpd2.readthedocs.io/en/latest/topics/commands.html +https://mpd.readthedocs.io/en/latest/protocol.html + +sudo -u mpd speaker-test -t wav -c 2 - https://github.com/Mic92/python-mpd2 - https://python-mpd2.readthedocs.io/en/latest/topics/commands.html - https://mpd.readthedocs.io/en/latest/protocol.html - - sudo -u mpd speaker-test -t wav -c 2 @@ -833,6 +834,7 @@ class PlayerMPD() Interface to MPD Music Player Daemon + #### mpd\_retry\_with\_mutex @@ -842,11 +844,13 @@ def mpd_retry_with_mutex(mpd_cmd, *args) ``` This method adds thread saftey for acceses to mpd via a mutex lock, + it shall be used for each access to mpd to ensure thread safety In case of a communication error the connection will be reestablished and the pending command will be repeated 2 times I think this should be refactored to a decorator + #### pause @@ -861,6 +865,7 @@ Enforce pause to state (1: pause, 0: resume) This is what you want as card removal action: pause the playback, so it can be resumed when card is placed on the reader again. What happens on re-placement depends on configured second swipe option + #### next @@ -872,6 +877,7 @@ def next() Play next track in current playlist + #### rewind @@ -885,6 +891,7 @@ Re-start current playlist from first track Note: Will not re-read folder config, but leave settings untouched + #### replay @@ -898,6 +905,7 @@ Re-start playing the last-played folder Will reset settings to folder config + #### toggle @@ -909,6 +917,7 @@ def toggle() Toggle pause state, i.e. do a pause / resume depending on current state + #### replay\_if\_stopped @@ -923,6 +932,7 @@ Re-start playing the last-played folder unless playlist is still playing .. note:: To me this seems much like the behaviour of play, but we keep it as it is specifically implemented in box 2.X + #### play\_card @@ -1010,6 +1020,7 @@ Get the current volume For volume control do not use directly, but use through the plugin 'volume', as the user may have configured a volume control manager other than MPD + #### set\_volume @@ -1023,16 +1034,19 @@ Set the volume For volume control do not use directly, but use through the plugin 'volume', as the user may have configured a volume control manager other than MPD + #### play\_card\_callbacks Callback handler instance for play_card events. + - is executed when play_card function is called States: - See :class:`PlayCardState` See :class:`PlayContentCallbacks` + # components.rpc\_command\_alias @@ -1041,6 +1055,7 @@ This file provides definitions for RPC command aliases See :ref:`userguide/rpc_commands` + # components.synchronisation.rfidcards @@ -1060,6 +1075,7 @@ This is done to allow to play audio offline. Otherwise we would also update other cardIds where the audiofolders have not been synced yet. The local copy is kept to reduce unnecessary syncing. + ## SyncRfidcards Objects @@ -1070,6 +1086,7 @@ class SyncRfidcards() Control class for sync RFID cards functionality + #### sync\_change\_on\_rfid\_scan @@ -1095,8 +1112,10 @@ def sync_all() -> bool ``` Sync all audiofolder and cardids from the remote server. + Removes local entries not existing at the remote server. + #### sync\_card\_database @@ -1199,6 +1218,7 @@ The following callbacks are provided. Register callbacks with these adder functi ``. :func:`add_on_output_change_callbacks` ``. :func:`add_on_volume_change_callback` + ## PulseMonitor Objects @@ -1217,6 +1237,7 @@ The context manager also locks the module to ensure proper thread sequencing, as only a single thread may work with pulsectl at any time. Currently, an RLock is used, even if it may not be necessary + ## SoundCardConnectCallbacks Objects @@ -1227,7 +1248,8 @@ class SoundCardConnectCallbacks(CallbackHandler) Callbacks are executed when - * new sound card gets connected +* new sound card gets connected + @@ -1258,7 +1280,7 @@ Callback signature is def run_callbacks(sink_name, alias, sink_index, error_state) ``` -:meta private: + @@ -1270,6 +1292,7 @@ def toggle_on_connect() ``` Returns :data:`True` if the sound card shall be changed when a new card connects/disconnects. Setting this + property changes the behavior. .. note:: A new card is always assumed to be the secondary device from the audio configuration. @@ -1277,6 +1300,7 @@ property changes the behavior. device connection will initiate the toggle. This, however, is no real issue as the RPi's audio system will be relatively stable once setup + #### toggle\_on\_connect @@ -1288,6 +1312,7 @@ def toggle_on_connect(state=True) Toggle Doc 2 + #### stop @@ -1298,6 +1323,7 @@ def stop() Stop the pulse monitor thread + #### run @@ -1308,6 +1334,7 @@ def run() -> None Starts the pulse monitor thread + ## PulseVolumeControl Objects @@ -1329,6 +1356,7 @@ state. Which is ensured by the context manager All private functions starting with `_function_name` assume that this is ensured by the calling function. All user functions acquire proper context! + ## OutputChangeCallbackHandler Objects @@ -1339,7 +1367,8 @@ class OutputChangeCallbackHandler(CallbackHandler) Callbacks are executed when - * audio sink is changed +* audio sink is changed + @@ -1375,7 +1404,7 @@ Callback signature is def run_callbacks(sink_name, alias, sink_index, error_state) ``` -:meta private: + @@ -1387,7 +1416,8 @@ class OutputVolumeCallbackHandler(CallbackHandler) Callbacks are executed when - * audio volume level is changed +* audio volume level is changed + @@ -1417,7 +1447,7 @@ Callback signature is def run_callbacks(sink_name, alias, sink_index, error_state) ``` -:meta private: + @@ -1430,6 +1460,7 @@ def toggle_output() Toggle the audio output sink + #### get\_outputs @@ -1441,6 +1472,7 @@ def get_outputs() Get current output and list of outputs + #### publish\_volume @@ -1452,6 +1484,7 @@ def publish_volume() Publish (volume, mute) + #### publish\_outputs @@ -1463,6 +1496,7 @@ def publish_outputs() Publish current output and list of outputs + #### set\_volume @@ -1474,6 +1508,7 @@ def set_volume(volume: int) Set the volume (0-100) for the currently active output + #### get\_volume @@ -1485,6 +1520,7 @@ def get_volume() Get the volume + #### change\_volume @@ -1496,6 +1532,7 @@ def change_volume(step: int) Increase/decrease the volume by step for the currently active output + #### get\_mute @@ -1507,6 +1544,7 @@ def get_mute() Return mute status for the currently active output + #### mute @@ -1518,6 +1556,7 @@ def mute(mute=True) Set mute status for the currently active output + #### set\_output @@ -1529,6 +1568,7 @@ def set_output(sink_index: int) Set the active output (sink_index = 0: primary, 1: secondary) + #### set\_soft\_max\_volume @@ -1540,6 +1580,7 @@ def set_soft_max_volume(max_volume: int) Limit the maximum volume to max_volume for the currently active output + #### get\_soft\_max\_volume @@ -1551,6 +1592,7 @@ def get_soft_max_volume() Return the maximum volume limit for the currently active output + #### card\_list @@ -1561,6 +1603,7 @@ def card_list() -> List[pulsectl.PulseCardInfo] Return the list of present sound card + # components.rfid @@ -1579,6 +1622,7 @@ class RfidCardDetectCallbacks(CallbackHandler) Callbacks are executed if rfid card is detected + #### register @@ -1606,15 +1650,17 @@ Callback signature is def run_callbacks(card_id: str, state: RfidCardDetectState) ``` -:meta private: + #### rfid\_card\_detect\_callbacks Callback handler instance for rfid_card_detect_callbacks events. + See :class:`RfidCardDetectCallbacks` + ## CardRemovalTimerClass Objects @@ -1625,6 +1671,7 @@ class CardRemovalTimerClass(threading.Thread) A timer watchdog thread that calls timeout_action on time-out + #### \_\_init\_\_ @@ -1754,6 +1801,7 @@ Write configuration to config_file Add GPIO input devices and output devices to the RFID Mock Reader GUI + #### create\_inputs @@ -1782,6 +1830,7 @@ def set_state(value, box_state_var) Change the value of a checkbox state variable + #### que\_set\_state @@ -1792,6 +1841,7 @@ def que_set_state(value, box_state_var) Queue the action to change a checkbox state variable to the TK GUI main thread + #### fix\_state @@ -1802,6 +1852,7 @@ def fix_state(box_state_var) Prevent a checkbox state variable to change on checkbox mouse press + #### pbox\_set\_state @@ -1812,6 +1863,7 @@ def pbox_set_state(value, pbox_state_var, label_var) Update progress bar state and related state label + #### que\_set\_pbox @@ -1822,6 +1874,7 @@ def que_set_pbox(value, pbox_state_var, label_var) Queue the action to change the progress bar state to the TK GUI main thread + #### create\_outputs @@ -1878,6 +1931,7 @@ def decode(raw_card_id: bytearray, number_format: int) -> str Decode the RDM6300 data format into actual card ID + # components.rfid.hardware.rdm6300\_serial.description @@ -1887,9 +1941,11 @@ Decode the RDM6300 data format into actual card ID # components.rfid.hardware.template\_new\_reader.description Provide a short title for this reader. + This is what that user will see when asked for selecting his RFID reader So, be precise but readable. Precise means 40 characters or less + # components.rfid.hardware.template\_new\_reader.template\_new\_reader @@ -1909,6 +1965,7 @@ It must return all configuration parameters that are necessary to later use the You can ask the user for selections and choices. And/or provide default values. If your reader requires absolutely no configuration return {} + ## ReaderClass Objects @@ -1934,6 +1991,7 @@ Put your code into these functions (see below for more information) - cleanup - stop + #### \_\_init\_\_ @@ -1948,6 +2006,7 @@ As you are dealing directly with potentially user-manipulated config information advisable to do some sanity checks and give useful error messages. Even if you cannot recover gracefully, a good error message helps :-) + #### cleanup @@ -1962,6 +2021,7 @@ Put all your cleanup code here, e.g. if you are using the serial bus or GPIO pin Will be called implicitly via the __exit__ function This function must exist! If there is nothing to do, just leave the pass statement in place below + #### stop @@ -1979,6 +2039,7 @@ This function is called before cleanup is called. to read a card. Once called, the function read_card will not be called again. When the reader thread exits cleanup is called from the reader thread itself. + #### read\_card @@ -1995,6 +2056,7 @@ In case of error, it may return None or an empty string The function should break and return with an empty string, once stop() is called + # components.rfid.readerbase @@ -2011,6 +2073,7 @@ Abstract Base Class for all Reader Classes to ensure common API Look at template_new_reader.py for documentation how to integrate a new RFID reader + # components.rfid.cards @@ -2029,6 +2092,7 @@ TODO: Add callback for on_database_change TODO: check card id type (if int, convert to str) TODO: check if args is really a list (convert if not?) + #### list\_cards @@ -2044,6 +2108,7 @@ This is intended as basis for a formatter function Format: 'id': {decoded_function_call, ignore_same_id_delay, ignore_card_removal_action, description, from_alias} + #### delete\_card @@ -2081,9 +2146,10 @@ If you are going to call this through the RPC it will get a little verbose **Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume (*here: 15*) and custom *ignore_same_id_delay value*:: -plugin.call_ignore_errors('cards', 'register_card', -args=['0009', 'inc_volume'], -kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) + plugin.call_ignore_errors('cards', 'register_card', + args=['0009', 'inc_volume'], + kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) + @@ -2096,6 +2162,7 @@ def register_card_custom() Register a new card with full RPC call specification (Not implemented yet) + #### save\_card\_database @@ -2107,6 +2174,7 @@ def save_card_database(filename=None, *, only_if_changed=True) Store the current card database. If filename is None, it is saved back to the file it was loaded from + # components.rfid.cardutils @@ -2115,6 +2183,7 @@ Common card decoding functions TODO: Thread safety when accessing the card DB! + #### decode\_card\_command @@ -2125,6 +2194,7 @@ def decode_card_command(cfg_rpc_cmd: Mapping, logger: logging.Logger = log) Extension of utils.decode_action with card-specific parameters + #### card\_command\_to\_str @@ -2137,6 +2207,7 @@ Returns a list of strings with [card_action, ignore_same_id_delay, ignore_card_r The last two parameters are only present, if *long* is True and if they are present in the cfg_rpc_cmd + #### card\_to\_str @@ -2147,6 +2218,7 @@ def card_to_str(card_id: str, long=False) -> List[str] Returns a list of strings from card entry command in the format of :func:`card_command_to_str` + # components.publishing @@ -2157,6 +2229,7 @@ Thin wrapper around jukebox.publishing to benefit from the plugin loading / exit This is the first package to be loaded and the last to be closed: put Hello and Goodbye publish messages here. + #### republish @@ -2186,6 +2259,7 @@ class MusicLibPath() Extract the music directory from the mpd.conf file + #### get\_music\_library\_path @@ -2196,12 +2270,14 @@ def get_music_library_path() Get the music library path + # components.jingle Jingle Playback Factory for extensible run-time support of various file types + ## JingleFactory Objects @@ -2212,6 +2288,7 @@ class JingleFactory() Jingle Factory + #### list @@ -2222,6 +2299,7 @@ def list() List the available volume services + #### play @@ -2251,6 +2329,7 @@ if (a) through the RPC or (b) some other plugin the volume is changed. Okay, now (c) no bluetooth device connects during this time (and pulseaudio control is set to toggle_on_connect) and take our changes with the threaded approach. + #### play\_startup @@ -2262,6 +2341,7 @@ def play_startup() Play the startup sound (using jingle.play) + #### play\_shutdown @@ -2273,12 +2353,14 @@ def play_shutdown() Play the shutdown sound (using jingle.play) + # components.jingle.alsawave ALSA wave jingle Service for jingle.JingleFactory + ## AlsaWave Objects @@ -2290,6 +2372,7 @@ class AlsaWave() Jingle Service for playing wave files directly from Python through ALSA + #### play @@ -2301,6 +2384,7 @@ def play(filename) Play the wave file + ## AlsaWaveBuilder Objects @@ -2318,15 +2402,18 @@ def __init__() ``` Builder instantiates AlsaWave during init and not during first call because + we want AlsaWave registers as plugin function in any case if this plugin is loaded (and not only on first use!) + # components.jingle.jinglemp3 Generic MP3 jingle Service for jingle.JingleFactory + ## JingleMp3Play Objects @@ -2338,6 +2425,7 @@ class JingleMp3Play() Jingle Service for playing MP3 files + #### play @@ -2348,6 +2436,7 @@ def play(filename) Play the MP3 file + ## JingleMp3PlayBuilder Objects @@ -2365,9 +2454,11 @@ def __init__() ``` Builder instantiates JingleMp3Play during init and not during first call because + we want JingleMp3Play registers as plugin function in any case if this plugin is loaded (and not only on first use!) + # components.hostif.linux @@ -2383,6 +2474,7 @@ def shutdown() Shutdown the host machine + #### reboot @@ -2394,6 +2486,7 @@ def reboot() Reboot the host machine + #### jukebox\_is\_service @@ -2405,6 +2498,7 @@ def jukebox_is_service() Check if current Jukebox process is running as a service + #### is\_any\_jukebox\_service\_active @@ -2418,6 +2512,7 @@ Check if a Jukebox service is running .. note:: Does not have the be the current app, that is running as a service! + #### restart\_service @@ -2429,6 +2524,7 @@ def restart_service() Restart Jukebox App if running as a service + #### get\_disk\_usage @@ -2440,6 +2536,7 @@ def get_disk_usage(path='/') Return the disk usage in Megabytes as dictionary for RPC export + #### get\_cpu\_temperature @@ -2453,6 +2550,7 @@ Get the CPU temperature with single decimal point No error handling: this is expected to take place up-level! + #### get\_ip\_address @@ -2464,6 +2562,7 @@ def get_ip_address() Get the IP address + #### wlan\_disable\_power\_down @@ -2478,6 +2577,7 @@ Turn off power management of wlan. Keep RPi reachable via WLAN This must be done after every reboot card=None takes card from configuration file + #### get\_autohotspot\_status @@ -2489,6 +2589,7 @@ def get_autohotspot_status() Get the status of the auto hotspot feature + #### stop\_autohotspot @@ -2502,6 +2603,7 @@ Stop auto hotspot functionality Basically disabling the cronjob and running the script one last time manually + #### start\_autohotspot @@ -2515,12 +2617,14 @@ start auto hotspot functionality Basically enabling the cronjob and running the script one time manually + # components.misc Miscellaneous function package + #### rpc\_cmd\_help @@ -2532,6 +2636,7 @@ def rpc_cmd_help() Return all commands for RPC + #### get\_all\_loaded\_packages @@ -2543,6 +2648,7 @@ def get_all_loaded_packages() Get all successfully loaded plugins + #### get\_all\_failed\_packages @@ -2554,6 +2660,7 @@ def get_all_failed_packages() Get all plugins with error during load or initialization + #### get\_start\_time @@ -2565,6 +2672,7 @@ def get_start_time() Time when JukeBox has been started + #### get\_log @@ -2575,6 +2683,7 @@ def get_log(handler_name: str) Get the log file from the loggers (debug_file_handler, error_file_handler) + #### get\_log\_debug @@ -2586,6 +2695,7 @@ def get_log_debug() Get the log file (from the debug_file_handler) + #### get\_log\_error @@ -2597,6 +2707,7 @@ def get_log_error() Get the log file (from the error_file_handler) + #### get\_git\_state @@ -2608,6 +2719,7 @@ def get_git_state() Return git state information for the current branch + #### empty\_rpc\_call @@ -2638,6 +2750,7 @@ up the module call stack. # components.controls.bluetooth\_audio\_buttons Plugin to attempt to automatically listen to it's buttons (play, next, ...) + when a bluetooth sound device (headphone, speakers) connects This effectively does: @@ -2646,12 +2759,14 @@ This effectively does: * if that is a bluetooth device, try opening an input device with similar name using * button listeners are run each in its own thread + # components.controls.common.evdev\_listener Generalized listener for ``dev/input`` devices + #### find\_device @@ -2690,6 +2805,7 @@ class EvDevKeyListener(threading.Thread) ``` Opens and event input device from ``/dev/inputs``, and runs callbacks upon the button presses. + Input devices could be .e.g. Keyboard, Bluetooth audio buttons, USB buttons Runs as a separate thread. When device disconnects or disappears, thread exists. A new thread must be started @@ -2697,6 +2813,7 @@ when device re-connects. Assign callbacks to :attr:`EvDevKeyListener.button_callbacks` + #### \_\_init\_\_ @@ -2732,6 +2849,7 @@ def start() -> None Start the tread and start listening + # components.music\_cover\_art @@ -2740,6 +2858,7 @@ Read all cover art from music save it to a cache for the UI to load .. note:: Not implemented. This is a feature planned for a future release. + ## MusicCoverArt Objects @@ -2759,6 +2878,7 @@ def get_by_filename_as_base64(audio_src: str) Not implemented. This is a feature planned for a future release. + # components.battery\_monitor @@ -2777,6 +2897,7 @@ class pt1_frac() fixed point first order filter, fractional format: 2^16,2^16 + ## BattmonBase Objects @@ -2787,6 +2908,7 @@ class BattmonBase() Battery Monitor base class + # components.battery\_monitor.batt\_mon\_simulator @@ -2801,6 +2923,7 @@ class battmon_simulator(BatteryMonitorBase.BattmonBase) Battery Monitor Simulator + # components.battery\_monitor.batt\_mon\_i2c\_ads1015 @@ -2848,52 +2971,62 @@ Attention: - the time between sample needs to be a minimum 1sec with this high impedance voltage divider don't use the continuous conversion method! + # components.gpio.gpioz.plugin The GPIOZ plugin interface build all input and output devices from the configuration file and connects + the actions and callbacks. It also provides a very restricted, but common API for the output devices to the RPC. That API is mainly used for testing. All the relevant output state changes are usually made through callbacks directly using the output device's API. + #### output\_devices List of all created output devices + #### input\_devices List of all created input devices + #### factory The global pin factory used in this module + Using different pin factories for different devices is not supported + #### IS\_ENABLED Indicates that the GPIOZ module is enabled and loaded w/o errors + #### IS\_MOCKED Indicates that the pin factory is a mock factory + #### CONFIG\_FILE The path of the config file the GPIOZ configuration was loaded from + ## ServiceIsRunningCallbacks Objects @@ -2904,7 +3037,7 @@ class ServiceIsRunningCallbacks(CallbackHandler) Callbacks are executed when - * Jukebox app started +* Jukebox app started * Jukebox shuts down This is intended to e.g. signal an LED to change state. @@ -2914,6 +3047,7 @@ This is integrated into this module because: * the plugin callback functions provide all the functionality to control the status of the LED * which means no need to adapt other modules + #### register @@ -2940,15 +3074,17 @@ Callback signature is def run_callbacks(status: int) ``` -:meta private: + #### service\_is\_running\_callbacks Callback handler instance for service_is_running_callbacks events. + See :class:`ServiceIsRunningCallbacks` + #### build\_output\_device @@ -2962,6 +3098,7 @@ Construct and register a new output device In principal all supported GPIOZero output devices can be used. For all devices a custom functions need to be written to control the state of the outputs + #### build\_input\_device @@ -2974,6 +3111,7 @@ Construct and connect a new input device Supported input devices are those from gpio.gpioz.core.input_devices + #### get\_output @@ -3072,17 +3210,20 @@ specific output device are silently ignored # components.gpio.gpioz.plugin.connectivity Provide connector functions to hook up to some kind of Jukebox functionality and change the output device's state + accordingly. Connector functions can often be used for various output devices. Some connector functions are specific to an output device type. + #### BUZZ\_TONE The tone to be used as buzz tone when the buzzer is an active buzzer + #### register\_rfid\_callback @@ -3101,6 +3242,7 @@ Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + #### register\_status\_led\_callback @@ -3117,6 +3259,7 @@ Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + #### register\_status\_buzzer\_callback @@ -3132,6 +3275,7 @@ Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + #### register\_status\_tonalbuzzer\_callback @@ -3146,6 +3290,7 @@ Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + #### register\_audio\_sink\_change\_callback @@ -3155,6 +3300,7 @@ def register_audio_sink_change_callback(device) ``` Turn LED on if secondary audio output is selected. If audio output change + fails, blink thrice Compatible devices: @@ -3163,6 +3309,7 @@ Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + #### register\_volume\_led\_callback @@ -3172,12 +3319,14 @@ def register_volume_led_callback(device) ``` Have a PWMLED change it's brightness according to current volume. LED flashes when minimum or maximum volume + is reached. Minimum value is still a very dimly turned on LED (i.e. LED is never off). Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + #### register\_volume\_buzzer\_callback @@ -3193,6 +3342,7 @@ Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + #### register\_volume\_rgbled\_callback @@ -3202,19 +3352,23 @@ def register_volume_rgbled_callback(device) ``` Have a :class:`RGBLED` change it's color according to current volume. LED flashes when minimum or maximum volume + is reached. Compatible devices: - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + # components.gpio.gpioz.core.converter Provides converter functions/classes for various Jukebox parameters to + values that can be assigned to GPIO output devices + ## ColorProperty Objects @@ -3225,7 +3379,6 @@ class ColorProperty() Color descriptor ensuring valid weight ranges -:meta private: @@ -3277,12 +3430,14 @@ def luminize(r, g, b) Apply the color weight factors to the input color values + # components.gpio.gpioz.core.mock Changes to the GPIOZero devices for using with the Mock RFID Reader + #### patch\_mock\_outputs\_with\_callback @@ -3298,6 +3453,7 @@ Other output devices cannot be represented in the GUI and are silently ignored. ..note:: Only for developing purposes! + # components.gpio.gpioz.core.input\_devices @@ -3312,6 +3468,7 @@ by using the :func:`set_rpc_actions` each input device exhibits. For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Input devices` + ## NameMixin Objects @@ -3322,7 +3479,6 @@ class NameMixin(ABC) Provides name property and RPC decode function -:meta private: @@ -3350,7 +3506,6 @@ class EventProperty() Event callback property -:meta private: @@ -3362,7 +3517,6 @@ class ButtonBase(ABC) Common stuff for single button devices -:meta private: @@ -3375,6 +3529,7 @@ def value() Returns 1 if the button is currently pressed, and 0 if it is not. + #### pin @@ -3386,6 +3541,7 @@ def pin() Returns the underlying pin class from GPIOZero. + #### pull\_up @@ -3397,6 +3553,7 @@ def pull_up() If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. + #### close @@ -3407,6 +3564,7 @@ def close() Close the device and release the pin + ## Button Objects @@ -3454,6 +3612,7 @@ def on_press() The function to run when the device has been pressed + ## LongPressButton Objects @@ -3485,6 +3644,7 @@ def on_press(func) The function to run when the device has been pressed for longer than :attr:`hold_time` + ## ShortLongPressButton Objects @@ -3541,6 +3701,7 @@ def pin_a() Returns the underlying pin A + #### pin\_b @@ -3552,6 +3713,7 @@ def pin_b() Returns the underlying pin B + #### on\_rotate\_clockwise @@ -3563,6 +3725,7 @@ def on_rotate_clockwise() The function to run when the encoder is rotated clockwise + #### on\_rotate\_counter\_clockwise @@ -3574,6 +3737,7 @@ def on_rotate_counter_clockwise() The function to run when the encoder is rotated counter clockwise + #### close @@ -3584,6 +3748,7 @@ def close() Close the device and release the pin + ## TwinButton Objects @@ -3627,7 +3792,6 @@ class StateVar(Enum) State encoding of the Mealy FSM -:meta private: @@ -3639,6 +3803,7 @@ def close() Close the device and release the pins + #### value @@ -3650,6 +3815,7 @@ def value() 2 bit integer indicating if and which button is currently pressed. Button A is the LSB. + #### is\_active @@ -3659,7 +3825,7 @@ def value() def is_active() ``` -:data:`True` if one or both buttons are currently pressed + @@ -3678,6 +3844,7 @@ are silently ignored. This is done to reduce the amount of coding required for c For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Output devices` + ## LED Objects @@ -3735,6 +3902,7 @@ def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) Flash the device and restore the previous value afterwards + ## PWMLED Objects @@ -3760,6 +3928,7 @@ def flash(on_time=1, Flash the LED and restore the previous value afterwards + ## RGBLED Objects @@ -3787,6 +3956,7 @@ def flash(on_time=1, Flash the LED with :attr:`on_color` and restore the previous value afterwards + ## TonalBuzzer Objects @@ -3811,6 +3981,7 @@ def flash(on_time=1, Play the tone :data:`tone` for :attr:`n` times + #### melody @@ -3825,6 +3996,7 @@ def melody(on_time=0.2, Play a melody from the list of tones in :attr:`tone` + # components.timers @@ -3839,6 +4011,7 @@ Play a melody from the list of tones in :attr:`tone` Provides a generic callback handler + ## CallbackHandler Objects @@ -3886,6 +4059,7 @@ Run all registered callbacks. *ALL* exceptions from callback functions will be caught and logged only. Exceptions are not raised upwards! + #### has\_callbacks @@ -3895,7 +4069,7 @@ Exceptions are not raised upwards! def has_callbacks() ``` -:data:`True` if there are any registered callbacks. Read-only property + @@ -3911,6 +4085,7 @@ def version() Return the Jukebox version as a string + #### version\_info @@ -3923,6 +4098,7 @@ Return the Jukebox version as a tuple of three numbers If this is a development version, an identifier string will be appended after the third integer. + # jukebox.cfghandler @@ -3943,6 +4119,7 @@ Handlers are identified by their name (in the above example *global*) The function :func:`get_handler` is the main entry point to obtain a new or existing handler. + ## ConfigHandler Objects @@ -3975,6 +4152,7 @@ Else we have a deadlock. Reading may be done without acquiring a lock. But be aware that when reading multiple values without locking, another thread may intervene and modify some values in between! So, locking is still recommended. + #### loaded\_from @@ -3986,6 +4164,7 @@ def loaded_from() -> Optional[str] Property to store filename from which the config was loaded + #### get @@ -3996,6 +4175,7 @@ def get(key, *, default=None) Enforce keyword on default to avoid accidental misuse when actually getn is wanted + #### setdefault @@ -4006,6 +4186,7 @@ def setdefault(key, *, value) Enforce keyword on default to avoid accidental misuse when actually setndefault is wanted + #### getn @@ -4019,6 +4200,7 @@ Get the value at arbitrary hierarchy depth. Return ``default`` if key not presen The *default* value is returned no matter at which hierarchy level the path aborts. A hierarchy is considered as any type with a :func:`get` method. + #### setn @@ -4088,6 +4270,7 @@ Check if the data has changed since the last load/store .. note: This relies on the *__str__* representation of the underlying data structure In case of ruamel, this ignores comments and only looks at the data + #### clear\_modified @@ -4098,6 +4281,7 @@ def clear_modified() -> None Sets the current state as new baseline, clearing the is_modified state + #### save @@ -4110,6 +4294,7 @@ Save config back to the file it was loaded from If you want to save to a different file, use :func:`write_yaml`. + #### load @@ -4120,6 +4305,7 @@ def load(filename: str) -> None Load YAML config file into memory + #### get\_handler @@ -4192,6 +4378,7 @@ None # jukebox.playlistgenerator Playlists are build from directory content in the following way: + a directory is parsed and files are added to the playlist in the following way 1. files are added in alphabetic order @@ -4224,12 +4411,14 @@ This means, one ``*.m3u`` file per sub-folder is processed (if present). In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. + #### TYPE\_DECODE Types if file entires in parsed directory + ## PlaylistCollector Objects @@ -4255,6 +4444,7 @@ But it can also be used with relative paths from current working directory:: The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. + #### \_\_init\_\_ @@ -4283,6 +4473,7 @@ Set the class-wide file ending exclusion list See :attr:`PlaylistCollector._exclude_endings` + #### get\_directory\_content @@ -4343,14 +4534,14 @@ different threads, always make a fresh call to ``get_publisher()`` to get the co Example:: -import jukebox.publishing as publishing + import jukebox.publishing as publishing -class MyClass: -def __init__(self): -pass + class MyClass: + def __init__(self): + pass -def say_hello(name): -publishing.get_publisher().send('hello', f'Hi {name}, howya?') + def say_hello(name): + publishing.get_publisher().send('hello', f'Hi {name}, howya?') To stress what **NOT** to do: don't get a publisher instance in the constructor and save it to ``self._pub``. If you do and ``say_hello`` gets called from different threads, the publisher of the thread which instantiated the class @@ -4361,6 +4552,7 @@ But: the use cases are very rare for that. I cannot think of one at the moment. **Remember**: Don’t share ZeroMQ sockets between threads. + # jukebox.publishing.subscriber @@ -4370,6 +4562,7 @@ But: the use cases are very rare for that. I cannot think of one at the moment. # jukebox.publishing.server Publishing Server + ******************** The common publishing server for the entire Jukebox using ZeroMQ @@ -4379,48 +4572,48 @@ Structure .. code-block:: text -+-----------------------+ -| functional interface | Publisher -| | - functional interface for single Thread -| PUB | - sends data to publisher (and thus across threads) -+-----------------------+ -| (1) -v -+-----------------------+ -| SUB (bind) | PublishServer -| | - Last Value (LV) Cache -| XPUB (bind) | - Subscriber notification and LV resend -+-----------------------+ - independent thread -| (2) -v + +-----------------------+ + | functional interface | Publisher + | | - functional interface for single Thread + | PUB | - sends data to publisher (and thus across threads) + +-----------------------+ + | (1) + v + +-----------------------+ + | SUB (bind) | PublishServer + | | - Last Value (LV) Cache + | XPUB (bind) | - Subscriber notification and LV resend + +-----------------------+ - independent thread + | (2) + v Connection (1): Internal connection -Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) + Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) -Protocol: Multi-part message + Protocol: Multi-part message -Part 1: Topic (in topic tree format) -E.g. player.status.elapsed + Part 1: Topic (in topic tree format) + E.g. player.status.elapsed -Part 2: Payload or Message in json serialization -If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same + Part 2: Payload or Message in json serialization + If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same -Part 3: Command -Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer -and the message is not forwarded to the outside. This third part of the message is never forwarded + Part 3: Command + Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer + and the message is not forwarded to the outside. This third part of the message is never forwarded Connection (2): External connection -Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! -Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will -also get you all the branch topics. To get everything, subscribe to ``b''`` + Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! + Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will + also get you all the branch topics. To get everything, subscribe to ``b''`` -Protocol: Multi-part message + Protocol: Multi-part message -Part 1: Topic (in topic tree format) -E.g. player.status.elapsed + Part 1: Topic (in topic tree format) + E.g. player.status.elapsed -Part 2: Payload or Message in json serialization -If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) + Part 2: Payload or Message in json serialization + If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) Why? Why? ------------- @@ -4434,13 +4627,13 @@ Design Rationales ------------------- * "If you need `millions of messages per second `_ -sent to thousands of points, -you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." + sent to thousands of points, + you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then -the `XSUB and XPUB `_" + the `XSUB and XPUB `_" * "Let’s imagine `our feed has an average of 100,000 100-byte messages a second -`_ [...]. -While 100K messages a second is easy for a ZeroMQ application, ..." + `_ [...]. + While 100K messages a second is easy for a ZeroMQ application, ..." **But we have:** @@ -4461,8 +4654,8 @@ This means, we can use less complex patters than used for these high-speed, high * Late joining client (or drop-off and re-join): get full state update * Server crash etc: No special handling necessary, we are simple -and don't need recovery in this case. Server will publish initial state -after re-start + and don't need recovery in this case. Server will publish initial state + after re-start * Subscriber too slow: Subscribers problem (TODO: Do we need to do anything about it?) **Start-up sequence:** @@ -4481,8 +4674,8 @@ RPC can trigger through function call in components/publishing plugin that Plugins publishing state information should publish initial state at @plugin.finalize .. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is -required per thread. But the publisher instance **must** be thread-local! -Always go through :func:`publishing.get_publisher()`. + required per thread. But the publisher instance **must** be thread-local! + Always go through :func:`publishing.get_publisher()`. **Sockets** @@ -4491,13 +4684,14 @@ Three sockets are opened: ``. TCP (on a configurable port) ``. Websocket (on a configurable port) ``. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules -that want to know about the current state on event based updates. + that want to know about the current state on event based updates. **Further ZeroMQ References:** * `Working with Messages `_ * `Multiple Threads `_ + ## PublishServer Objects @@ -4507,12 +4701,14 @@ class PublishServer(threading.Thread) ``` The publish proxy server that collects and caches messages from all internal publishers and + forwards them to the outside world Handles new subscriptions by sending out the entire cached state to **all** subscribers The code is structures using a `Reactor Pattern `_ + #### run @@ -4523,6 +4719,7 @@ def run() Thread's activity + #### handle\_message @@ -4533,6 +4730,7 @@ def handle_message(msg) Handle incoming messages + #### handle\_subscription @@ -4543,6 +4741,7 @@ def handle_subscription(msg) Handle new subscribers + ## Publisher Objects @@ -4557,6 +4756,7 @@ The publisher that provides the functional interface to the application * An instance must not be shared across threads! * One instance per thread is enough + #### \_\_init\_\_ @@ -4580,6 +4780,7 @@ def send(topic: str, payload) Send out a message for topic + #### revoke @@ -4590,6 +4791,7 @@ def revoke(topic: str) Revoke a single topic element (not a topic tree!) + #### resend @@ -4602,6 +4804,7 @@ Instructs the PublishServer to resend current status to all subscribers Not necessary to call after incremental updates or new subscriptions - that will happen automatically! + #### close\_server @@ -4612,6 +4815,7 @@ def close_server() Instructs the PublishServer to close itself down + # jukebox.daemon @@ -4626,10 +4830,12 @@ def log_active_threads() ``` This functions is registered with atexit very early, meaning it will be run very late. It is the best guess to + evaluate which Threads are still running (and probably shouldn't be) This function is registered before all the plugins and their dependencies are loaded + ## JukeBox Objects @@ -4651,6 +4857,7 @@ Signal handler for orderly shutdown On first Ctrl-C (or SIGTERM) orderly shutdown procedure is embarked upon. It gets allocated a time-out! On third Ctrl-C (or SIGTERM), this is interrupted and there will be a hard exit! + # jukebox.plugs @@ -4668,65 +4875,66 @@ python packages and can be accessed by normal means If you want to provide additional functionality to the same feature (probably even for run-time switching) you can implement a Factory Pattern using this package. Take a look at volume.py as an example. -**Example:** Decorate a function for auto-registering under it's own name:: +**Example:** Decorate a function for auto-registering under it's own name: +.. code-block:: python + + import jukebox.plugs as plugs + @plugs.register + def func1(param): + pass **Example:** Decorate a function for auto-registering under a new name:: -@plugs.register(name='better_name') -def func2(param): -pass + @plugs.register(name='better_name') + def func2(param): + pass **Example:** Register a function during run-time under it's own name:: -def func3(param): -pass -plugs.register(func3) + def func3(param): + pass + plugs.register(func3) **Example:** Register a function during run-time under a new name:: -def func4(param): -pass -plugs.register(func4, name='other_name', package='other_package') + def func4(param): + pass + plugs.register(func4, name='other_name', package='other_package') **Example:** Decorate a class for auto registering during initialization, including all methods (see _register_class for more info):: -@plugs.register(auto_tag=True) -class MyClass1: -pass + @plugs.register(auto_tag=True) + class MyClass1: + pass **Example:** Register a class instance, from which only report is a callable method through the plugs interface:: -class MyClass2: -@plugs.tag -def report(self): -pass -myinst2 = MyClass2() -plugin.register(myinst2, name='myinst2') + class MyClass2: + @plugs.tag + def report(self): + pass + myinst2 = MyClass2() + plugin.register(myinst2, name='myinst2') Naming convention: package -1. Either a python package -2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) + 1. Either a python package + 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) plugin -1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) -2. The string name to above object + 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) + 2. The string name to above object name -The string name of the plugin object for registration + The string name of the plugin object for registration method -1. In case the object is a class instance a bound method to call from the class instance -2. The string name to above object -```python -import jukebox.plugs as plugs -@plugs.register -def func1(param): - pass -``` + 1. In case the object is a class instance a bound method to call from the class instance + 2. The string name to above object + @@ -4738,6 +4946,7 @@ class PluginPackageClass() A local data class for holding all information about a loaded plugin package + #### register @@ -4749,6 +4958,7 @@ def register(plugin: Callable) -> Callable 1-level decorator around a function + #### register @@ -4760,6 +4970,7 @@ def register(plugin: Type) -> Any Signature: 1-level decorator around a class + #### register @@ -4771,6 +4982,7 @@ def register(*, name: str, package: Optional[str] = None) -> Callable Signature: 2-level decorator around a function + #### register @@ -4782,6 +4994,7 @@ def register(*, auto_tag: bool = False, package: Optional[str] = None) -> Type Signature: 2-level decorator around a class + #### register @@ -4797,6 +5010,7 @@ def register(plugin: Callable[..., Any] = None, Signature: Run-time registration of function / class instance / bound method + #### register @@ -4977,6 +5191,7 @@ def load_all_unnamed(packages_unnamed: Iterable[str], Load all packages in packages_unnamed with default names + #### load\_all\_finalize @@ -4989,6 +5204,7 @@ Calls all functions registered with @finalize from all loaded modules in the ord This must be executed after the last plugin package is loaded + #### close\_down @@ -5076,6 +5292,7 @@ Errors get logged. See :func:`call` for parameter documentation. + #### exists @@ -5088,6 +5305,7 @@ def exists(package: str, Check if an object is registered within the plugs package + #### get @@ -5106,6 +5324,7 @@ The return object depends on the number of parameters * 2 arguments: Get the plugin reference for the plugs *package.plugin* * 3 arguments: Get the plugin reference for the plugs *package.plugin.method* + #### loaded\_as @@ -5116,6 +5335,7 @@ def loaded_as(module_name: str) -> str Return the plugin name a python module is loaded as + #### delete @@ -5128,6 +5348,7 @@ Delete a plugin object from the registered plugs callables Note: This does not 'unload' the python module. It merely makes it un-callable via plugs! + #### dump\_plugins @@ -5138,6 +5359,7 @@ def dump_plugins(stream) Write a human readable summary of all plugin callables to stream + #### summarize @@ -5148,6 +5370,7 @@ def summarize() Create a reference summary of all plugin callables in dictionary format + #### generate\_help\_rst @@ -5158,6 +5381,7 @@ def generate_help_rst(stream) Write a reference of all plugin callables in Restructured Text format + #### get\_all\_loaded\_packages @@ -5200,12 +5424,14 @@ Dictionary of the form `{loaded_as: loaded_from, ...}` Text to Speech. Plugin to speak any given text via speaker + # jukebox.multitimer Multitimer Module + ## MultiTimer Objects @@ -5225,6 +5451,7 @@ Initiates start and publishing by calling self.publish_callback Note: Inspired by threading.Timer and generally using the same API + #### cancel @@ -5235,6 +5462,7 @@ def cancel() Stop the timer if it hasn't finished all iterations yet. + ## GenericTimerClass Objects @@ -5245,6 +5473,7 @@ class GenericTimerClass() Interface for plugin / RPC accessibility for a single event timer + #### \_\_init\_\_ @@ -5271,6 +5500,7 @@ def start(wait_seconds=None) Start the timer (with default or new parameters) + #### cancel @@ -5282,6 +5512,7 @@ def cancel() Cancel the timer + #### toggle @@ -5293,6 +5524,7 @@ def toggle() Toggle the activation of the timer + #### trigger @@ -5304,6 +5536,7 @@ def trigger() Trigger the next target execution before the time is up + #### is\_alive @@ -5315,6 +5548,7 @@ def is_alive() Check if timer is active + #### get\_timeout @@ -5341,6 +5575,7 @@ def set_timeout(wait_seconds: float) Set a new time-out in seconds. Re-starts the timer if already running! + #### publish @@ -5352,6 +5587,7 @@ def publish() Publish the current state and config + #### get\_state @@ -5363,6 +5599,7 @@ def get_state() Get the current state and config as dictionary + ## GenericEndlessTimerClass Objects @@ -5373,6 +5610,7 @@ class GenericEndlessTimerClass(GenericTimerClass) Interface for plugin / RPC accessibility for an event timer call function endlessly every m seconds + ## GenericMultiTimerClass Objects @@ -5383,6 +5621,7 @@ class GenericMultiTimerClass(GenericTimerClass) Interface for plugin / RPC accessibility for an event timer that performs an action n times every m seconds + #### \_\_init\_\_ @@ -5417,12 +5656,14 @@ def start(iterations=None, wait_seconds_per_iteration=None) Start the timer (with default or new parameters) + # jukebox.utils Common utility functions + #### decode\_rpc\_call @@ -5484,6 +5725,7 @@ def decode_and_call_rpc_command(rpc_cmd: Dict, logger: logging.Logger = log) Convenience function combining decode_rpc_command and plugs.call_ignore_errors + #### bind\_rpc\_command @@ -5540,6 +5782,7 @@ def generate_cmd_alias_rst(stream) Write a reference of all rpc command aliases in Restructured Text format + #### generate\_cmd\_alias\_reference @@ -5550,6 +5793,7 @@ def generate_cmd_alias_reference(stream) Write a reference of all rpc command aliases in text format + #### get\_git\_state @@ -5560,6 +5804,7 @@ def get_git_state() Return git state information for the current branch + # jukebox.rpc @@ -5573,6 +5818,7 @@ Return git state information for the current branch # jukebox.rpc.server Remote Procedure Call Server (RPC) + ************************************* Bind to tcp and/or websocket port and translates incoming requests to procedure calls. @@ -5617,6 +5863,7 @@ Three sockets are opened the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which button triggers what action) + ## RpcServer Objects @@ -5627,6 +5874,7 @@ class RpcServer() The RPC Server Class + #### \_\_init\_\_ @@ -5637,6 +5885,7 @@ def __init__(context=None) Initialize the connections and bind to the ports + #### run @@ -5646,5 +5895,7 @@ def run() ``` The main endless loop waiting for requests and forwarding the + call request to the plugin module + diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index c00eb47e7..f221a0bb6 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -3,7 +3,7 @@ loaders: search_path: [./src/jukebox] processors: - type: filter - - type: smart + - type: sphinx - type: crossref renderer: type: markdown diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index bc3c3e441..452aec12b 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -14,14 +14,12 @@ If you want to provide additional functionality to the same feature (probably even for run-time switching) you can implement a Factory Pattern using this package. Take a look at volume.py as an example. -**Example:** Decorate a function for auto-registering under it's own name:: +**Example:** Decorate a function for auto-registering under it's own name: -```python -import jukebox.plugs as plugs -@plugs.register -def func1(param): - pass -``` + import jukebox.plugs as plugs + @plugs.register + def func1(param): + pass **Example:** Decorate a function for auto-registering under a new name:: From 34aeebcf3477b9af402985a74a52525a0ea63cfa Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 22:43:40 +0100 Subject: [PATCH 17/41] update markdown --- documentation/developers/docstring/docstring-api.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index 52e632bc0..f6335f409 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -4877,8 +4877,6 @@ you can implement a Factory Pattern using this package. Take a look at volume.py **Example:** Decorate a function for auto-registering under it's own name: -.. code-block:: python - import jukebox.plugs as plugs @plugs.register def func1(param): From 2df3313c55818d57736293646bfe7e392249ca23 Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 22:56:11 +0100 Subject: [PATCH 18/41] convert links and formatting to markdown --- documentation/developers/docstring/docstring-api.md | 11 ++++++----- src/jukebox/jukebox/plugs.py | 7 ++++--- src/jukebox/run_jukebox.py | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index f6335f409..e92f2f9bf 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -389,11 +389,11 @@ This is the main app and starts the Jukebox Core. Usually this runs as a service, which is started automatically after boot-up. At times, it may be necessary to restart the service. For example after a configuration change. Not all configuration changes can be applied on-the-fly. -See :ref:`userguide/configuration:Jukebox Configuration`. +See [Jukebox Configuration](../../builders/configuration.md#jukebox-configuration). For debugging, it is usually desirable to run the Jukebox directly from the console rather than as service. This gives direct logging info in the console and allows changing command line parameters. -See :ref:`userguide/troubleshooting:Troubleshooting`. +See [Troubleshooting](../../builders/troubleshooting.md). @@ -5118,9 +5118,10 @@ def atexit(func: Callable[[int], Any]) -> Callable[[int], Any] Decorator for functions that shall be called by the plugs package directly after at exit of program. -.. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called - during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your - shutdown handler. +> [!IMPORTANT] +> There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called +> during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your +> shutdown handler. The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) It is intended for passing down the signal number that initiated the program termination diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index 452aec12b..36daa74fe 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -504,9 +504,10 @@ def atexit(func: Callable[[int], Any]) -> Callable[[int], Any]: """ Decorator for functions that shall be called by the plugs package directly after at exit of program. - .. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called - during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your - shutdown handler. + > [!IMPORTANT] + > There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called + > during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your + > shutdown handler. The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) It is intended for passing down the signal number that initiated the program termination diff --git a/src/jukebox/run_jukebox.py b/src/jukebox/run_jukebox.py index 0735e2b8e..789e57aca 100755 --- a/src/jukebox/run_jukebox.py +++ b/src/jukebox/run_jukebox.py @@ -5,11 +5,11 @@ Usually this runs as a service, which is started automatically after boot-up. At times, it may be necessary to restart the service. For example after a configuration change. Not all configuration changes can be applied on-the-fly. -See :ref:`userguide/configuration:Jukebox Configuration`. +See [Jukebox Configuration](../../builders/configuration.md#jukebox-configuration). For debugging, it is usually desirable to run the Jukebox directly from the console rather than as service. This gives direct logging info in the console and allows changing command line parameters. -See :ref:`userguide/troubleshooting:Troubleshooting`. +See [Troubleshooting](../../builders/troubleshooting.md). """ import os.path import argparse From 325c7ce3ecc15dbe9fe30df81e43e6efb1a0c5bc Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 23:07:03 +0100 Subject: [PATCH 19/41] make link to plugin docs --- documentation/developers/README.md | 1 + documentation/developers/docstring/docstring-api.md | 9 +++++---- documentation/developers/known-issues.md | 2 ++ src/jukebox/jukebox/plugs.py | 10 ++++++---- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/documentation/developers/README.md b/documentation/developers/README.md index f98aa296e..0e699bb2e 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -10,5 +10,6 @@ * [Jukebox Apps](./coreapps.md) * [RFID Readers](./rfid) * [Docstring API Docs (from py files)](./docstring/docstring-api.md) +* [Plugin Reference](./docstring/docstring-api.md#jukeboxplugs) * [Feature Status](./status.md) * [Known Issues](./known-issues.md) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index e92f2f9bf..d64f10861 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -5248,9 +5248,9 @@ Also remember, that method must have the attribute ``plugin_callable = True`` Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. -.. note:: - There is no logger in this function as they all belong up-level where the exceptions are handled. - If you want logger messages instead of exceptions, use :func:`call_ignore_errors` +> [!NOTE] +> There is no logger in this function as they all belong up-level where the exceptions are handled. +> If you want logger messages instead of exceptions, use :func:`call_ignore_errors` **Arguments**: @@ -5345,7 +5345,8 @@ def delete(package: str, plugin: Optional[str] = None, ignore_errors=False) Delete a plugin object from the registered plugs callables -Note: This does not 'unload' the python module. It merely makes it un-callable via plugs! +> [!NOTE] +> This does not 'unload' the python module. It merely makes it un-callable via plugs! diff --git a/documentation/developers/known-issues.md b/documentation/developers/known-issues.md index 1460c1cb4..4937ddf46 100644 --- a/documentation/developers/known-issues.md +++ b/documentation/developers/known-issues.md @@ -16,6 +16,8 @@ RUN cd ${HOME} && mkdir ${ZMQ_TMP_DIR} && cd ${ZMQ_TMP_DIR}; \ make && make install ``` +[libzmq details](./libzmq.md) + ## Configuration In `jukebox.yaml` (and all other config files): diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index 36daa74fe..30374a29f 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -724,9 +724,9 @@ def call(package: str, plugin: str, method: Optional[str] = None, *, Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. - .. note:: - There is no logger in this function as they all belong up-level where the exceptions are handled. - If you want logger messages instead of exceptions, use :func:`call_ignore_errors` + > [!NOTE] + > There is no logger in this function as they all belong up-level where the exceptions are handled. + > If you want logger messages instead of exceptions, use :func:`call_ignore_errors` :param package: Name of the plugin package in which to look for function/class instance :param plugin: Function name or instance name of a class @@ -825,7 +825,9 @@ def loaded_as(module_name: str) -> str: def delete(package: str, plugin: Optional[str] = None, ignore_errors=False): """Delete a plugin object from the registered plugs callables - Note: This does not 'unload' the python module. It merely makes it un-callable via plugs!""" + > [!NOTE] + > This does not 'unload' the python module. It merely makes it un-callable via plugs! + """ with _lock_module: if exists(package, plugin): if plugin is None: From 0c4ad4f987982fce3204e826c2e4a14e1d3034cc Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 23:12:21 +0100 Subject: [PATCH 20/41] fix comment --- run_docgeneration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_docgeneration.sh b/run_docgeneration.sh index 4c2324d07..22ab8bc14 100755 --- a/run_docgeneration.sh +++ b/run_docgeneration.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Runner script for lazydocs to ensure +# Runner script for pydoc-markdown to ensure # - independent from working directory # Change working directory to location of script From 9176013a21dd7ef357623fc2427277d30958c10a Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 26 Dec 2023 23:21:54 +0100 Subject: [PATCH 21/41] fix wrong docstring --- .../developers/docstring/docstring-api.md | 19 +++++++++---------- src/jukebox/misc/loggingext.py | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index d64f10861..a8596ced7 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -626,22 +626,21 @@ def __init__(enable=True, color_levelname=True) class PubStream() ``` -" - Stream handler wrapper around the publisher for logging.StreamHandler Allows logging to send all log information (based on logging configuration) to the Publisher. -ATTENTION: This can lead to recursions! - -Recursions come up when -(a) Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, - which causes a send, ..... -(b) Publisher initialization emits logs, which need a Publisher instance to send logs +> [!CAUTION] +> This can lead to recursions! +> Recursions come up when +> * Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, +> which causes a send, ..... +> * Publisher initialization emits logs, which need a Publisher instance to send logs -IMPORTANT: To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the -functions in the send-function stack! +> [!IMPORTANT] +> To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the +> functions in the send-function stack! diff --git a/src/jukebox/misc/loggingext.py b/src/jukebox/misc/loggingext.py index 9328cfea8..30ba80780 100644 --- a/src/jukebox/misc/loggingext.py +++ b/src/jukebox/misc/loggingext.py @@ -80,21 +80,22 @@ def filter(self, record): class PubStream: - """" + """ Stream handler wrapper around the publisher for logging.StreamHandler Allows logging to send all log information (based on logging configuration) to the Publisher. - ATTENTION: This can lead to recursions! - - Recursions come up when - (a) Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, - which causes a send, ..... - (b) Publisher initialization emits logs, which need a Publisher instance to send logs + > [!CAUTION] + > This can lead to recursions! + > Recursions come up when + > * Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, + > which causes a send, ..... + > * Publisher initialization emits logs, which need a Publisher instance to send logs - IMPORTANT: To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the - functions in the send-function stack! + > [!IMPORTANT] + > To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the + > functions in the send-function stack! """ def __init__(self): self._topic = 'core.logger' From fb74a4e6a231111c544495982202e9c12390f225 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 11:39:56 +0100 Subject: [PATCH 22/41] add more fixes to doc --- .../developers/docstring/docstring-api.md | 36 ++++++++++++++---- src/jukebox/misc/loggingext.py | 37 ++++++++++--------- src/jukebox/run_configure_audio.py | 2 +- src/jukebox/run_register_rfid_reader.py | 7 ++-- src/jukebox/run_rpc_tool.py | 2 +- 5 files changed, 53 insertions(+), 31 deletions(-) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index a8596ced7..177ef199c 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -407,10 +407,11 @@ See [Troubleshooting](../../builders/troubleshooting.md). Setup tool to configure the RFID Readers. Run this once to register and configure the RFID readers with the Jukebox. Can be re-run at any time to change -the settings. For more information see :ref:`rfid/rfid:RFID Readers`. +the settings. For more information see [RFID Readers](../rfid/README.md). -.. note:: This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). - Any manual modifications to the settings will have to be re-applied +> [!NOTE] +> This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). +> Any manual modifications to the settings will have to be re-applied @@ -428,7 +429,7 @@ The tool features auto-completion and command history. The list of available commands is fetched from the running Jukebox service. .. todo: - - kwargs support + - kwargs support @@ -451,7 +452,7 @@ Setup tool to register the PulseAudio sinks as primary and secondary audio outpu Will also setup equalizer and mono down mixer in the pulseaudio config file. Run this once after installation. Can be re-run at any time to change the settings. -For more information see :ref:`userguide/audio:Audio Configuration`. +For more information see [Audio Configuration](../../builders/audio.md#audio-configuration). @@ -574,10 +575,8 @@ boolean value read from user input # misc.loggingext -############## +## Logger -Logger -############## We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. The top-level logger is called 'jb' (to make it short). In any module you may simple create a child-logger at any hierarchy @@ -585,6 +584,27 @@ level below 'jb'. It will inherit settings from it's parent logger unless otherw Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be created on the spot. +Example: How to get logger and log away at your heart's content: + + >>> import logging + >>> logger = logging.getLogger('jb.awesome_module') + >>> logger.info('Started general awesomeness aura') + +Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module: + + loggers: + jb: + level: WARNING + handlers: [console, debug_file_handler, error_file_handler] + propagate: no + jb.awesome_module: + level: DEBUG + + +> [!NOTE] +> The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense). +> There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output + diff --git a/src/jukebox/misc/loggingext.py b/src/jukebox/misc/loggingext.py index 30ba80780..36b040339 100644 --- a/src/jukebox/misc/loggingext.py +++ b/src/jukebox/misc/loggingext.py @@ -1,7 +1,6 @@ """ -############## -Logger -############## +## Logger + We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. The top-level logger is called 'jb' (to make it short). In any module you may simple create a child-logger at any hierarchy @@ -9,25 +8,27 @@ Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be created on the spot. -:Example: How to get logger and log away at your heart's content: +Example: How to get logger and log away at your heart's content: + >>> import logging >>> logger = logging.getLogger('jb.awesome_module') >>> logger.info('Started general awesomeness aura') -Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: -`` -loggers: - jb: - level: WARNING - handlers: [console, debug_file_handler, error_file_handler] - propagate: no - jb.awesome_module: - level: DEBUG -`` - -.. note:: -The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense) -There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output +Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module: + + loggers: + jb: + level: WARNING + handlers: [console, debug_file_handler, error_file_handler] + propagate: no + jb.awesome_module: + level: DEBUG + + +> [!NOTE] +> The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes +> sense). +> There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output """ import sys import logging diff --git a/src/jukebox/run_configure_audio.py b/src/jukebox/run_configure_audio.py index 6bb0e70f6..93f0a4c6a 100755 --- a/src/jukebox/run_configure_audio.py +++ b/src/jukebox/run_configure_audio.py @@ -5,7 +5,7 @@ Will also setup equalizer and mono down mixer in the pulseaudio config file. Run this once after installation. Can be re-run at any time to change the settings. -For more information see :ref:`userguide/audio:Audio Configuration`. +For more information see [Audio Configuration](../../builders/audio.md#audio-configuration). """ import os import argparse diff --git a/src/jukebox/run_register_rfid_reader.py b/src/jukebox/run_register_rfid_reader.py index 3aa69735e..18a1614d8 100755 --- a/src/jukebox/run_register_rfid_reader.py +++ b/src/jukebox/run_register_rfid_reader.py @@ -3,10 +3,11 @@ Setup tool to configure the RFID Readers. Run this once to register and configure the RFID readers with the Jukebox. Can be re-run at any time to change -the settings. For more information see :ref:`rfid/rfid:RFID Readers`. +the settings. For more information see [RFID Readers](../rfid/README.md). -.. note:: This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). - Any manual modifications to the settings will have to be re-applied +> [!NOTE] +> This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). +> Any manual modifications to the settings will have to be re-applied """ import os diff --git a/src/jukebox/run_rpc_tool.py b/src/jukebox/run_rpc_tool.py index 5f84702d4..ad040983e 100755 --- a/src/jukebox/run_rpc_tool.py +++ b/src/jukebox/run_rpc_tool.py @@ -11,7 +11,7 @@ The list of available commands is fetched from the running Jukebox service. .. todo: - - kwargs support + - kwargs support """ From 1b6aafc7419482005b9f3e9498dbf5fe0cd62438 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 11:40:20 +0100 Subject: [PATCH 23/41] update docstring-md --- documentation/developers/docstring/docstring-api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/docstring-api.md index 177ef199c..5a910dbea 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/docstring-api.md @@ -602,7 +602,8 @@ Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for > [!NOTE] -> The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense). +> The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes +> sense). > There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output From 0de16d30529d2c22dc462e7c1cb271f67a5cf6b1 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 11:57:20 +0100 Subject: [PATCH 24/41] rename to README.md so github picks it up directly --- documentation/developers/README.md | 4 +- .../docstring/{docstring-api.md => README.md} | 67 ++++++++++--------- pydoc-markdown.yml | 2 +- 3 files changed, 37 insertions(+), 36 deletions(-) rename documentation/developers/docstring/{docstring-api.md => README.md} (98%) diff --git a/documentation/developers/README.md b/documentation/developers/README.md index 0e699bb2e..b6518a6e8 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -9,7 +9,7 @@ * [Jukebox Apps](./coreapps.md) * [RFID Readers](./rfid) -* [Docstring API Docs (from py files)](./docstring/docstring-api.md) -* [Plugin Reference](./docstring/docstring-api.md#jukeboxplugs) +* [Docstring API Docs (from py files)](./docstring/README.md) +* [Plugin Reference](./docstring/README.md#jukeboxplugs) * [Feature Status](./status.md) * [Known Issues](./known-issues.md) diff --git a/documentation/developers/docstring/docstring-api.md b/documentation/developers/docstring/README.md similarity index 98% rename from documentation/developers/docstring/docstring-api.md rename to documentation/developers/docstring/README.md index 5a910dbea..a6b0b5a61 100644 --- a/documentation/developers/docstring/docstring-api.md +++ b/documentation/developers/docstring/README.md @@ -949,8 +949,9 @@ def replay_if_stopped() Re-start playing the last-played folder unless playlist is still playing -.. note:: To me this seems much like the behaviour of play, - but we keep it as it is specifically implemented in box 2.X +> [!NOTE] +> To me this seems much like the behaviour of play, +> but we keep it as it is specifically implemented in box 2.X @@ -1315,10 +1316,11 @@ Returns :data:`True` if the sound card shall be changed when a new card connects property changes the behavior. -.. note:: A new card is always assumed to be the secondary device from the audio configuration. - At the moment there is no check it actually is the configured device. This means any new - device connection will initiate the toggle. This, however, is no real issue as the RPi's audio - system will be relatively stable once setup +> [!NOTE] +> A new card is always assumed to be the secondary device from the audio configuration. +> At the moment there is no check it actually is the configured device. This means any new +> device connection will initiate the toggle. This, however, is no real issue as the RPi's audio +> system will be relatively stable once setup @@ -2054,10 +2056,11 @@ This function is called to tell the reader to exist it's reading function. This function is called before cleanup is called. -.. note: This is usually called from a different thread than the reader's thread! And this is the reason for the - two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt - to read a card. Once called, the function read_card will not be called again. When the reader thread exits - cleanup is called from the reader thread itself. +> [!NOTE] +> This is usually called from a different thread than the reader's thread! And this is the reason for the +> two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt +> to read a card. Once called, the function read_card will not be called again. When the reader thread exits +> cleanup is called from the reader thread itself. @@ -2530,7 +2533,8 @@ def is_any_jukebox_service_active() Check if a Jukebox service is running -.. note:: Does not have the be the current app, that is running as a service! +> [!NOTE] +> Does not have the be the current app, that is running as a service! @@ -4772,9 +4776,9 @@ class Publisher() The publisher that provides the functional interface to the application -.. note:: - * An instance must not be shared across threads! - * One instance per thread is enough +> [!NOTE] +> * An instance must not be shared across threads! +> * One instance per thread is enough @@ -5426,13 +5430,13 @@ def get_all_failed_packages() -> Dict[str, str] Report those packages that did not load error free -.. note:: Package could fail to load - - 1. altogether: these package are not registered - 2. partially: during initializer, finalizer functions: The package is loaded, - but the function did not execute error-free - - Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED +> [!NOTE] +> Package could fail to load +> * altogether: these package are not registered +> * partially: during initializer, finalizer functions: The package is loaded, +> but the function did not execute error-free +> +> Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED **Returns**: @@ -5694,7 +5698,8 @@ def decode_rpc_call(cfg_rpc_call: Dict) -> Optional[Dict] Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. -.. important: Leaves all other parameters in cfg_action untouched or later downstream processing! +> [!IMPORTANT] +> Leaves all other parameters in cfg_action untouched or later downstream processing! **Arguments**: @@ -5717,8 +5722,8 @@ Decode an RPC Command from a config entry. This means - * Decode RPC command alias (if present) - * Ensure all RPC call parameters have valid default values +* Decode RPC command alias (if present) +* Ensure all RPC call parameters have valid default values If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call which emits a misuse warning when called @@ -5837,19 +5842,15 @@ Return git state information for the current branch # jukebox.rpc.server -Remote Procedure Call Server (RPC) - -************************************* +## Remote Procedure Call Server (RPC) Bind to tcp and/or websocket port and translates incoming requests to procedure calls. Avaiable procedures to call are all functions registered with the plugin package. -To protocol is loosely based on `jsonrpc `_ +The protocol is loosely based on [jsonrpc](https://www.jsonrpc.org/specification) But with different elements directly relating to the plugin concept and Python function argument options -.. code-block:: yaml - { 'package' : str # The plugin package loaded from python module 'plugin' : str # The plugin object to be accessed from the package @@ -5876,9 +5877,9 @@ If present, 'id' and 'tsp' may not be None. If they are None, there are treated Three sockets are opened -``. TCP (on a configurable port) -``. Websocket (on a configurable port) -``. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which button triggers what action) diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index f221a0bb6..96cb17916 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -8,5 +8,5 @@ processors: renderer: type: markdown render_toc: true - filename: ./documentation/developers/docstring/docstring-api.md + filename: ./documentation/developers/docstring/README.md render_page_title: true From c630ce0be7d80d8390517c058b934ecd48601d52 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 11:58:37 +0100 Subject: [PATCH 25/41] fix formatting --- documentation/builders/system.md | 5 +++-- src/jukebox/components/hostif/linux/__init__.py | 3 ++- src/jukebox/components/playermpd/__init__.py | 5 +++-- .../template_new_reader/template_new_reader.py | 9 +++++---- src/jukebox/components/volume/__init__.py | 9 +++++---- src/jukebox/jukebox/plugs.py | 14 +++++++------- src/jukebox/jukebox/publishing/server.py | 6 +++--- src/jukebox/jukebox/rpc/server.py | 13 +++++-------- src/jukebox/jukebox/utils.py | 7 ++++--- 9 files changed, 37 insertions(+), 34 deletions(-) diff --git a/documentation/builders/system.md b/documentation/builders/system.md index 873d489f6..9a502ff8b 100644 --- a/documentation/builders/system.md +++ b/documentation/builders/system.md @@ -10,8 +10,9 @@ The system consists of 4. [Web UI](system.md#web-ui) which is served through an Nginx web server 5. A set of [Configuration Tools](../developers/coreapps.md#configuration-tools) and a set of [Developer Tools](../developers/coreapps.md#developer-tools) -.. note:: The default install puts everything into the users home folder `~/RPi-Jukebox-RFID`. - Another folder might work, but is certainly not tested. +> [!NOTE] +> The default install puts everything into the users home folder `~/RPi-Jukebox-RFID`. +> Another folder might work, but is certainly not tested. ## Music Player Daemon (MPD) diff --git a/src/jukebox/components/hostif/linux/__init__.py b/src/jukebox/components/hostif/linux/__init__.py index 582074413..32dfded08 100644 --- a/src/jukebox/components/hostif/linux/__init__.py +++ b/src/jukebox/components/hostif/linux/__init__.py @@ -103,7 +103,8 @@ def jukebox_is_service(): def is_any_jukebox_service_active(): """Check if a Jukebox service is running - .. note:: Does not have the be the current app, that is running as a service! + > [!NOTE] + > Does not have the be the current app, that is running as a service! """ ret = subprocess.run(["systemctl", "--user", "show", "jukebox-daemon", "--property", "ActiveState", "--value"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index 3975fdb67..b30f1c945 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -366,8 +366,9 @@ def replay_if_stopped(self): """ Re-start playing the last-played folder unless playlist is still playing - .. note:: To me this seems much like the behaviour of play, - but we keep it as it is specifically implemented in box 2.X""" + > [!NOTE] + > To me this seems much like the behaviour of play, + > but we keep it as it is specifically implemented in box 2.X""" with self.mpd_lock: if self.mpd_status['state'] == 'stop': self.play_folder(self.music_player_status['player_status']['last_played_folder']) diff --git a/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py b/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py index b33399b0a..a054a3c1a 100644 --- a/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py +++ b/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py @@ -101,10 +101,11 @@ def stop(self): This function is called before cleanup is called. - .. note: This is usually called from a different thread than the reader's thread! And this is the reason for the - two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt - to read a card. Once called, the function read_card will not be called again. When the reader thread exits - cleanup is called from the reader thread itself. + > [!NOTE] + > This is usually called from a different thread than the reader's thread! And this is the reason for the + > two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt + > to read a card. Once called, the function read_card will not be called again. When the reader thread exits + > cleanup is called from the reader thread itself. """ self._keep_running = False diff --git a/src/jukebox/components/volume/__init__.py b/src/jukebox/components/volume/__init__.py index 6653baa77..187118366 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -149,10 +149,11 @@ def toggle_on_connect(self): """Returns :data:`True` if the sound card shall be changed when a new card connects/disconnects. Setting this property changes the behavior. - .. note:: A new card is always assumed to be the secondary device from the audio configuration. - At the moment there is no check it actually is the configured device. This means any new - device connection will initiate the toggle. This, however, is no real issue as the RPi's audio - system will be relatively stable once setup + > [!NOTE] + > A new card is always assumed to be the secondary device from the audio configuration. + > At the moment there is no check it actually is the configured device. This means any new + > device connection will initiate the toggle. This, however, is no real issue as the RPi's audio + > system will be relatively stable once setup """ return self._toggle_on_connect diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index 30374a29f..0dd378367 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -974,13 +974,13 @@ def get_all_loaded_packages() -> Dict[str, str]: def get_all_failed_packages() -> Dict[str, str]: """Report those packages that did not load error free - .. note:: Package could fail to load - - 1. altogether: these package are not registered - 2. partially: during initializer, finalizer functions: The package is loaded, - but the function did not execute error-free - - Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED + > [!NOTE] + > Package could fail to load + > * altogether: these package are not registered + > * partially: during initializer, finalizer functions: The package is loaded, + > but the function did not execute error-free + > + > Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED :return: Dictionary of the form `{loaded_as: loaded_from, ...}`""" with _lock_module: diff --git a/src/jukebox/jukebox/publishing/server.py b/src/jukebox/jukebox/publishing/server.py index 7db5b5846..19f51bf31 100644 --- a/src/jukebox/jukebox/publishing/server.py +++ b/src/jukebox/jukebox/publishing/server.py @@ -271,9 +271,9 @@ class Publisher: """ The publisher that provides the functional interface to the application - .. note:: - * An instance must not be shared across threads! - * One instance per thread is enough + > [!NOTE] + > * An instance must not be shared across threads! + > * One instance per thread is enough """ def __init__(self, check_thread_owner=True): diff --git a/src/jukebox/jukebox/rpc/server.py b/src/jukebox/jukebox/rpc/server.py index 8615bced7..b7e55b243 100644 --- a/src/jukebox/jukebox/rpc/server.py +++ b/src/jukebox/jukebox/rpc/server.py @@ -1,17 +1,14 @@ # -*- coding: utf-8 -*- """ -Remote Procedure Call Server (RPC) -************************************* +## Remote Procedure Call Server (RPC) Bind to tcp and/or websocket port and translates incoming requests to procedure calls. Avaiable procedures to call are all functions registered with the plugin package. -To protocol is loosely based on `jsonrpc `_ +The protocol is loosely based on [jsonrpc](https://www.jsonrpc.org/specification) But with different elements directly relating to the plugin concept and Python function argument options -.. code-block:: yaml - { 'package' : str # The plugin package loaded from python module 'plugin' : str # The plugin object to be accessed from the package @@ -38,9 +35,9 @@ Three sockets are opened -#. TCP (on a configurable port) -#. Websocket (on a configurable port) -#. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which button triggers what action) diff --git a/src/jukebox/jukebox/utils.py b/src/jukebox/jukebox/utils.py index 9be97b6db..dbd647490 100644 --- a/src/jukebox/jukebox/utils.py +++ b/src/jukebox/jukebox/utils.py @@ -17,7 +17,8 @@ def decode_rpc_call(cfg_rpc_call: Dict) -> Optional[Dict]: """Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. - .. important: Leaves all other parameters in cfg_action untouched or later downstream processing! + > [!IMPORTANT] + > Leaves all other parameters in cfg_action untouched or later downstream processing! :param cfg_rpc_call: RPC command as configuration entry :return: A fully populated deep copy of cfg_rpc_call @@ -41,8 +42,8 @@ def decode_rpc_command(cfg_rpc_cmd: Dict, logger: logging.Logger = log) -> Optio This means - * Decode RPC command alias (if present) - * Ensure all RPC call parameters have valid default values + * Decode RPC command alias (if present) + * Ensure all RPC call parameters have valid default values If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call which emits a misuse warning when called From 8106d06d4593d7395be3ccb2c9e0a14b27ab5cf0 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 12:16:55 +0100 Subject: [PATCH 26/41] improve docs --- documentation/builders/rpc-commands.md | 8 +- documentation/builders/system.md | 3 +- documentation/developers/docstring/README.md | 89 +++++++++---------- src/jukebox/jukebox/publishing/server.py | 92 ++++++++++---------- 4 files changed, 91 insertions(+), 101 deletions(-) diff --git a/documentation/builders/rpc-commands.md b/documentation/builders/rpc-commands.md index f578ccd99..285a078dc 100644 --- a/documentation/builders/rpc-commands.md +++ b/documentation/builders/rpc-commands.md @@ -1,6 +1,5 @@ # RPC Commands - We use the RPC commands when triggering actions from different inputs like a card swipe, a GPIO button press, etc. Triggering an action is equal to sending an RPC function call. In many places the command to send when an input is triggered is configurable in a YAML-file. @@ -98,10 +97,9 @@ do exactly the same, but use different ways of specifying the command. folder: path/to/folder recursive: True - -.. important:: *args* must be a **list** of arguments to be passed! Even if only a single argument is passed. - So, use *args: [value]*. We try catch mis-uses but that might not always work. - +> [!IMPORTANT] +> *args* must be a **list** of arguments to be passed! Even if only a single argument is passed. +> So, use *args: [value]*. We try catch mis-uses but that might not always work. You will find some more examples the configuration of the [Card Database](card-database.md) diff --git a/documentation/builders/system.md b/documentation/builders/system.md index 9a502ff8b..8c3467acf 100644 --- a/documentation/builders/system.md +++ b/documentation/builders/system.md @@ -35,7 +35,8 @@ $ systemctl --user start mpd $ systemctl --user stop mpd ``` -.. important:: Never start or enable the system-wide MPD service with `sudo systemctl start mpd`! +> [!IMPORTANT] +> Never start or enable the system-wide MPD service with `sudo systemctl start mpd`! To check if MPD is running or has issues, use diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index a6b0b5a61..c02fce30b 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -4585,34 +4585,30 @@ But: the use cases are very rare for that. I cannot think of one at the moment. # jukebox.publishing.server -Publishing Server - -******************** +## Publishing Server The common publishing server for the entire Jukebox using ZeroMQ -Structure ----------------- +### Structure -.. code-block:: text + +-----------------------+ + | functional interface | Publisher + | | - functional interface for single Thread + | PUB | - sends data to publisher (and thus across threads) + +-----------------------+ + | (1) + v + +-----------------------+ + | SUB (bind) | PublishServer + | | - Last Value (LV) Cache + | XPUB (bind) | - Subscriber notification and LV resend + +-----------------------+ - independent thread + | (2) + v - +-----------------------+ - | functional interface | Publisher - | | - functional interface for single Thread - | PUB | - sends data to publisher (and thus across threads) - +-----------------------+ - | (1) - v - +-----------------------+ - | SUB (bind) | PublishServer - | | - Last Value (LV) Cache - | XPUB (bind) | - Subscriber notification and LV resend - +-----------------------+ - independent thread - | (2) - v - -Connection (1): Internal connection - Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) +#### Connection (1): Internal connection + +Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) Protocol: Multi-part message @@ -4626,10 +4622,11 @@ Connection (1): Internal connection Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer and the message is not forwarded to the outside. This third part of the message is never forwarded -Connection (2): External connection - Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! - Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will - also get you all the branch topics. To get everything, subscribe to ``b''`` +#### Connection (2): External connection + +Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! +Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will +also get you all the branch topics. To get everything, subscribe to ``b''`` Protocol: Multi-part message @@ -4639,24 +4636,22 @@ Connection (2): External connection Part 2: Payload or Message in json serialization If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) -Why? Why? -------------- +### Why? Why? -Check out the `ZeroMQ Documentation `_ +Check out the [ZeroMQ Documentation](https://zguide.zeromq.org/docs/chapter5) for why you need a proxy in a good design. For use case, we made a few simplifications -Design Rationales -------------------- +### Design Rationales -* "If you need `millions of messages per second `_ +* "If you need [millions of messages per second](https://zguide.zeromq.org/docs/chapter5/`Pros`-and-Cons-of-Pub-Sub) sent to thousands of points, you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then - the `XSUB and XPUB `_" -* "Let’s imagine `our feed has an average of 100,000 100-byte messages a second - `_ [...]. + the [XSUB and XPUB](https://zguide.zeromq.org/docs/chapter5/`Last`-Value-Caching)" +* "Let’s imagine [our feed has an average of 100,000 100-byte messages a + second](https://zguide.zeromq.org/docs/chapter5/`High`-Speed-Subscribers-Black-Box-Pattern) [...]. While 100K messages a second is easy for a ZeroMQ application, ..." **But we have:** @@ -4687,8 +4682,7 @@ This means, we can use less complex patters than used for these high-speed, high * Publisher plugin is first plugin to be loaded * Due to Publisher - PublisherServer structure no further sequencing required -Plugin interactions and usage ------------------------------- +### Plugin interactions and usage RPC can trigger through function call in components/publishing plugin that @@ -4697,23 +4691,24 @@ RPC can trigger through function call in components/publishing plugin that Plugins publishing state information should publish initial state at @plugin.finalize -.. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is - required per thread. But the publisher instance **must** be thread-local! - Always go through :func:`publishing.get_publisher()`. +> [!IMPORTANT] +> Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is +> required per thread. But the publisher instance **must** be thread-local! +> Always go through :func:`publishing.get_publisher()`. **Sockets** Three sockets are opened: -``. TCP (on a configurable port) -``. Websocket (on a configurable port) -``. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules that want to know about the current state on event based updates. **Further ZeroMQ References:** -* `Working with Messages `_ -* `Multiple Threads `_ +* [Working with Messages](https://zguide.zeromq.org/docs/chapter2/`Working`-with-Messages) +* [Multiple Threads](https://zguide.zeromq.org/docs/chapter2/`Multithreading`-with-ZeroMQ) @@ -4730,7 +4725,7 @@ forwards them to the outside world Handles new subscriptions by sending out the entire cached state to **all** subscribers -The code is structures using a `Reactor Pattern `_ +The code is structures using a [Reactor Pattern](https://zguide.zeromq.org/docs/chapter5/`Using`-a-Reactor) diff --git a/src/jukebox/jukebox/publishing/server.py b/src/jukebox/jukebox/publishing/server.py index 19f51bf31..81c958b8d 100644 --- a/src/jukebox/jukebox/publishing/server.py +++ b/src/jukebox/jukebox/publishing/server.py @@ -1,31 +1,28 @@ """ -Publishing Server -******************** +## Publishing Server The common publishing server for the entire Jukebox using ZeroMQ -Structure ----------------- - -.. code-block:: text - - +-----------------------+ - | functional interface | Publisher - | | - functional interface for single Thread - | PUB | - sends data to publisher (and thus across threads) - +-----------------------+ - | (1) - v - +-----------------------+ - | SUB (bind) | PublishServer - | | - Last Value (LV) Cache - | XPUB (bind) | - Subscriber notification and LV resend - +-----------------------+ - independent thread - | (2) - v - -Connection (1): Internal connection - Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) +### Structure + + +-----------------------+ + | functional interface | Publisher + | | - functional interface for single Thread + | PUB | - sends data to publisher (and thus across threads) + +-----------------------+ + | (1) + v + +-----------------------+ + | SUB (bind) | PublishServer + | | - Last Value (LV) Cache + | XPUB (bind) | - Subscriber notification and LV resend + +-----------------------+ - independent thread + | (2) + v + +#### Connection (1): Internal connection + +Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) Protocol: Multi-part message @@ -39,10 +36,11 @@ Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer and the message is not forwarded to the outside. This third part of the message is never forwarded -Connection (2): External connection - Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! - Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will - also get you all the branch topics. To get everything, subscribe to ``b''`` +#### Connection (2): External connection + +Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! +Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will +also get you all the branch topics. To get everything, subscribe to ``b''`` Protocol: Multi-part message @@ -52,24 +50,22 @@ Part 2: Payload or Message in json serialization If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) -Why? Why? -------------- +### Why? Why? -Check out the `ZeroMQ Documentation `_ +Check out the [ZeroMQ Documentation](https://zguide.zeromq.org/docs/chapter5) for why you need a proxy in a good design. For use case, we made a few simplifications -Design Rationales -------------------- +### Design Rationales -* "If you need `millions of messages per second `_ +* "If you need [millions of messages per second](https://zguide.zeromq.org/docs/chapter5/#Pros-and-Cons-of-Pub-Sub) sent to thousands of points, you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then - the `XSUB and XPUB `_" -* "Let’s imagine `our feed has an average of 100,000 100-byte messages a second - `_ [...]. + the [XSUB and XPUB](https://zguide.zeromq.org/docs/chapter5/#Last-Value-Caching)" +* "Let’s imagine [our feed has an average of 100,000 100-byte messages a + second](https://zguide.zeromq.org/docs/chapter5/#High-Speed-Subscribers-Black-Box-Pattern) [...]. While 100K messages a second is easy for a ZeroMQ application, ..." **But we have:** @@ -100,8 +96,7 @@ * Publisher plugin is first plugin to be loaded * Due to Publisher - PublisherServer structure no further sequencing required -Plugin interactions and usage ------------------------------- +### Plugin interactions and usage RPC can trigger through function call in components/publishing plugin that @@ -110,23 +105,24 @@ Plugins publishing state information should publish initial state at @plugin.finalize -.. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is - required per thread. But the publisher instance **must** be thread-local! - Always go through :func:`publishing.get_publisher()`. +> [!IMPORTANT] +> Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is +> required per thread. But the publisher instance **must** be thread-local! +> Always go through :func:`publishing.get_publisher()`. **Sockets** Three sockets are opened: -#. TCP (on a configurable port) -#. Websocket (on a configurable port) -#. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules that want to know about the current state on event based updates. **Further ZeroMQ References:** -* `Working with Messages `_ -* `Multiple Threads `_ +* [Working with Messages](https://zguide.zeromq.org/docs/chapter2/#Working-with-Messages) +* [Multiple Threads](https://zguide.zeromq.org/docs/chapter2/#Multithreading-with-ZeroMQ) """ # Developer's notes: @@ -190,7 +186,7 @@ class PublishServer(threading.Thread): Handles new subscriptions by sending out the entire cached state to **all** subscribers - The code is structures using a `Reactor Pattern `_ + The code is structures using a [Reactor Pattern](https://zguide.zeromq.org/docs/chapter5/#Using-a-Reactor) """ def __init__(self, tcp_port, websocket_port): super().__init__(name='PubServer') From 7caec0e345b5510d87b253acf5b4b49d2dca6dbb Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 12:17:11 +0100 Subject: [PATCH 27/41] improve docs --- documentation/developers/docstring/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index c02fce30b..600c9a1ea 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -4650,7 +4650,7 @@ For use case, we made a few simplifications you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then the [XSUB and XPUB](https://zguide.zeromq.org/docs/chapter5/`Last`-Value-Caching)" -* "Let’s imagine [our feed has an average of 100,000 100-byte messages a +* "Let’s imagine [our feed has an average of 100,000 100-byte messages a second](https://zguide.zeromq.org/docs/chapter5/`High`-Speed-Subscribers-Black-Box-Pattern) [...]. While 100K messages a second is easy for a ZeroMQ application, ..." From 998310d91731e2968f11c69cda5c3b582cfd0326 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 12:34:08 +0100 Subject: [PATCH 28/41] fix docs --- documentation/developers/docstring/README.md | 55 +++++++++----------- src/jukebox/jukebox/plugs.py | 51 ++++++++---------- src/jukebox/jukebox/publishing/server.py | 4 +- 3 files changed, 50 insertions(+), 60 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index 600c9a1ea..5f56a966c 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -4647,10 +4647,10 @@ For use case, we made a few simplifications * "If you need [millions of messages per second](https://zguide.zeromq.org/docs/chapter5/`Pros`-and-Cons-of-Pub-Sub) sent to thousands of points, - you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." + you'll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then the [XSUB and XPUB](https://zguide.zeromq.org/docs/chapter5/`Last`-Value-Caching)" -* "Let’s imagine [our feed has an average of 100,000 100-byte messages a +* "Let's imagine [our feed has an average of 100,000 100-byte messages a second](https://zguide.zeromq.org/docs/chapter5/`High`-Speed-Subscribers-Black-Box-Pattern) [...]. While 100K messages a second is easy for a ZeroMQ application, ..." @@ -4901,32 +4901,32 @@ you can implement a Factory Pattern using this package. Take a look at volume.py def func1(param): pass -**Example:** Decorate a function for auto-registering under a new name:: +**Example:** Decorate a function for auto-registering under a new name: @plugs.register(name='better_name') def func2(param): pass -**Example:** Register a function during run-time under it's own name:: +**Example:** Register a function during run-time under it's own name: def func3(param): pass plugs.register(func3) -**Example:** Register a function during run-time under a new name:: +**Example:** Register a function during run-time under a new name: def func4(param): pass plugs.register(func4, name='other_name', package='other_package') **Example:** Decorate a class for auto registering during initialization, -including all methods (see _register_class for more info):: +including all methods (see _register_class for more info): @plugs.register(auto_tag=True) class MyClass1: pass -**Example:** Register a class instance, from which only report is a callable method through the plugs interface:: +**Example:** Register a class instance, from which only report is a callable method through the plugs interface: class MyClass2: @plugs.tag @@ -4937,20 +4937,17 @@ including all methods (see _register_class for more info):: Naming convention: -package - 1. Either a python package - 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) - -plugin - 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) - 2. The string name to above object - -name - The string name of the plugin object for registration - -method - 1. In case the object is a class instance a bound method to call from the class instance - 2. The string name to above object +* package + * Either a python package + * or a plugin package (which is the python package but probably loaded under a different name inside plugs) +* plugin + * An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) + * The string name to above object +* name + * The string name of the plugin object for registration +* method + * In case the object is a class instance a bound method to call from the class instance + * The string name to above object @@ -5050,15 +5047,13 @@ The functions comes in five distinct signatures for 5 use cases: 3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments 4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments 5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of - * function * bound method * class instance For more documentation see the functions - - * :func:`_register_obj` - * :func:`_register_class` +* :func:`_register_obj` +* :func:`_register_class` See the examples in Module :mod:`plugs` how to use this decorator / function @@ -5165,11 +5160,11 @@ def load(package: str, Loads a python package as plugin package -Executes a regular python package load. That means a potentially existing __init__.py is executed. -Decorator @register can by used to register functions / classes / class istances as plugin callable -Decorator @initializer can be used to tag functions that shall be called after package loading -Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded -Instead of using @initializer, you may of course use __init__.py +Executes a regular python package load. That means a potentially existing `__init__.py` is executed. +Decorator `@register` can by used to register functions / classes / class istances as plugin callable +Decorator `@initializer` can be used to tag functions that shall be called after package loading +Decorator `@finalizer` can be used to tag functions that shall be called after ALL plugin packges have been loaded +Instead of using `@initializer`, you may of course use `__init__.py` Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under which they are loaded as plugin package also. diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index 0dd378367..5e4a95f21 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -21,32 +21,32 @@ def func1(param): pass -**Example:** Decorate a function for auto-registering under a new name:: +**Example:** Decorate a function for auto-registering under a new name: @plugs.register(name='better_name') def func2(param): pass -**Example:** Register a function during run-time under it's own name:: +**Example:** Register a function during run-time under it's own name: def func3(param): pass plugs.register(func3) -**Example:** Register a function during run-time under a new name:: +**Example:** Register a function during run-time under a new name: def func4(param): pass plugs.register(func4, name='other_name', package='other_package') **Example:** Decorate a class for auto registering during initialization, -including all methods (see _register_class for more info):: +including all methods (see _register_class for more info): @plugs.register(auto_tag=True) class MyClass1: pass -**Example:** Register a class instance, from which only report is a callable method through the plugs interface:: +**Example:** Register a class instance, from which only report is a callable method through the plugs interface: class MyClass2: @plugs.tag @@ -57,20 +57,17 @@ def report(self): Naming convention: -package - 1. Either a python package - 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) - -plugin - 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) - 2. The string name to above object - -name - The string name of the plugin object for registration - -method - 1. In case the object is a class instance a bound method to call from the class instance - 2. The string name to above object +* package + * Either a python package + * or a plugin package (which is the python package but probably loaded under a different name inside plugs) +* plugin + * An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) + * The string name to above object +* name + * The string name of the plugin object for registration +* method + * In case the object is a class instance a bound method to call from the class instance + * The string name to above object """ @@ -405,15 +402,13 @@ def register(plugin: Optional[Callable] = None, *, 3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments 4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments 5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of - * function * bound method * class instance For more documentation see the functions - - * :func:`_register_obj` - * :func:`_register_class` + * :func:`_register_obj` + * :func:`_register_class` See the examples in Module :mod:`plugs` how to use this decorator / function @@ -528,11 +523,11 @@ def load(package: str, load_as: Optional[str] = None, prefix: Optional[str] = No """ Loads a python package as plugin package - Executes a regular python package load. That means a potentially existing __init__.py is executed. - Decorator @register can by used to register functions / classes / class istances as plugin callable - Decorator @initializer can be used to tag functions that shall be called after package loading - Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded - Instead of using @initializer, you may of course use __init__.py + Executes a regular python package load. That means a potentially existing `__init__.py` is executed. + Decorator `@register` can by used to register functions / classes / class istances as plugin callable + Decorator `@initializer` can be used to tag functions that shall be called after package loading + Decorator `@finalizer` can be used to tag functions that shall be called after ALL plugin packges have been loaded + Instead of using `@initializer`, you may of course use `__init__.py` Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under which they are loaded as plugin package also. diff --git a/src/jukebox/jukebox/publishing/server.py b/src/jukebox/jukebox/publishing/server.py index 81c958b8d..66729da6e 100644 --- a/src/jukebox/jukebox/publishing/server.py +++ b/src/jukebox/jukebox/publishing/server.py @@ -61,10 +61,10 @@ * "If you need [millions of messages per second](https://zguide.zeromq.org/docs/chapter5/#Pros-and-Cons-of-Pub-Sub) sent to thousands of points, - you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." + you'll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then the [XSUB and XPUB](https://zguide.zeromq.org/docs/chapter5/#Last-Value-Caching)" -* "Let’s imagine [our feed has an average of 100,000 100-byte messages a +* "Let's imagine [our feed has an average of 100,000 100-byte messages a second](https://zguide.zeromq.org/docs/chapter5/#High-Speed-Subscribers-Black-Box-Pattern) [...]. While 100K messages a second is easy for a ZeroMQ application, ..." From f1bc6578bb2b794967d72126873ed09548e2a980 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 12:37:43 +0100 Subject: [PATCH 29/41] updte docstream md --- documentation/developers/docstring/README.md | 65 ++++++++++---------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index 5f56a966c..928694358 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -7,6 +7,7 @@ * [run\_register\_rfid\_reader](#run_register_rfid_reader) * [run\_rpc\_tool](#run_rpc_tool) * [get\_common\_beginning](#run_rpc_tool.get_common_beginning) + * [runcmd](#run_rpc_tool.runcmd) * [run\_configure\_audio](#run_configure_audio) * [run\_publicity\_sniffer](#run_publicity_sniffer) * [misc](#misc) @@ -40,12 +41,14 @@ * [toggle](#components.playermpd.PlayerMPD.toggle) * [replay\_if\_stopped](#components.playermpd.PlayerMPD.replay_if_stopped) * [play\_card](#components.playermpd.PlayerMPD.play_card) + * [get\_single\_coverart](#components.playermpd.PlayerMPD.get_single_coverart) * [get\_folder\_content](#components.playermpd.PlayerMPD.get_folder_content) * [play\_folder](#components.playermpd.PlayerMPD.play_folder) * [play\_album](#components.playermpd.PlayerMPD.play_album) * [get\_volume](#components.playermpd.PlayerMPD.get_volume) * [set\_volume](#components.playermpd.PlayerMPD.set_volume) * [play\_card\_callbacks](#components.playermpd.play_card_callbacks) +* [components.playermpd.coverart\_cache\_manager](#components.playermpd.coverart_cache_manager) * [components.rpc\_command\_alias](#components.rpc_command_alias) * [components.synchronisation.rfidcards](#components.synchronisation.rfidcards) * [SyncRfidcards](#components.synchronisation.rfidcards.SyncRfidcards) @@ -182,9 +185,6 @@ * [\_\_init\_\_](#components.controls.common.evdev_listener.EvDevKeyListener.__init__) * [run](#components.controls.common.evdev_listener.EvDevKeyListener.run) * [start](#components.controls.common.evdev_listener.EvDevKeyListener.start) -* [components.music\_cover\_art](#components.music_cover_art) - * [MusicCoverArt](#components.music_cover_art.MusicCoverArt) - * [get\_by\_filename\_as\_base64](#components.music_cover_art.MusicCoverArt.get_by_filename_as_base64) * [components.battery\_monitor](#components.battery_monitor) * [components.battery\_monitor.BatteryMonitorBase](#components.battery_monitor.BatteryMonitorBase) * [pt1\_frac](#components.battery_monitor.BatteryMonitorBase.pt1_frac) @@ -443,6 +443,20 @@ def get_common_beginning(strings) Return the strings that are common to the beginning of each string in the strings list. + + +#### runcmd + +```python +def runcmd(cmd) +``` + +Just run a command. + +Right now duplicates more or less main() +:todo remove duplication of code + + # run\_configure\_audio @@ -973,6 +987,18 @@ accordingly. - `folder`: Folder path relative to music library path - `recursive`: Add folder recursively + + +#### get\_single\_coverart + +```python +@plugs.tag +def get_single_coverart(song_url) +``` + +Saves the album art image to a cache and returns the filename. + + #### get\_folder\_content @@ -1068,6 +1094,10 @@ States: See :class:`PlayContentCallbacks` + + +# components.playermpd.coverart\_cache\_manager + # components.rpc\_command\_alias @@ -2874,35 +2904,6 @@ def start() -> None Start the tread and start listening - - -# components.music\_cover\_art - -Read all cover art from music save it to a cache for the UI to load - -.. note:: Not implemented. This is a feature planned for a future release. - - - - -## MusicCoverArt Objects - -```python -class MusicCoverArt() -``` - - - -#### get\_by\_filename\_as\_base64 - -```python -@plugin.tag -def get_by_filename_as_base64(audio_src: str) -``` - -Not implemented. This is a feature planned for a future release. - - # components.battery\_monitor From 3315e1579d4f7a7e3e20832ec06bfa9db18da38c Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 13:13:57 +0100 Subject: [PATCH 30/41] fix formatting for md --- documentation/developers/docstring/README.md | 209 +++++++++--------- .../batt_mon_i2c_ads1015/__init__.py | 53 +++-- .../bluetooth_audio_buttons/__init__.py | 6 +- .../controls/common/evdev_listener.py | 6 +- .../gpio/gpioz/core/input_devices.py | 3 +- .../components/gpio/gpioz/core/mock.py | 2 +- .../gpio/gpioz/core/output_devices.py | 3 +- .../components/gpio/gpioz/plugin/__init__.py | 12 +- .../gpio/gpioz/plugin/connectivity.py | 36 +-- src/jukebox/components/jingle/__init__.py | 11 +- src/jukebox/components/rpc_command_alias.py | 2 +- src/jukebox/components/volume/__init__.py | 66 +++--- src/jukebox/jukebox/cfghandler.py | 4 +- 13 files changed, 207 insertions(+), 206 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index 928694358..ed41028ac 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -1104,7 +1104,7 @@ See :class:`PlayContentCallbacks` This file provides definitions for RPC command aliases -See :ref:`userguide/rpc_commands` +See [../../builders/rpc-commands.md](RPC Commands) @@ -1213,33 +1213,35 @@ Sync the folder from the remote server, if existing PulseAudio Volume Control Plugin Package -Features +## Features - * Volume Control - * Two outputs - * Watcher thread on volume / output change +* Volume Control +* Two outputs +* Watcher thread on volume / output change -Publishes +## Publishes - * volume.level - * volume.sink +* volume.level +* volume.sink -PulseAudio References +## PulseAudio References -https://brokkr.net/2018/05/24/down-the-drain-the-elusive-default-pulseaudio-sink/ + Check fallback device (on device de-connect): -$ pacmd list-sinks | grep -e 'name:' -e 'index' + $ pacmd list-sinks | grep -e 'name:' -e 'index' -Integration + +## Integration Pulse Audio runs as a user process. Processes who want to communicate / stream to it must also run as a user process. -This means must also run as user process, as described in :ref:`userguide/system:Music Player Daemon (MPD)` +This means must also run as user process, as described in +[../../builders/system.md#music-player-daemon-mpd](Music Player Daemon). -Misc +## Misc PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration @@ -1247,27 +1249,25 @@ in ``/usr/pulse/default.pa``. So, we don't need to worry about it. If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs from the Jukebox. Remove it from the configuration! -.. code-block:: text - ### Use hot-plugged devices like Bluetooth or USB automatically (LP: `1702794`) ### not available on PI? .ifexists module-switch-on-connect.so load-module module-switch-on-connect .endif -Why PulseAudio? +## Why PulseAudio? The audio configuration of the system is one of those topics, which has a myriad of options and possibilities. Every system is different and PulseAudio unifies these and makes our life easier. Besides, it is only option to support Bluetooth at the moment. -Callbacks: +## Callbacks: The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): - ``. :func:`add_on_connect_callback` - ``. :func:`add_on_output_change_callbacks` - ``. :func:`add_on_volume_change_callback` +1. :func:`add_on_connect_callback` +2. :func:`add_on_output_change_callbacks` +3. :func:`add_on_volume_change_callback` @@ -1317,11 +1317,12 @@ Callback signature is .. py:function:: func(card_driver: str, device_name: str) :noindex: - :param card_driver: The PulseAudio card driver module, - e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` - :param device_name: The sound card device name as reported - in device properties +**Arguments**: +- `card_driver`: The PulseAudio card driver module, +e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` +- `device_name`: The sound card device name as reported +in device properties @@ -1440,13 +1441,14 @@ Callback signature is .. py:function:: func(sink_name: str, alias: str, sink_index: int, error_state: int) :noindex: - :param sink_name: PulseAudio's sink name - :param alias: The alias for :attr:`sink_name` - :param sink_index: The index of the sink in the configuration list - :param error_state: 1 if there was an attempt to change the output - but an error occurred. Above parameters always give the now valid sink! - If a sink change is successful, it is 0. +**Arguments**: +- `sink_name`: PulseAudio's sink name +- `alias`: The alias for :attr:`sink_name` +- `sink_index`: The index of the sink in the configuration list +- `error_state`: 1 if there was an attempt to change the output +but an error occurred. Above parameters always give the now valid sink! +If a sink change is successful, it is 0. @@ -1486,10 +1488,11 @@ Callback signature is .. py:function:: func(volume: int, is_min: bool, is_max: bool) :noindex: - :param volume: Volume level - :param is_min: 1, if volume level is minimum, else 0 - :param is_max: 1, if volume level is maximum, else 0 +**Arguments**: +- `volume`: Volume level +- `is_min`: 1, if volume level is minimum, else 0 +- `is_max`: 1, if volume level is maximum, else 0 @@ -2364,11 +2367,12 @@ def play(filename) Play the jingle using the configured jingle service -Note: This runs in a separate thread. And this may cause troubles -when changing the volume level before -and after the sound playback: There is nothing to prevent another -thread from changing the volume and sink while playback happens -and afterwards we change the volume back to where it was before! +> [!NOTE] +> This runs in a separate thread. And this may cause troubles +> when changing the volume level before +> and after the sound playback: There is nothing to prevent another +> thread from changing the volume and sink while playback happens +> and afterwards we change the volume back to where it was before! There is no way around this dilemma except for not running the jingle as a separate thread. Currently (as thread) even the RPC is started before the sound @@ -2809,9 +2813,9 @@ when a bluetooth sound device (headphone, speakers) connects This effectively does: - * register a callback with components.volume to get notified when a new sound card connects - * if that is a bluetooth device, try opening an input device with similar name using - * button listeners are run each in its own thread +* register a callback with components.volume to get notified when a new sound card connects +* if that is a bluetooth device, try opening an input device with similar name using +* button listeners are run each in its own thread @@ -2833,19 +2837,18 @@ def find_device(device_name: str, Find an input device with device_name and mandatory keys. -Raises - - ``. FileNotFoundError, if no device is found. - ``. AttributeError, if device does not have the mandatory keys - -If multiple devices match, the first match is returned - **Arguments**: - `device_name`: See :func:`_filter_by_device_name` - `exact_name`: See :func:`_filter_by_device_name` - `mandatory_keys`: See :func:`_filter_by_mandatory_keys` +**Raises**: + +- `FileNotFoundError`: if no device is found. +- `AttributeError`: if device does not have the mandatory key +If multiple devices match, the first match is returned + **Returns**: The path to the device @@ -2963,37 +2966,34 @@ class battmon_ads1015(BatteryMonitorBase.BattmonBase) Battery Monitor based on a ADS1015 -CAUTION - WARNING -======================================================================== -Lithium and other batteries are dangerous and must be treated with care. -Rechargeable Lithium Ion batteries are potentially hazardous and can -present a serious FIRE HAZARD if damaged, defective or improperly used. -Do not use this circuit to a lithium ion battery without expertise and -training in handling and use of batteries of this type. -Use appropriate test equipment and safety protocols during development. - -There is no warranty, this may not work as expected or at all! -========================================================================= +> [!CAUTION] +> Lithium and other batteries are dangerous and must be treated with care. +> Rechargeable Lithium Ion batteries are potentially hazardous and can +> present a serious **FIRE HAZARD** if damaged, defective or improperly used. +> Do not use this circuit to a lithium ion battery without expertise and +> training in handling and use of batteries of this type. +> Use appropriate test equipment and safety protocols during development. +> There is no warranty, this may not work as expected or at all! This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: - 3.3V - + - | - .----o----. - ___ | | SDA - .--------|___|---o----o---------o AIN0 o------ - | 2MΩ | | | | SCL - | .-. | | ADS1015 o------ - --- | | --- | | - Battery - 1.5MΩ| | ---100nF '----o----' - 2.9V-4.2V| '-' | | - | | | | - === === === === + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === Attention: - - the circuit is constantly draining the battery! (leak current up to: 2.1µA) - - the time between sample needs to be a minimum 1sec with this high impedance voltage divider +* the circuit is constantly draining the battery! (leak current up to: 2.1µA) +* the time between sample needs to be a minimum 1sec with this high impedance voltage divider don't use the continuous conversion method! @@ -3063,14 +3063,14 @@ class ServiceIsRunningCallbacks(CallbackHandler) Callbacks are executed when * Jukebox app started - * Jukebox shuts down +* Jukebox shuts down This is intended to e.g. signal an LED to change state. This is integrated into this module because: - * we need the GPIO to control a LED (it must be available when the status callback comes) - * the plugin callback functions provide all the functionality to control the status of the LED - * which means no need to adapt other modules +* we need the GPIO to control a LED (it must be available when the status callback comes) +* the plugin callback functions provide all the functionality to control the status of the LED +* which means no need to adapt other modules @@ -3088,8 +3088,9 @@ Callback signature is .. py:function:: func(status: int) :noindex: - :param status: 1 if app started, 0 if app shuts down +**Arguments**: +- `status`: 1 if app started, 0 if app shuts down @@ -3261,11 +3262,11 @@ Flash the output device once on successful RFID card detection and thrice if car Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` +* :class:`components.gpio.gpioz.core.output_devices.LED` +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` +* :class:`components.gpio.gpioz.core.output_devices.Buzzer` +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` @@ -3280,9 +3281,9 @@ Turn LED on when Jukebox App has started Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` +* :class:`components.gpio.gpioz.core.output_devices.LED` +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` @@ -3297,8 +3298,8 @@ Buzz once when Jukebox App has started, twice when closing down Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` +* :class:`components.gpio.gpioz.core.output_devices.Buzzer` +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` @@ -3313,7 +3314,7 @@ Buzz a multi-note melody when Jukebox App has started and when closing down Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` @@ -3330,9 +3331,9 @@ fails, blink thrice Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` +* :class:`components.gpio.gpioz.core.output_devices.LED` +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` @@ -3349,7 +3350,7 @@ is reached. Minimum value is still a very dimly turned on LED (i.e. LED is never Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` @@ -3364,8 +3365,8 @@ Sound a buzzer once when minimum or maximum value is reached Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` +* :class:`components.gpio.gpioz.core.output_devices.Buzzer` +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` @@ -3382,7 +3383,7 @@ is reached. Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` @@ -3476,7 +3477,7 @@ Monkey Patch LED + Buzzer to get a callback when state changes This targets to represent the state in the TK GUI. Other output devices cannot be represented in the GUI and are silently ignored. -..note:: Only for developing purposes! +> [!NOTE] Only for developing purposes! @@ -3491,7 +3492,8 @@ their documentation. All callback handlers are replaced by GPIOZ callback handlers. These are usually configured by using the :func:`set_rpc_actions` each input device exhibits. -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Input devices` +For examples how to use the devices from the configuration files, see +[../../builders/gpio.md#input-devices](GPIO: Input Devices). @@ -3867,7 +3869,8 @@ to all devices. This function provides a unified API to all devices. This means with parameters for this device and optional parameters from another device. Unused/unsupported parameters are silently ignored. This is done to reduce the amount of coding required for connectivity functions. -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Output devices` +For examples how to use the devices from the configuration files, see +[../../builders/gpio.md#output-devices](GPIO: Output Devices). @@ -4292,8 +4295,8 @@ def is_modified() -> bool Check if the data has changed since the last load/store -.. note: This relies on the *__str__* representation of the underlying data structure - In case of ruamel, this ignores comments and only looks at the data +> [!NOTE] This relies on the *__str__* representation of the underlying data structure +> In case of ruamel, this ignores comments and only looks at the data diff --git a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py index 04459c9ea..af193a37f 100644 --- a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py +++ b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py @@ -35,42 +35,39 @@ class battmon_ads1015(BatteryMonitorBase.BattmonBase): - '''Battery Monitor based on a ADS1015 + """Battery Monitor based on a ADS1015 - CAUTION - WARNING - ======================================================================== - Lithium and other batteries are dangerous and must be treated with care. - Rechargeable Lithium Ion batteries are potentially hazardous and can - present a serious FIRE HAZARD if damaged, defective or improperly used. - Do not use this circuit to a lithium ion battery without expertise and - training in handling and use of batteries of this type. - Use appropriate test equipment and safety protocols during development. - - There is no warranty, this may not work as expected or at all! - ========================================================================= + > [!CAUTION] + > Lithium and other batteries are dangerous and must be treated with care. + > Rechargeable Lithium Ion batteries are potentially hazardous and can + > present a serious **FIRE HAZARD** if damaged, defective or improperly used. + > Do not use this circuit to a lithium ion battery without expertise and + > training in handling and use of batteries of this type. + > Use appropriate test equipment and safety protocols during development. + > There is no warranty, this may not work as expected or at all! This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: - 3.3V - + - | - .----o----. - ___ | | SDA - .--------|___|---o----o---------o AIN0 o------ - | 2MΩ | | | | SCL - | .-. | | ADS1015 o------ - --- | | --- | | - Battery - 1.5MΩ| | ---100nF '----o----' - 2.9V-4.2V| '-' | | - | | | | - === === === === + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === Attention: - - the circuit is constantly draining the battery! (leak current up to: 2.1µA) - - the time between sample needs to be a minimum 1sec with this high impedance voltage divider + * the circuit is constantly draining the battery! (leak current up to: 2.1µA) + * the time between sample needs to be a minimum 1sec with this high impedance voltage divider don't use the continuous conversion method! - ''' + """ def __init__(self, cfg): super().__init__(cfg, logger) diff --git a/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py b/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py index 999a4f218..4d17f398e 100644 --- a/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py +++ b/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py @@ -4,9 +4,9 @@ This effectively does: - * register a callback with components.volume to get notified when a new sound card connects - * if that is a bluetooth device, try opening an input device with similar name using - * button listeners are run each in its own thread +* register a callback with components.volume to get notified when a new sound card connects +* if that is a bluetooth device, try opening an input device with similar name using +* button listeners are run each in its own thread """ import logging diff --git a/src/jukebox/components/controls/common/evdev_listener.py b/src/jukebox/components/controls/common/evdev_listener.py index 05b92005c..a4279afda 100644 --- a/src/jukebox/components/controls/common/evdev_listener.py +++ b/src/jukebox/components/controls/common/evdev_listener.py @@ -49,10 +49,8 @@ def _filter_by_device_name(all_devices: List[evdev.InputDevice], def find_device(device_name: str, exact_name: bool = True, mandatory_keys: Optional[Set[int]] = None) -> str: """Find an input device with device_name and mandatory keys. - Raises - - #. FileNotFoundError, if no device is found. - #. AttributeError, if device does not have the mandatory keys + :raise FileNotFoundError: if no device is found. + :raise AttributeError: if device does not have the mandatory key If multiple devices match, the first match is returned diff --git a/src/jukebox/components/gpio/gpioz/core/input_devices.py b/src/jukebox/components/gpio/gpioz/core/input_devices.py index 5090762f4..fc0727d2c 100644 --- a/src/jukebox/components/gpio/gpioz/core/input_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/input_devices.py @@ -9,7 +9,8 @@ All callback handlers are replaced by GPIOZ callback handlers. These are usually configured by using the :func:`set_rpc_actions` each input device exhibits. -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Input devices` +For examples how to use the devices from the configuration files, see +[../../builders/gpio.md#input-devices](GPIO: Input Devices). """ import functools diff --git a/src/jukebox/components/gpio/gpioz/core/mock.py b/src/jukebox/components/gpio/gpioz/core/mock.py index bccd5e0e1..e30fd8714 100644 --- a/src/jukebox/components/gpio/gpioz/core/mock.py +++ b/src/jukebox/components/gpio/gpioz/core/mock.py @@ -19,7 +19,7 @@ def patch_mock_outputs_with_callback(): This targets to represent the state in the TK GUI. Other output devices cannot be represented in the GUI and are silently ignored. - ..note:: Only for developing purposes!""" + > [!NOTE] Only for developing purposes!""" gpiozero.LED._write_orig = gpiozero.LED._write gpiozero.LED._write = rewrite gpiozero.LED.on_change_callback = None diff --git a/src/jukebox/components/gpio/gpioz/core/output_devices.py b/src/jukebox/components/gpio/gpioz/core/output_devices.py index 50949f82b..1cf9384ae 100644 --- a/src/jukebox/components/gpio/gpioz/core/output_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/output_devices.py @@ -11,7 +11,8 @@ with parameters for this device and optional parameters from another device. Unused/unsupported parameters are silently ignored. This is done to reduce the amount of coding required for connectivity functions. -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Output devices` +For examples how to use the devices from the configuration files, see +[../../builders/gpio.md#output-devices](GPIO: Output Devices). """ from typing import Optional, List diff --git a/src/jukebox/components/gpio/gpioz/plugin/__init__.py b/src/jukebox/components/gpio/gpioz/plugin/__init__.py index 9bc151e55..6fc9ab973 100644 --- a/src/jukebox/components/gpio/gpioz/plugin/__init__.py +++ b/src/jukebox/components/gpio/gpioz/plugin/__init__.py @@ -56,15 +56,15 @@ class ServiceIsRunningCallbacks(CallbackHandler): """ Callbacks are executed when - * Jukebox app started - * Jukebox shuts down + * Jukebox app started + * Jukebox shuts down This is intended to e.g. signal an LED to change state. This is integrated into this module because: - * we need the GPIO to control a LED (it must be available when the status callback comes) - * the plugin callback functions provide all the functionality to control the status of the LED - * which means no need to adapt other modules + * we need the GPIO to control a LED (it must be available when the status callback comes) + * the plugin callback functions provide all the functionality to control the status of the LED + * which means no need to adapt other modules """ def register(self, func: Callable[[int], None]): @@ -76,7 +76,7 @@ def register(self, func: Callable[[int], None]): .. py:function:: func(status: int) :noindex: - :param status: 1 if app started, 0 if app shuts down + :param status: 1 if app started, 0 if app shuts down """ super().register(func) diff --git a/src/jukebox/components/gpio/gpioz/plugin/connectivity.py b/src/jukebox/components/gpio/gpioz/plugin/connectivity.py index 3e5baea2d..abbcb1a32 100644 --- a/src/jukebox/components/gpio/gpioz/plugin/connectivity.py +++ b/src/jukebox/components/gpio/gpioz/plugin/connectivity.py @@ -55,11 +55,11 @@ def register_rfid_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.LED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.Buzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def rfid_callback(card_id: str, state: RfidCardDetectState): @@ -78,9 +78,9 @@ def register_status_led_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.LED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` """ def set_status_led(state): @@ -101,8 +101,8 @@ def register_status_buzzer_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.Buzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def set_status_buzzer(state): @@ -121,7 +121,7 @@ def register_status_tonalbuzzer_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def set_status_buzzer(state): @@ -143,9 +143,9 @@ def register_audio_sink_change_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.LED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` """ def audio_sink_change_callback(alias, sink_name, sink_index, error_state): @@ -167,7 +167,7 @@ def register_volume_led_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` """ def audio_volume_change_callback(volume, is_min, is_max): @@ -191,8 +191,8 @@ def register_volume_buzzer_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.Buzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def set_volume_buzzer(volume, is_min, is_max): @@ -210,7 +210,7 @@ def register_volume_rgbled_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` """ volume_to_rgb = VolumeToRGB(100, 120, 180) diff --git a/src/jukebox/components/jingle/__init__.py b/src/jukebox/components/jingle/__init__.py index 43e55cd74..940015dd5 100644 --- a/src/jukebox/components/jingle/__init__.py +++ b/src/jukebox/components/jingle/__init__.py @@ -56,11 +56,12 @@ def initialize(): def play(filename): """Play the jingle using the configured jingle service - Note: This runs in a separate thread. And this may cause troubles - when changing the volume level before - and after the sound playback: There is nothing to prevent another - thread from changing the volume and sink while playback happens - and afterwards we change the volume back to where it was before! + > [!NOTE] + > This runs in a separate thread. And this may cause troubles + > when changing the volume level before + > and after the sound playback: There is nothing to prevent another + > thread from changing the volume and sink while playback happens + > and afterwards we change the volume back to where it was before! There is no way around this dilemma except for not running the jingle as a separate thread. Currently (as thread) even the RPC is started before the sound diff --git a/src/jukebox/components/rpc_command_alias.py b/src/jukebox/components/rpc_command_alias.py index bb484891a..1ba5695f1 100644 --- a/src/jukebox/components/rpc_command_alias.py +++ b/src/jukebox/components/rpc_command_alias.py @@ -1,7 +1,7 @@ """ This file provides definitions for RPC command aliases -See :ref:`userguide/rpc_commands` +See [../../builders/rpc-commands.md](RPC Commands) """ # -------------------------------------------------------------- diff --git a/src/jukebox/components/volume/__init__.py b/src/jukebox/components/volume/__init__.py index 187118366..f2566ed83 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -2,33 +2,35 @@ # Copyright (c) See file LICENSE in project root folder """PulseAudio Volume Control Plugin Package -Features +## Features - * Volume Control - * Two outputs - * Watcher thread on volume / output change +* Volume Control +* Two outputs +* Watcher thread on volume / output change -Publishes +## Publishes - * volume.level - * volume.sink +* volume.level +* volume.sink -PulseAudio References +## PulseAudio References -https://brokkr.net/2018/05/24/down-the-drain-the-elusive-default-pulseaudio-sink/ + Check fallback device (on device de-connect): -$ pacmd list-sinks | grep -e 'name:' -e 'index' + $ pacmd list-sinks | grep -e 'name:' -e 'index' -Integration + +## Integration Pulse Audio runs as a user process. Processes who want to communicate / stream to it must also run as a user process. -This means must also run as user process, as described in :ref:`userguide/system:Music Player Daemon (MPD)` +This means must also run as user process, as described in +[../../builders/system.md#music-player-daemon-mpd](Music Player Daemon). -Misc +## Misc PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration @@ -36,27 +38,25 @@ If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs from the Jukebox. Remove it from the configuration! -.. code-block:: text - ### Use hot-plugged devices like Bluetooth or USB automatically (LP: #1702794) ### not available on PI? .ifexists module-switch-on-connect.so load-module module-switch-on-connect .endif -Why PulseAudio? +## Why PulseAudio? The audio configuration of the system is one of those topics, which has a myriad of options and possibilities. Every system is different and PulseAudio unifies these and makes our life easier. Besides, it is only option to support Bluetooth at the moment. -Callbacks: +## Callbacks: The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): - #. :func:`add_on_connect_callback` - #. :func:`add_on_output_change_callbacks` - #. :func:`add_on_volume_change_callback` +1. :func:`add_on_connect_callback` +2. :func:`add_on_output_change_callbacks` +3. :func:`add_on_volume_change_callback` """ import collections import logging @@ -116,10 +116,10 @@ def register(self, func: Callable[[str, str], None]): .. py:function:: func(card_driver: str, device_name: str) :noindex: - :param card_driver: The PulseAudio card driver module, - e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` - :param device_name: The sound card device name as reported - in device properties + :param card_driver: The PulseAudio card driver module, + e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` + :param device_name: The sound card device name as reported + in device properties """ super().register(func) @@ -310,12 +310,12 @@ def register(self, func: Callable[[str, str, int, int], None]): .. py:function:: func(sink_name: str, alias: str, sink_index: int, error_state: int) :noindex: - :param sink_name: PulseAudio's sink name - :param alias: The alias for :attr:`sink_name` - :param sink_index: The index of the sink in the configuration list - :param error_state: 1 if there was an attempt to change the output - but an error occurred. Above parameters always give the now valid sink! - If a sink change is successful, it is 0. + :param sink_name: PulseAudio's sink name + :param alias: The alias for :attr:`sink_name` + :param sink_index: The index of the sink in the configuration list + :param error_state: 1 if there was an attempt to change the output + but an error occurred. Above parameters always give the now valid sink! + If a sink change is successful, it is 0. """ super().register(func) @@ -339,9 +339,9 @@ def register(self, func: Callable[[int, bool, bool], None]): .. py:function:: func(volume: int, is_min: bool, is_max: bool) :noindex: - :param volume: Volume level - :param is_min: 1, if volume level is minimum, else 0 - :param is_max: 1, if volume level is maximum, else 0 + :param volume: Volume level + :param is_min: 1, if volume level is minimum, else 0 + :param is_max: 1, if volume level is maximum, else 0 """ super().register(func) diff --git a/src/jukebox/jukebox/cfghandler.py b/src/jukebox/jukebox/cfghandler.py index 3482a1b42..e378a1bed 100644 --- a/src/jukebox/jukebox/cfghandler.py +++ b/src/jukebox/jukebox/cfghandler.py @@ -236,8 +236,8 @@ def is_modified(self) -> bool: """ Check if the data has changed since the last load/store - .. note: This relies on the *__str__* representation of the underlying data structure - In case of ruamel, this ignores comments and only looks at the data + > [!NOTE] This relies on the *__str__* representation of the underlying data structure + > In case of ruamel, this ignores comments and only looks at the data """ with self._lock: is_modified_value = self._hash != hashlib.md5(self._data.__str__().encode('utf8')).digest() From 256c4a01eb66b882ba39e1a75bd91bcc752e63f1 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 13:55:48 +0100 Subject: [PATCH 31/41] fix formatting to md and check crossref --- documentation/developers/docstring/README.md | 15 +++++---------- pydoc-markdown.yml | 1 + .../components/gpio/gpioz/core/converter.py | 2 -- .../components/gpio/gpioz/core/input_devices.py | 2 +- .../template_new_reader/template_new_reader.py | 2 +- src/jukebox/components/rfid/reader/__init__.py | 4 ++-- src/jukebox/components/volume/__init__.py | 2 -- src/jukebox/jukebox/playlistgenerator.py | 2 -- 8 files changed, 10 insertions(+), 20 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index ed41028ac..1de994543 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -1401,8 +1401,6 @@ Volume control manager for PulseAudio When accessing the pulse library, it needs to be put into a special state. Which is ensured by the context manager -.. code-block: python - with pulse_monitor as pulse ... @@ -1693,9 +1691,10 @@ Callback signature is .. py:function:: func(card_id: str, state: int) :noindex: - :param card_id: Card ID - :param state: See :class:`RfidCardDetectState` +**Arguments**: +- `card_id`: Card ID +- `state`: See :class:`RfidCardDetectState` @@ -2041,7 +2040,7 @@ which ensures proper resource de-allocation. For this to work derive this class All the required interfaces are implemented there. Put your code into these functions (see below for more information) - - __init__ + - `__init__` - read_card - cleanup - stop @@ -3424,8 +3423,6 @@ traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) - `section`: The section of the full color circle to use in degrees Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 -.. code-block:: python - conv = VolumeToRGB(100, offset=120, section=180) (r, g, b) = conv(50) @@ -3652,7 +3649,7 @@ A Button that runs a single actions only when the button is pressed long enough **Arguments**: -- `pull_up`: See `Button`_ +- `pull_up`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) - `active_state`: See `Button`_ - `bounce_time`: See `Button`_ - `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action @@ -4418,8 +4415,6 @@ a directory is parsed and files are added to the playlist in the following way An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. -.. code-block:: bash - 01-livestream.txt 02-livestream.txt music.mp3 diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 96cb17916..62519e694 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -3,6 +3,7 @@ loaders: search_path: [./src/jukebox] processors: - type: filter +# skip_empty_modules: true # Uncommenting this skips also run_jukebox etc. - type: sphinx - type: crossref renderer: diff --git a/src/jukebox/components/gpio/gpioz/core/converter.py b/src/jukebox/components/gpio/gpioz/core/converter.py index ba9581113..849bc8e17 100644 --- a/src/jukebox/components/gpio/gpioz/core/converter.py +++ b/src/jukebox/components/gpio/gpioz/core/converter.py @@ -43,8 +43,6 @@ class VolumeToRGB: Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 - .. code-block:: python - conv = VolumeToRGB(100, offset=120, section=180) (r, g, b) = conv(50) diff --git a/src/jukebox/components/gpio/gpioz/core/input_devices.py b/src/jukebox/components/gpio/gpioz/core/input_devices.py index fc0727d2c..27a07d2b2 100644 --- a/src/jukebox/components/gpio/gpioz/core/input_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/input_devices.py @@ -234,7 +234,7 @@ class LongPressButton(NameMixin, ButtonBase): """ A Button that runs a single actions only when the button is pressed long enough - :param pull_up: See `Button`_ + :param pull_up: See #Button :param active_state: See `Button`_ diff --git a/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py b/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py index a054a3c1a..a4481efd6 100644 --- a/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py +++ b/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py @@ -48,7 +48,7 @@ class ReaderClass(ReaderBaseClass): All the required interfaces are implemented there. Put your code into these functions (see below for more information) - - __init__ + - `__init__` - read_card - cleanup - stop diff --git a/src/jukebox/components/rfid/reader/__init__.py b/src/jukebox/components/rfid/reader/__init__.py index db0ccb1da..105a36ac0 100644 --- a/src/jukebox/components/rfid/reader/__init__.py +++ b/src/jukebox/components/rfid/reader/__init__.py @@ -41,8 +41,8 @@ def register(self, func: Callable[[str, RfidCardDetectState], None]): .. py:function:: func(card_id: str, state: int) :noindex: - :param card_id: Card ID - :param state: See :class:`RfidCardDetectState` + :param card_id: Card ID + :param state: See :class:`RfidCardDetectState` """ super().register(func) diff --git a/src/jukebox/components/volume/__init__.py b/src/jukebox/components/volume/__init__.py index f2566ed83..c5b22bbcc 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -283,8 +283,6 @@ class PulseVolumeControl: When accessing the pulse library, it needs to be put into a special state. Which is ensured by the context manager - .. code-block: python - with pulse_monitor as pulse ... diff --git a/src/jukebox/jukebox/playlistgenerator.py b/src/jukebox/jukebox/playlistgenerator.py index db64d3eff..b9f0223c6 100755 --- a/src/jukebox/jukebox/playlistgenerator.py +++ b/src/jukebox/jukebox/playlistgenerator.py @@ -12,8 +12,6 @@ An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. -.. code-block:: bash - 01-livestream.txt 02-livestream.txt music.mp3 From 33c8393442622eb833722fde9233a6a47a84683e Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 14:11:12 +0100 Subject: [PATCH 32/41] fix links --- documentation/developers/docstring/README.md | 44 +++++++++---------- .../controls/common/evdev_listener.py | 2 +- .../gpio/gpioz/core/input_devices.py | 32 +++++++------- .../gpio/gpioz/core/output_devices.py | 2 +- .../components/rfid/reader/__init__.py | 4 +- src/jukebox/components/rpc_command_alias.py | 2 +- src/jukebox/components/volume/__init__.py | 8 ++-- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index 1de994543..2261d6115 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -1104,7 +1104,7 @@ See :class:`PlayContentCallbacks` This file provides definitions for RPC command aliases -See [../../builders/rpc-commands.md](RPC Commands) +See [RPC Commands](../../builders/rpc-commands.md) @@ -1239,7 +1239,7 @@ Pulse Audio runs as a user process. Processes who want to communicate / stream t must also run as a user process. This means must also run as user process, as described in -[../../builders/system.md#music-player-daemon-mpd](Music Player Daemon). +[Music Player Daemon](../../builders/system.md#music-player-daemon-mpd). ## Misc @@ -1694,7 +1694,7 @@ Callback signature is **Arguments**: - `card_id`: Card ID -- `state`: See :class:`RfidCardDetectState` +- `state`: See `RfidCardDetectState` @@ -1712,7 +1712,7 @@ def run_callbacks(card_id: str, state: RfidCardDetectState) Callback handler instance for rfid_card_detect_callbacks events. -See :class:`RfidCardDetectCallbacks` +See [`RfidCardDetectCallbacks`](#components.rfid.reader.RfidCardDetectCallbacks) @@ -2838,7 +2838,7 @@ Find an input device with device_name and mandatory keys. **Arguments**: -- `device_name`: See :func:`_filter_by_device_name` +- `device_name`: See :func:`_filter_by_device_name - `exact_name`: See :func:`_filter_by_device_name` - `mandatory_keys`: See :func:`_filter_by_mandatory_keys` @@ -3490,7 +3490,7 @@ All callback handlers are replaced by GPIOZ callback handlers. These are usually by using the :func:`set_rpc_actions` each input device exhibits. For examples how to use the devices from the configuration files, see -[../../builders/gpio.md#input-devices](GPIO: Input Devices). +[GPIO: Input Devices](../../builders/gpio.md#input-devices). @@ -3650,8 +3650,8 @@ A Button that runs a single actions only when the button is pressed long enough **Arguments**: - `pull_up`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ +- `active_state`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) - `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action is run only once independent of the length of time the button is pressed for. - `hold_time`: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. @@ -3687,16 +3687,16 @@ in this case! **Arguments**: -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ +- `pull_up`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `active_state`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) - `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the short press action is ignored - `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press action -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ +- `pin_factory`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) @@ -3710,9 +3710,9 @@ A rotary encoder to run one of two actions depending on the rotation direction. **Arguments**: -- `bounce_time`: See `Button`_ -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `pin_factory`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) @@ -3795,16 +3795,16 @@ It is not necessary to configure all actions. **Arguments**: -- `pull_up`: See `Button`_ -- `active_state`: See `Button`_ -- `bounce_time`: See `Button`_ +- `pull_up`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `active_state`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) - `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the short press action is ignored. - `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press action. A long dual press is never repeated independent of this setting -- `pin_factory`: See `Button`_ -- `name`: See `Button`_ +- `pin_factory`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) @@ -3867,7 +3867,7 @@ with parameters for this device and optional parameters from another device. Unu are silently ignored. This is done to reduce the amount of coding required for connectivity functions. For examples how to use the devices from the configuration files, see -[../../builders/gpio.md#output-devices](GPIO: Output Devices). +[GPIO: Output Devices](../../builders/gpio.md#output-devices). diff --git a/src/jukebox/components/controls/common/evdev_listener.py b/src/jukebox/components/controls/common/evdev_listener.py index a4279afda..f8f0e7263 100644 --- a/src/jukebox/components/controls/common/evdev_listener.py +++ b/src/jukebox/components/controls/common/evdev_listener.py @@ -54,7 +54,7 @@ def find_device(device_name: str, exact_name: bool = True, mandatory_keys: Optio If multiple devices match, the first match is returned - :param device_name: See :func:`_filter_by_device_name` + :param device_name: See :func:`_filter_by_device_name :param exact_name: See :func:`_filter_by_device_name` :param mandatory_keys: See :func:`_filter_by_mandatory_keys` :return: The path to the device diff --git a/src/jukebox/components/gpio/gpioz/core/input_devices.py b/src/jukebox/components/gpio/gpioz/core/input_devices.py index 27a07d2b2..a63b257b0 100644 --- a/src/jukebox/components/gpio/gpioz/core/input_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/input_devices.py @@ -10,7 +10,7 @@ by using the :func:`set_rpc_actions` each input device exhibits. For examples how to use the devices from the configuration files, see -[../../builders/gpio.md#input-devices](GPIO: Input Devices). +[GPIO: Input Devices](../../builders/gpio.md#input-devices). """ import functools @@ -236,9 +236,9 @@ class LongPressButton(NameMixin, ButtonBase): :param pull_up: See #Button - :param active_state: See `Button`_ + :param active_state: See #Button - :param bounce_time: See `Button`_ + :param bounce_time: See #Button :param hold_repeat: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action is run only once independent of the length of time the button is pressed for. @@ -292,11 +292,11 @@ class ShortLongPressButton(NameMixin, ButtonBase): event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run in this case! - :param pull_up: See `Button`_ + :param pull_up: See #Button - :param active_state: See `Button`_ + :param active_state: See #Button - :param bounce_time: See `Button`_ + :param bounce_time: See #Button :param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the @@ -305,9 +305,9 @@ class ShortLongPressButton(NameMixin, ButtonBase): :param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press action - :param pin_factory: See `Button`_ + :param pin_factory: See #Button - :param name: See `Button`_ + :param name: See #Button """ def __init__( self, pin=None, *, pull_up=True, active_state=None, bounce_time=None, @@ -371,11 +371,11 @@ class RotaryEncoder(NameMixin): """ A rotary encoder to run one of two actions depending on the rotation direction. - :param bounce_time: See `Button`_ + :param bounce_time: See #Button - :param pin_factory: See `Button`_ + :param pin_factory: See #Button - :param name: See `Button`_ + :param name: See #Button """ def __init__(self, a, b, *, bounce_time=None, pin_factory=None, name=None): super().__init__(name=name) @@ -443,11 +443,11 @@ class TwinButton(NameMixin): It is not necessary to configure all actions. - :param pull_up: See `Button`_ + :param pull_up: See #Button - :param active_state: See `Button`_ + :param active_state: See #Button - :param bounce_time: See `Button`_ + :param bounce_time: See #Button :param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the @@ -456,9 +456,9 @@ class TwinButton(NameMixin): :param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press action. A long dual press is never repeated independent of this setting - :param pin_factory: See `Button`_ + :param pin_factory: See #Button - :param name: See `Button`_ + :param name: See #Button """ class StateVar(Enum): diff --git a/src/jukebox/components/gpio/gpioz/core/output_devices.py b/src/jukebox/components/gpio/gpioz/core/output_devices.py index 1cf9384ae..78f1d23da 100644 --- a/src/jukebox/components/gpio/gpioz/core/output_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/output_devices.py @@ -12,7 +12,7 @@ are silently ignored. This is done to reduce the amount of coding required for connectivity functions. For examples how to use the devices from the configuration files, see -[../../builders/gpio.md#output-devices](GPIO: Output Devices). +[GPIO: Output Devices](../../builders/gpio.md#output-devices). """ from typing import Optional, List diff --git a/src/jukebox/components/rfid/reader/__init__.py b/src/jukebox/components/rfid/reader/__init__.py index 105a36ac0..37d4a363d 100644 --- a/src/jukebox/components/rfid/reader/__init__.py +++ b/src/jukebox/components/rfid/reader/__init__.py @@ -42,7 +42,7 @@ def register(self, func: Callable[[str, RfidCardDetectState], None]): :noindex: :param card_id: Card ID - :param state: See :class:`RfidCardDetectState` + :param state: See #RfidCardDetectState """ super().register(func) @@ -52,7 +52,7 @@ def run_callbacks(self, card_id: str, state: RfidCardDetectState): #: Callback handler instance for rfid_card_detect_callbacks events. -#: See :class:`RfidCardDetectCallbacks` +#: See #RfidCardDetectCallbacks rfid_card_detect_callbacks: RfidCardDetectCallbacks = RfidCardDetectCallbacks('rfid_card_detect_callbacks', log) diff --git a/src/jukebox/components/rpc_command_alias.py b/src/jukebox/components/rpc_command_alias.py index 1ba5695f1..5f8138958 100644 --- a/src/jukebox/components/rpc_command_alias.py +++ b/src/jukebox/components/rpc_command_alias.py @@ -1,7 +1,7 @@ """ This file provides definitions for RPC command aliases -See [../../builders/rpc-commands.md](RPC Commands) +See [RPC Commands](../../builders/rpc-commands.md) """ # -------------------------------------------------------------- diff --git a/src/jukebox/components/volume/__init__.py b/src/jukebox/components/volume/__init__.py index c5b22bbcc..ccc4873d7 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -28,7 +28,7 @@ must also run as a user process. This means must also run as user process, as described in -[../../builders/system.md#music-player-daemon-mpd](Music Player Daemon). +[Music Player Daemon](../../builders/system.md#music-player-daemon-mpd). ## Misc @@ -140,7 +140,7 @@ def __init__(self): # For the callback handler: We use the context lock only explicitly for registering new functions # When the callbacks are run, it happens from inside the pulse_monitor which an already acquired lock #: Callback handler instance for on_connect_callbacks events. - #: See :class:`PulseMonitor.SoundCardConnectCallbacks` + #: See #PulseMonitor.SoundCardConnectCallbacks self.on_connect_callbacks: PulseMonitor.SoundCardConnectCallbacks = PulseMonitor.SoundCardConnectCallbacks( 'on_connect_callback', logger, context=self) @@ -358,12 +358,12 @@ def __init__(self, sink_list: List[PulseAudioSinkClass]): # When the callbacks are run, it happens from inside the pulse_control which an already acquired lock #: Callback handler instance for on_output_change_callbacks events. - #: See :class:`PulseVolumeControl.OutputChangeCallbackHandler` + #: See #PulseVolumeControl.OutputChangeCallbackHandler self.on_output_change_callbacks = PulseVolumeControl.OutputChangeCallbackHandler( 'on_output_change_callbacks', logger, context=pulse_monitor) #: Callback handler instance for on_output_change_callbacks events. - #: See :class:`PulseVolumeControl.OutputVolumeCallbackHandler` + #: See #PulseVolumeControl.OutputVolumeCallbackHandler self.on_volume_change_callbacks = PulseVolumeControl.OutputVolumeCallbackHandler( 'on_volume_change_callbacks', logger, context=pulse_monitor) From 3e6bd51fd0f6f25b334bfbff3f88ee254e54727c Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 14:15:35 +0100 Subject: [PATCH 33/41] fix formatting for notes --- documentation/developers/docstring/README.md | 6 ++++-- src/jukebox/components/gpio/gpioz/core/mock.py | 3 ++- src/jukebox/jukebox/cfghandler.py | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index 2261d6115..4ef5cba8c 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -3474,7 +3474,8 @@ Monkey Patch LED + Buzzer to get a callback when state changes This targets to represent the state in the TK GUI. Other output devices cannot be represented in the GUI and are silently ignored. -> [!NOTE] Only for developing purposes! +> [!NOTE] +> Only for developing purposes! @@ -4292,7 +4293,8 @@ def is_modified() -> bool Check if the data has changed since the last load/store -> [!NOTE] This relies on the *__str__* representation of the underlying data structure +> [!NOTE] +> This relies on the *__str__* representation of the underlying data structure > In case of ruamel, this ignores comments and only looks at the data diff --git a/src/jukebox/components/gpio/gpioz/core/mock.py b/src/jukebox/components/gpio/gpioz/core/mock.py index e30fd8714..ae2e49e15 100644 --- a/src/jukebox/components/gpio/gpioz/core/mock.py +++ b/src/jukebox/components/gpio/gpioz/core/mock.py @@ -19,7 +19,8 @@ def patch_mock_outputs_with_callback(): This targets to represent the state in the TK GUI. Other output devices cannot be represented in the GUI and are silently ignored. - > [!NOTE] Only for developing purposes!""" + > [!NOTE] + > Only for developing purposes!""" gpiozero.LED._write_orig = gpiozero.LED._write gpiozero.LED._write = rewrite gpiozero.LED.on_change_callback = None diff --git a/src/jukebox/jukebox/cfghandler.py b/src/jukebox/jukebox/cfghandler.py index e378a1bed..8108f1d33 100644 --- a/src/jukebox/jukebox/cfghandler.py +++ b/src/jukebox/jukebox/cfghandler.py @@ -236,7 +236,8 @@ def is_modified(self) -> bool: """ Check if the data has changed since the last load/store - > [!NOTE] This relies on the *__str__* representation of the underlying data structure + > [!NOTE] + > This relies on the *__str__* representation of the underlying data structure > In case of ruamel, this ignores comments and only looks at the data """ with self._lock: From a8f189325e5ab47a82077f941db60bd67e590d06 Mon Sep 17 00:00:00 2001 From: s-martin Date: Thu, 28 Dec 2023 14:20:07 +0100 Subject: [PATCH 34/41] fix links --- documentation/developers/docstring/README.md | 2 +- src/jukebox/components/gpio/gpioz/core/input_devices.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index 4ef5cba8c..dc5b39e4b 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -3519,7 +3519,7 @@ Set all input device callbacks from :attr:`action_config` **Arguments**: - `action_config`: Dictionary with one -:ref:`RPC Command ` definition entry for every device callback +[RPC Commands](../../builders/rpc-commands.md) definition entry for every device callback diff --git a/src/jukebox/components/gpio/gpioz/core/input_devices.py b/src/jukebox/components/gpio/gpioz/core/input_devices.py index a63b257b0..bae049cc5 100644 --- a/src/jukebox/components/gpio/gpioz/core/input_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/input_devices.py @@ -76,7 +76,7 @@ def set_rpc_actions(self, action_config) -> None: Set all input device callbacks from :attr:`action_config` :param action_config: Dictionary with one - :ref:`RPC Command ` definition entry for every device callback + [RPC Commands](../../builders/rpc-commands.md) definition entry for every device callback """ pass From 365089b2cd1dd1e296525c3646fc39038381bd49 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 2 Jan 2024 23:34:55 +0100 Subject: [PATCH 35/41] revert wrong fix --- src/jukebox/components/controls/common/evdev_listener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jukebox/components/controls/common/evdev_listener.py b/src/jukebox/components/controls/common/evdev_listener.py index f8f0e7263..a4279afda 100644 --- a/src/jukebox/components/controls/common/evdev_listener.py +++ b/src/jukebox/components/controls/common/evdev_listener.py @@ -54,7 +54,7 @@ def find_device(device_name: str, exact_name: bool = True, mandatory_keys: Optio If multiple devices match, the first match is returned - :param device_name: See :func:`_filter_by_device_name + :param device_name: See :func:`_filter_by_device_name` :param exact_name: See :func:`_filter_by_device_name` :param mandatory_keys: See :func:`_filter_by_mandatory_keys` :return: The path to the device From d46a0e7d14c732534048eaae4ed7b80274fd9478 Mon Sep 17 00:00:00 2001 From: s-martin Date: Wed, 3 Jan 2024 23:56:17 +0100 Subject: [PATCH 36/41] fixed indentation --- src/jukebox/components/playermpd/playcontentcallback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jukebox/components/playermpd/playcontentcallback.py b/src/jukebox/components/playermpd/playcontentcallback.py index a60452a23..ce5a1b8fb 100644 --- a/src/jukebox/components/playermpd/playcontentcallback.py +++ b/src/jukebox/components/playermpd/playcontentcallback.py @@ -27,8 +27,8 @@ def register(self, func: Callable[[str, STATE], None]): .. py:function:: func(folder: str, state: STATE) :noindex: - :param folder: relativ path to folder to play - :param state: indicator of the state inside the calling + :param folder: relativ path to folder to play + :param state: indicator of the state inside the calling """ super().register(func) From f5fc587b5d6543054016e4301b462ed97d10f7bf Mon Sep 17 00:00:00 2001 From: s-martin Date: Wed, 3 Jan 2024 23:56:39 +0100 Subject: [PATCH 37/41] add generated docstring --- documentation/developers/docstring/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index dc5b39e4b..dde85b224 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -785,9 +785,10 @@ Callback signature is .. py:function:: func(folder: str, state: STATE) :noindex: - :param folder: relativ path to folder to play - :param state: indicator of the state inside the calling +**Arguments**: +- `folder`: relativ path to folder to play +- `state`: indicator of the state inside the calling @@ -2838,7 +2839,7 @@ Find an input device with device_name and mandatory keys. **Arguments**: -- `device_name`: See :func:`_filter_by_device_name +- `device_name`: See :func:`_filter_by_device_name` - `exact_name`: See :func:`_filter_by_device_name` - `mandatory_keys`: See :func:`_filter_by_mandatory_keys` From bbec5ded81a1ef7a4a84fa0f3f311f72e0b44c57 Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 5 Jan 2024 22:44:57 +0100 Subject: [PATCH 38/41] Check for docstring in action --- .github/workflows/pythonpackage_future3.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml index f2174d0c0..66ef6e4ed 100644 --- a/.github/workflows/pythonpackage_future3.yml +++ b/.github/workflows/pythonpackage_future3.yml @@ -54,6 +54,19 @@ jobs: pip3 install flake8 # Stop the build if linting fails ./run_flake8.sh + - name: Generate docstring + run: | + # If commited docstring is not up to date, this triggers a modified file in git + ./run_docgeneration.sh + - name: Check docstring is up to date (if no modified file in git) + uses: CatChen/check-git-status-action@v1 + with: + fail-if-not-clean: true + push-if-not-clean: false + request-changes-if-not-clean: true + request-changes-token: ${{ secrets.GITHUB_TOKEN }} + request-changes-comment: 'Please execute ./run_docgeneration.sh, to ensure docstrings are generated.' + targets: './documentation/developers/docstring' finish: needs: build From 490877112a4f9229cea22f765761dac3f7416bf4 Mon Sep 17 00:00:00 2001 From: s-martin Date: Fri, 5 Jan 2024 22:57:36 +0100 Subject: [PATCH 39/41] Try without request changes --- .github/workflows/pythonpackage_future3.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml index 66ef6e4ed..df8e64e36 100644 --- a/.github/workflows/pythonpackage_future3.yml +++ b/.github/workflows/pythonpackage_future3.yml @@ -56,16 +56,16 @@ jobs: ./run_flake8.sh - name: Generate docstring run: | - # If commited docstring is not up to date, this triggers a modified file in git + # If commited docstring is not up to date, this triggers a dirty git ./run_docgeneration.sh - - name: Check docstring is up to date (if no modified file in git) + - name: Check docstring is up to date (if git is not dirty) uses: CatChen/check-git-status-action@v1 with: fail-if-not-clean: true push-if-not-clean: false - request-changes-if-not-clean: true - request-changes-token: ${{ secrets.GITHUB_TOKEN }} - request-changes-comment: 'Please execute ./run_docgeneration.sh, to ensure docstrings are generated.' + request-changes-if-not-clean: false + #request-changes-token: ${{ secrets.GITHUB_TOKEN }} + #request-changes-comment: 'Please execute ./run_docgeneration.sh, to ensure docstrings are generated.' targets: './documentation/developers/docstring' finish: From efe5097ad236717d49df84c4b83bf6d28bb2b171 Mon Sep 17 00:00:00 2001 From: s-martin Date: Sat, 13 Jan 2024 14:05:41 +0100 Subject: [PATCH 40/41] try request changes --- .github/workflows/pythonpackage_future3.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml index df8e64e36..1545b8abe 100644 --- a/.github/workflows/pythonpackage_future3.yml +++ b/.github/workflows/pythonpackage_future3.yml @@ -58,14 +58,14 @@ jobs: run: | # If commited docstring is not up to date, this triggers a dirty git ./run_docgeneration.sh - - name: Check docstring is up to date (if git is not dirty) + - name: Check docstring is up to date (if git is clean) uses: CatChen/check-git-status-action@v1 with: fail-if-not-clean: true push-if-not-clean: false - request-changes-if-not-clean: false - #request-changes-token: ${{ secrets.GITHUB_TOKEN }} - #request-changes-comment: 'Please execute ./run_docgeneration.sh, to ensure docstrings are generated.' + request-changes-if-not-clean: true + request-changes-token: ${{ secrets.GITHUB_TOKEN }} + request-changes-comment: 'Please execute ./run_docgeneration.sh, to ensure docstrings are generated.' targets: './documentation/developers/docstring' finish: From a7279a41a0ef58ce9a66b7c716de9730da414346 Mon Sep 17 00:00:00 2001 From: s-martin Date: Sun, 14 Jan 2024 10:02:00 +0100 Subject: [PATCH 41/41] remove docstring check from action --- .github/workflows/pythonpackage_future3.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml index 1545b8abe..f2174d0c0 100644 --- a/.github/workflows/pythonpackage_future3.yml +++ b/.github/workflows/pythonpackage_future3.yml @@ -54,19 +54,6 @@ jobs: pip3 install flake8 # Stop the build if linting fails ./run_flake8.sh - - name: Generate docstring - run: | - # If commited docstring is not up to date, this triggers a dirty git - ./run_docgeneration.sh - - name: Check docstring is up to date (if git is clean) - uses: CatChen/check-git-status-action@v1 - with: - fail-if-not-clean: true - push-if-not-clean: false - request-changes-if-not-clean: true - request-changes-token: ${{ secrets.GITHUB_TOKEN }} - request-changes-comment: 'Please execute ./run_docgeneration.sh, to ensure docstrings are generated.' - targets: './documentation/developers/docstring' finish: needs: build