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/documentation/developers/README.md b/documentation/developers/README.md index 64fee44c1..965ffa49d 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -9,6 +9,8 @@ * [Jukebox Apps](./coreapps.md) * [RFID Readers](./rfid) +* [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/README.md b/documentation/developers/docstring/README.md new file mode 100644 index 000000000..dde85b224 --- /dev/null +++ b/documentation/developers/docstring/README.md @@ -0,0 +1,5914 @@ +# 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) + * [runcmd](#run_rpc_tool.runcmd) +* [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\_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) + * [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.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 [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 [Troubleshooting](../../builders/troubleshooting.md). + + + + +# \_\_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 [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 + + + + +# 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. + + + + +#### runcmd + +```python +def runcmd(cmd) +``` + +Just run a command. + +Right now duplicates more or less main() +:todo remove duplication of code + + + + +# 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 [Audio Configuration](../../builders/audio.md#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. + +> [!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! + + + + +## 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: + +**Arguments**: + +- `folder`: relativ path to folder to play +- `state`: indicator of the state inside the calling + + + +#### run\_callbacks + +```python +def run_callbacks(folder: str, state: STATE) +``` + + + + + +# 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\_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 + +```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.playermpd.coverart\_cache\_manager + + + +# components.rpc\_command\_alias + +This file provides definitions for RPC command aliases + +See [RPC Commands](../../builders/rpc-commands.md) + + + + +# 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 + + + +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 +[Music Player Daemon](../../builders/system.md#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! + + ### 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): + +1. :func:`add_on_connect_callback` +2. :func:`add_on_output_change_callbacks` +3. :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: + +**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 + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + + + + + +#### 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 + + 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: + +**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. + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + + + + + +## 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: + +**Arguments**: + +- `volume`: Volume level +- `is_min`: 1, if volume level is minimum, else 0 +- `is_max`: 1, if volume level is maximum, else 0 + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + + + + + +#### 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: + +**Arguments**: + +- `card_id`: Card ID +- `state`: See `RfidCardDetectState` + + + +#### run\_callbacks + +```python +def run_callbacks(card_id: str, state: RfidCardDetectState) +``` + + + + + +#### rfid\_card\_detect\_callbacks + +Callback handler instance for rfid_card_detect_callbacks events. + +See [`RfidCardDetectCallbacks`](#components.rfid.reader.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. + +**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 + + + +## 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.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] +> 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: + +**Arguments**: + +- `status`: 1 if app started, 0 if app shuts down + + + +#### run\_callbacks + +```python +def run_callbacks(status: int) +``` + + + + + +#### 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 + + + + +## 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 + + 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 +[GPIO: Input Devices](../../builders/gpio.md#input-devices). + + + + +## NameMixin Objects + +```python +class NameMixin(ABC) +``` + +Provides name property and RPC decode function + + + + +#### 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 +[RPC Commands](../../builders/rpc-commands.md) definition entry for every device callback + + + +## EventProperty Objects + +```python +class EventProperty() +``` + +Event callback property + + + + +## ButtonBase Objects + +```python +class ButtonBase(ABC) +``` + +Common stuff for single button devices + + + + +#### 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`](#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_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`](#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`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.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`](#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) + + + +#### 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`](#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`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) + + + +## StateVar Objects + +```python +class StateVar(Enum) +``` + +State encoding of the Mealy FSM + + + + +#### 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() +``` + + + + + +# 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 +[GPIO: Output Devices](../../builders/gpio.md#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() +``` + + + + + +# 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. + + 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 + + +-----------------------+ + | 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](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 + +* "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](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:** + +* 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: + +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](https://zguide.zeromq.org/docs/chapter2/`Working`-with-Messages) +* [Multiple Threads](https://zguide.zeromq.org/docs/chapter2/`Multithreading`-with-ZeroMQ) + + + + +## 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](https://zguide.zeromq.org/docs/chapter5/`Using`-a-Reactor) + + + + +#### 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 + * 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 + + + + +## 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 +> * 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**: + +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. + +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 + + { + '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 + +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) + + + + +## 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/documentation/developers/known-issues.md b/documentation/developers/known-issues.md index 817298c60..db3429bcc 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/pydoc-markdown.yml b/pydoc-markdown.yml new file mode 100644 index 000000000..62519e694 --- /dev/null +++ b/pydoc-markdown.yml @@ -0,0 +1,13 @@ +loaders: +- type: python + search_path: [./src/jukebox] +processors: + - type: filter +# skip_empty_modules: true # Uncommenting this skips also run_jukebox etc. + - type: sphinx + - type: crossref +renderer: + type: markdown + render_toc: true + filename: ./documentation/developers/docstring/README.md + render_page_title: true diff --git a/requirements.txt b/requirements.txt index ea9546315..c172a7636 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,3 +34,6 @@ flake8>=4.0.0 pytest pytest-cov mock + +# API docs generation +pydoc-markdown diff --git a/run_docgeneration.sh b/run_docgeneration.sh new file mode 100755 index 000000000..22ab8bc14 --- /dev/null +++ b/run_docgeneration.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Runner script for pydoc-markdown 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 pydoc-markdown +# make sure, directory exists +mkdir -p ./documentation/developers/docstring +# expects pydoc-markdown.yml at working dir +pydoc-markdown 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/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 5090762f4..bae049cc5 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 +[GPIO: Input Devices](../../builders/gpio.md#input-devices). """ import functools @@ -75,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 @@ -233,11 +234,11 @@ 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`_ + :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. @@ -291,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 @@ -304,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, @@ -370,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) @@ -442,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 @@ -455,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/mock.py b/src/jukebox/components/gpio/gpioz/core/mock.py index bccd5e0e1..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/components/gpio/gpioz/core/output_devices.py b/src/jukebox/components/gpio/gpioz/core/output_devices.py index 50949f82b..78f1d23da 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 +[GPIO: Output Devices](../../builders/gpio.md#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/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/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/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index e854ee5ee..2fb585c9e 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -372,8 +372,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/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) 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..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 @@ -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/rfid/reader/__init__.py b/src/jukebox/components/rfid/reader/__init__.py index db0ccb1da..37d4a363d 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 #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 bb484891a..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 :ref:`userguide/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 6653baa77..ccc4873d7 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 +[Music Player Daemon](../../builders/system.md#music-player-daemon-mpd). -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) @@ -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) @@ -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 @@ -282,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 ... @@ -309,12 +308,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) @@ -338,9 +337,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) @@ -359,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) diff --git a/src/jukebox/jukebox/cfghandler.py b/src/jukebox/jukebox/cfghandler.py index 3482a1b42..8108f1d33 100644 --- a/src/jukebox/jukebox/cfghandler.py +++ b/src/jukebox/jukebox/cfghandler.py @@ -236,8 +236,9 @@ 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() 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 diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index afad5da28..5e4a95f21 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -14,39 +14,39 @@ 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: import jukebox.plugs as plugs @plugs.register 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 @@ -504,9 +499,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 @@ -527,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. @@ -723,9 +719,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 @@ -824,7 +820,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: @@ -971,13 +969,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..66729da6e 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." + 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') @@ -271,9 +267,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 diff --git a/src/jukebox/misc/loggingext.py b/src/jukebox/misc/loggingext.py index 9328cfea8..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 @@ -80,21 +81,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' 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_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 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 1593d7796..4bd834e12 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 """