gukebox 1.0.0.dev10__tar.gz → 1.0.0.dev12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/PKG-INFO +1 -1
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/json_library_adapter.py +3 -2
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +5 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/app.py +77 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/cli_presentation.py +6 -19
- gukebox-1.0.0.dev12/jukebox/admin/pn532_command_handlers.py +222 -0
- gukebox-1.0.0.dev12/jukebox/admin/pn532_commands.py +20 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/di_container.py +7 -3
- gukebox-1.0.0.dev12/jukebox/pn532/__init__.py +19 -0
- gukebox-1.0.0.dev12/jukebox/pn532/profiles.py +70 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/entities.py +2 -3
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/resolve.py +3 -2
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/runtime_resolver.py +6 -10
- gukebox-1.0.0.dev12/jukebox/shared/terminal_ui.py +12 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/pyproject.toml +1 -1
- gukebox-1.0.0.dev10/jukebox/pn532/__init__.py +0 -8
- gukebox-1.0.0.dev10/jukebox/pn532/profiles.py +0 -32
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/LICENSE +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/README.md +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api/current_tag_router.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api/discs_router.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api/models.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api/settings_router.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api_controller.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/cli_display.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/ui_controller.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/ui_pages/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/ui_pages/library.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/ui_pages/settings.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/ui_pages/sonos.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/command_handlers.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/commands.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/di_container.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/entities/current_tag_status.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/add_disc.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/edit_disc.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/get_disc.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/list_discs.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/remove_disc.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/search_discs.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/inbound/config.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/players/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/players/sonos_player_adapter.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/readers/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/sonos_discovery_adapter.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/command_handlers.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/commands.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/di_container.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/services.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/admin/sonos_households.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/app.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/entities/current_tag_action.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/entities/disc.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/entities/library.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/entities/playback_action.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/entities/playback_session.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/entities/tag_event.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/ports/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/ports/player_port.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/ports/reader_port.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/repositories/current_tag_repository.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/repositories/library_repository.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/use_cases/determine_action.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/use_cases/handle_tag_event.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/definitions.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/dict_utils.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/errors.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/file_settings_repository.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/migration.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/repositories.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/runtime_validation.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/selected_sonos_group_repository.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/service_protocols.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/timing_validation.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/types.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/validation_rules.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/view_utils.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/shared/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/shared/config_utils.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/shared/dependency_messages.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/shared/logger.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/shared/timing.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/sonos/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/sonos/discovery.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/sonos/selection.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/sonos/service.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/pn532/__init__.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/pn532/pn532.py +0 -0
- {gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/pn532/spi.py +0 -0
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/json_library_adapter.py
RENAMED
|
@@ -26,8 +26,8 @@ class JsonLibraryAdapter(LibraryRepository):
|
|
|
26
26
|
with open(self.filepath, "r", encoding="utf-8") as f:
|
|
27
27
|
data = json.load(f)
|
|
28
28
|
return Library.model_validate(data)
|
|
29
|
-
except FileNotFoundError
|
|
30
|
-
LOGGER.warning(f"
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
LOGGER.warning(f"No library file found, starting with an empty library: {self.filepath}")
|
|
31
31
|
return Library()
|
|
32
32
|
except (json.JSONDecodeError, ValidationError) as err:
|
|
33
33
|
LOGGER.warning(
|
|
@@ -37,6 +37,7 @@ class JsonLibraryAdapter(LibraryRepository):
|
|
|
37
37
|
|
|
38
38
|
def _write_library(self, library: Library) -> None:
|
|
39
39
|
directory = os.path.dirname(self.filepath) or "."
|
|
40
|
+
os.makedirs(directory, exist_ok=True)
|
|
40
41
|
temp_fd, temp_path = tempfile.mkstemp(dir=directory, prefix=".library-", suffix=".json")
|
|
41
42
|
|
|
42
43
|
try:
|
|
@@ -47,8 +47,13 @@ class Pn532ReaderAdapter(ReaderPort):
|
|
|
47
47
|
self.read_timeout_seconds = read_timeout_seconds
|
|
48
48
|
ic, ver, rev, support = self.pn532.get_firmware_version()
|
|
49
49
|
LOGGER.info(f"Found PN532 with firmware version: {ver}.{rev}")
|
|
50
|
+
self._firmware_version: tuple[int, int] = (ver, rev)
|
|
50
51
|
self.pn532.SAM_configuration()
|
|
51
52
|
|
|
53
|
+
@property
|
|
54
|
+
def firmware_version(self) -> tuple[int, int]:
|
|
55
|
+
return self._firmware_version
|
|
56
|
+
|
|
52
57
|
def read(self) -> Union[str, None]:
|
|
53
58
|
rawuid = self.pn532.read_passive_target(timeout=self.read_timeout_seconds)
|
|
54
59
|
if rawuid is None:
|
|
@@ -42,6 +42,8 @@ from .commands import (
|
|
|
42
42
|
is_sonos_command,
|
|
43
43
|
)
|
|
44
44
|
from .di_container import build_admin_api_app, build_admin_services, build_admin_ui_app, build_settings_service
|
|
45
|
+
from .pn532_command_handlers import execute_pn532_command
|
|
46
|
+
from .pn532_commands import Pn532ProbeCommand, Pn532ProfilesCommand, Pn532SelectCommand, is_pn532_command
|
|
45
47
|
from .sonos_households import GroupedSonosHousehold
|
|
46
48
|
|
|
47
49
|
|
|
@@ -88,6 +90,15 @@ def _run_command(ctx: typer.Context, command: object) -> None:
|
|
|
88
90
|
coordinator_prompt_fn=_prompt_for_sonos_group_coordinator,
|
|
89
91
|
status_fn=_emit_cli_status,
|
|
90
92
|
)
|
|
93
|
+
elif is_pn532_command(command):
|
|
94
|
+
execute_pn532_command(
|
|
95
|
+
command=command,
|
|
96
|
+
settings_service=services.settings,
|
|
97
|
+
profile_prompt_fn=_prompt_for_pn532_profile,
|
|
98
|
+
protocol_prompt_fn=_prompt_for_pn532_protocol,
|
|
99
|
+
pin_prompt_fn=_prompt_for_pn532_pin,
|
|
100
|
+
stdout_fn=typer.echo,
|
|
101
|
+
)
|
|
91
102
|
else:
|
|
92
103
|
execute_server_command(
|
|
93
104
|
verbose=state.verbose,
|
|
@@ -98,6 +109,8 @@ def _run_command(ctx: typer.Context, command: object) -> None:
|
|
|
98
109
|
source_command="jukebox-admin",
|
|
99
110
|
)
|
|
100
111
|
except RuntimeError as err:
|
|
112
|
+
if state.verbose:
|
|
113
|
+
raise
|
|
101
114
|
typer.echo(str(err), err=True)
|
|
102
115
|
raise typer.Exit(code=1)
|
|
103
116
|
except SystemExit as err:
|
|
@@ -110,6 +123,9 @@ def _run_command(ctx: typer.Context, command: object) -> None:
|
|
|
110
123
|
except SettingsError as err:
|
|
111
124
|
typer.echo(render_cli_error(err, verbose=state.verbose), err=True)
|
|
112
125
|
raise typer.Exit(code=1)
|
|
126
|
+
except ModuleNotFoundError as err:
|
|
127
|
+
typer.echo(str(err), err=True)
|
|
128
|
+
raise typer.Exit(code=1)
|
|
113
129
|
except OSError as err:
|
|
114
130
|
typer.echo(str(err), err=True)
|
|
115
131
|
raise typer.Exit(code=1)
|
|
@@ -195,6 +211,38 @@ def _prompt_for_sonos_household_selection(households: list[GroupedSonosHousehold
|
|
|
195
211
|
return None
|
|
196
212
|
|
|
197
213
|
|
|
214
|
+
def _prompt_for_pn532_profile(profiles: list[str]) -> Optional[str]:
|
|
215
|
+
import questionary
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
return questionary.select("Select a PN532 board profile", choices=profiles).ask()
|
|
219
|
+
except KeyboardInterrupt:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _prompt_for_pn532_protocol(protocols: list[str], default: str) -> Optional[str]:
|
|
224
|
+
import questionary
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
return questionary.select("Select a PN532 protocol", choices=protocols, default=default).ask()
|
|
228
|
+
|
|
229
|
+
except KeyboardInterrupt:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _prompt_for_pn532_pin(pin_name: str, default: Optional[int]) -> Optional[str]:
|
|
234
|
+
import questionary
|
|
235
|
+
|
|
236
|
+
default_str = str(default) if default is not None else ""
|
|
237
|
+
try:
|
|
238
|
+
return questionary.text(
|
|
239
|
+
f"SPI {pin_name} pin (GPIO number, leave blank to clear):",
|
|
240
|
+
default=default_str,
|
|
241
|
+
).ask()
|
|
242
|
+
except KeyboardInterrupt:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
198
246
|
def _prompt_for_sonos_group_coordinator(speakers: list[DiscoveredSonosSpeaker]) -> Optional[str]:
|
|
199
247
|
import questionary
|
|
200
248
|
|
|
@@ -223,9 +271,11 @@ app = typer.Typer(help="Admin CLI for jukebox")
|
|
|
223
271
|
settings_app = typer.Typer(help="Inspect and manage application settings")
|
|
224
272
|
library_app = typer.Typer(help="Manage the library")
|
|
225
273
|
sonos_app = typer.Typer(help="Inspect Sonos speakers and manage the saved Sonos selection")
|
|
274
|
+
pn532_app = typer.Typer(help="Inspect and debug the PN532 NFC reader")
|
|
226
275
|
app.add_typer(settings_app, name="settings")
|
|
227
276
|
app.add_typer(library_app, name="library")
|
|
228
277
|
app.add_typer(sonos_app, name="sonos")
|
|
278
|
+
app.add_typer(pn532_app, name="pn532")
|
|
229
279
|
|
|
230
280
|
|
|
231
281
|
@app.callback()
|
|
@@ -379,6 +429,33 @@ def sonos_show(ctx: typer.Context) -> None:
|
|
|
379
429
|
_run_command(ctx, SonosShowCommand(type="sonos_show"))
|
|
380
430
|
|
|
381
431
|
|
|
432
|
+
@pn532_app.command("profiles")
|
|
433
|
+
def pn532_profiles(ctx: typer.Context) -> None:
|
|
434
|
+
"""List available board profiles and their default GPIO pins."""
|
|
435
|
+
_run_command(ctx, Pn532ProfilesCommand(type="pn532_profiles"))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@pn532_app.command("select")
|
|
439
|
+
def pn532_select(
|
|
440
|
+
ctx: typer.Context,
|
|
441
|
+
profile: Annotated[
|
|
442
|
+
Optional[str],
|
|
443
|
+
typer.Option(
|
|
444
|
+
"--profile",
|
|
445
|
+
help="board profile to persist (waveshare_hat, hiletgo_v3, custom)",
|
|
446
|
+
),
|
|
447
|
+
] = None,
|
|
448
|
+
) -> None:
|
|
449
|
+
"""Select a board profile and persist it to settings."""
|
|
450
|
+
_run_command(ctx, Pn532SelectCommand(type="pn532_select", profile=profile))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@pn532_app.command("probe")
|
|
454
|
+
def pn532_probe(ctx: typer.Context) -> None:
|
|
455
|
+
"""Verify the PN532 is connected, show firmware version and attempt one tag read."""
|
|
456
|
+
_run_command(ctx, Pn532ProbeCommand(type="pn532_probe"))
|
|
457
|
+
|
|
458
|
+
|
|
382
459
|
@library_app.command("add")
|
|
383
460
|
def library_add(
|
|
384
461
|
ctx: typer.Context,
|
|
@@ -11,6 +11,7 @@ from jukebox.settings.errors import (
|
|
|
11
11
|
)
|
|
12
12
|
from jukebox.settings.types import JsonObject, JsonValue
|
|
13
13
|
from jukebox.settings.view_utils import MISSING, lookup_object, lookup_optional_dotted_path, lookup_provenance_label
|
|
14
|
+
from jukebox.shared.terminal_ui import table
|
|
14
15
|
from jukebox.sonos.discovery import DiscoveredSonosSpeaker
|
|
15
16
|
from jukebox.sonos.selection import SonosSelectionResult, SonosSelectionStatus
|
|
16
17
|
|
|
@@ -51,26 +52,12 @@ def render_cli_error(err: BaseException, verbose: bool = False) -> str:
|
|
|
51
52
|
def render_sonos_speakers_output(households: list[GroupedSonosHousehold]) -> str:
|
|
52
53
|
if not households:
|
|
53
54
|
return "No visible Sonos speakers found."
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
name_width = max(len(speaker.name) for speaker in all_speakers)
|
|
57
|
-
host_width = max(len(speaker.host) for speaker in all_speakers)
|
|
58
|
-
lines = []
|
|
55
|
+
headers = ["name", "host", "uid"]
|
|
56
|
+
sections = []
|
|
59
57
|
for household in households:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
" {index}. {name:<{name_width}} {host:<{host_width}} {uid}".format(
|
|
64
|
-
index=index,
|
|
65
|
-
name=speaker.name,
|
|
66
|
-
name_width=name_width,
|
|
67
|
-
host=speaker.host,
|
|
68
|
-
host_width=host_width,
|
|
69
|
-
uid=speaker.uid,
|
|
70
|
-
)
|
|
71
|
-
)
|
|
72
|
-
lines.append("")
|
|
73
|
-
return "\n".join(lines[:-1])
|
|
58
|
+
rows = [[s.name, s.host, s.uid] for s in household.speakers]
|
|
59
|
+
sections.append(f"Household: {household.household_id}\n\n" + table(headers, rows, indexed=True))
|
|
60
|
+
return "Sonos speakers:\n\n" + "\n\n".join(sections)
|
|
74
61
|
|
|
75
62
|
|
|
76
63
|
def build_sonos_household_choice_label(household: GroupedSonosHousehold) -> str:
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Callable, Optional, cast
|
|
3
|
+
|
|
4
|
+
from jukebox.pn532.profiles import (
|
|
5
|
+
PN532_PROFILES,
|
|
6
|
+
Pn532ConnectionParams,
|
|
7
|
+
Pn532Protocol,
|
|
8
|
+
SpiConnectionParams,
|
|
9
|
+
resolve_connection_params,
|
|
10
|
+
)
|
|
11
|
+
from jukebox.settings.service_protocols import SettingsService
|
|
12
|
+
from jukebox.shared.terminal_ui import table
|
|
13
|
+
|
|
14
|
+
from .pn532_commands import Pn532ProbeCommand, Pn532ProfilesCommand, Pn532SelectCommand
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _default_build_pn532_reader(
|
|
18
|
+
read_timeout_seconds: float,
|
|
19
|
+
protocol: Pn532Protocol,
|
|
20
|
+
connection: Pn532ConnectionParams,
|
|
21
|
+
) -> Any:
|
|
22
|
+
from jukebox.adapters.outbound.readers.pn532_reader_adapter import Pn532ReaderAdapter
|
|
23
|
+
|
|
24
|
+
if protocol == "spi":
|
|
25
|
+
if not isinstance(connection, SpiConnectionParams):
|
|
26
|
+
raise ValueError(f"Expected SpiConnectionParams for protocol 'spi', got {type(connection).__name__}")
|
|
27
|
+
return Pn532ReaderAdapter(
|
|
28
|
+
read_timeout_seconds=read_timeout_seconds,
|
|
29
|
+
spi_reset=connection.reset,
|
|
30
|
+
spi_cs=connection.cs,
|
|
31
|
+
spi_irq=connection.irq,
|
|
32
|
+
)
|
|
33
|
+
raise ValueError(f"Unsupported PN532 protocol: {protocol}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_pin(raw: Optional[str]) -> "tuple[bool, Optional[str]]":
|
|
37
|
+
"""Returns (ok, value). ok=False means the user cancelled the prompt.
|
|
38
|
+
value=None means blank input (reset to profile default)."""
|
|
39
|
+
if raw is None:
|
|
40
|
+
return False, None
|
|
41
|
+
stripped = raw.strip()
|
|
42
|
+
return True, stripped if stripped else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def execute_pn532_command(
|
|
46
|
+
command: object,
|
|
47
|
+
settings_service: SettingsService,
|
|
48
|
+
profile_prompt_fn: Optional[Callable[[list], Optional[str]]] = None,
|
|
49
|
+
protocol_prompt_fn: Optional[Callable[[list, str], Optional[str]]] = None,
|
|
50
|
+
pin_prompt_fn: Optional[Callable[[str, Optional[int]], Optional[str]]] = None,
|
|
51
|
+
build_pn532_reader: Callable[..., Any] = _default_build_pn532_reader,
|
|
52
|
+
stdout_fn: Callable[[str], None] = print,
|
|
53
|
+
) -> None:
|
|
54
|
+
if isinstance(command, Pn532ProfilesCommand):
|
|
55
|
+
stdout_fn(render_pn532_profiles_output())
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if isinstance(command, Pn532SelectCommand):
|
|
59
|
+
if command.profile is not None:
|
|
60
|
+
selected = command.profile
|
|
61
|
+
settings_service.set_persisted_value("jukebox.reader.pn532.board_profile", selected)
|
|
62
|
+
stdout_fn(render_pn532_select_output(selected))
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Interactive mode
|
|
66
|
+
if profile_prompt_fn is None:
|
|
67
|
+
raise RuntimeError("Interactive PN532 profile selection is not available in this context.")
|
|
68
|
+
selected_profile = profile_prompt_fn(list(PN532_PROFILES.keys()))
|
|
69
|
+
if selected_profile is None:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
defaults = PN532_PROFILES.get(cast(Any, selected_profile))
|
|
73
|
+
if defaults is None:
|
|
74
|
+
raise RuntimeError(f"Unknown board profile: {selected_profile!r}")
|
|
75
|
+
|
|
76
|
+
selected_protocol = defaults.default_protocol
|
|
77
|
+
if protocol_prompt_fn is not None:
|
|
78
|
+
prompted = protocol_prompt_fn(list(defaults.connections.keys()), defaults.default_protocol)
|
|
79
|
+
if prompted is None:
|
|
80
|
+
return
|
|
81
|
+
selected_protocol = prompted
|
|
82
|
+
|
|
83
|
+
pin_defaults = defaults.connections.get(cast(Pn532Protocol, selected_protocol))
|
|
84
|
+
if pin_defaults is None:
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
f"Protocol '{selected_protocol}' is not supported by board profile '{selected_profile}'."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if pin_prompt_fn is not None:
|
|
90
|
+
field_values: dict[str, Optional[str]] = {}
|
|
91
|
+
for f in dataclasses.fields(pin_defaults):
|
|
92
|
+
default = getattr(pin_defaults, f.name)
|
|
93
|
+
raw = pin_prompt_fn(f.name, default)
|
|
94
|
+
ok, value = _parse_pin(raw)
|
|
95
|
+
if not ok:
|
|
96
|
+
return
|
|
97
|
+
field_values[f.name] = value
|
|
98
|
+
|
|
99
|
+
settings_service.set_persisted_value("jukebox.reader.pn532.board_profile", selected_profile)
|
|
100
|
+
if selected_protocol != defaults.default_protocol:
|
|
101
|
+
settings_service.set_persisted_value("jukebox.reader.pn532.protocol", selected_protocol)
|
|
102
|
+
else:
|
|
103
|
+
settings_service.reset_persisted_value("jukebox.reader.pn532.protocol")
|
|
104
|
+
for f in dataclasses.fields(pin_defaults):
|
|
105
|
+
path = f"jukebox.reader.pn532.{selected_protocol}.{f.name}"
|
|
106
|
+
value = field_values[f.name]
|
|
107
|
+
profile_default = getattr(pin_defaults, f.name)
|
|
108
|
+
try:
|
|
109
|
+
is_default = value is None or int(value) == profile_default
|
|
110
|
+
except ValueError:
|
|
111
|
+
is_default = False
|
|
112
|
+
if is_default:
|
|
113
|
+
settings_service.reset_persisted_value(path)
|
|
114
|
+
else:
|
|
115
|
+
assert value is not None
|
|
116
|
+
settings_service.set_persisted_value(path, value)
|
|
117
|
+
stdout_fn(render_pn532_configure_output(selected_profile, selected_protocol, pin_defaults, field_values))
|
|
118
|
+
else:
|
|
119
|
+
settings_service.set_persisted_value("jukebox.reader.pn532.board_profile", selected_profile)
|
|
120
|
+
stdout_fn(render_pn532_select_output(selected_profile))
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if isinstance(command, Pn532ProbeCommand):
|
|
124
|
+
pn532 = settings_service.get_effective_settings().jukebox.reader.pn532
|
|
125
|
+
overrides = SpiConnectionParams(reset=pn532.spi.reset, cs=pn532.spi.cs, irq=pn532.spi.irq)
|
|
126
|
+
resolved = resolve_connection_params(pn532.board_profile, pn532.protocol, overrides)
|
|
127
|
+
|
|
128
|
+
stdout_fn(render_pn532_probe_setup_output(pn532.board_profile, pn532.protocol, resolved))
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
reader = build_pn532_reader(
|
|
132
|
+
read_timeout_seconds=pn532.read_timeout_seconds,
|
|
133
|
+
protocol=pn532.protocol,
|
|
134
|
+
connection=resolved,
|
|
135
|
+
)
|
|
136
|
+
except (ModuleNotFoundError, RuntimeError):
|
|
137
|
+
raise
|
|
138
|
+
except Exception as err:
|
|
139
|
+
msg = str(err)
|
|
140
|
+
if any(s in msg.lower() for s in ("not permitted", "permission", "bad gpio")):
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
"GPIO error — your pin configuration may be incorrect.\n"
|
|
143
|
+
"Update it with: jukebox-admin pn532 select\n"
|
|
144
|
+
"Re-run with `--verbose` for details."
|
|
145
|
+
) from err
|
|
146
|
+
raise RuntimeError(msg) from err
|
|
147
|
+
|
|
148
|
+
ver, rev = reader.firmware_version
|
|
149
|
+
stdout_fn(f"PN532 firmware version: {ver}.{rev}")
|
|
150
|
+
|
|
151
|
+
uid = reader.read()
|
|
152
|
+
stdout_fn(f"Tag UID: {uid}" if uid else "No tag detected")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
raise TypeError("Unsupported PN532 command")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def render_pn532_probe_setup_output(
|
|
159
|
+
board_profile: str,
|
|
160
|
+
protocol: str,
|
|
161
|
+
connection: Pn532ConnectionParams,
|
|
162
|
+
) -> str:
|
|
163
|
+
fields = " ".join(
|
|
164
|
+
f"{f.name}={getattr(connection, f.name) if getattr(connection, f.name) is not None else '-'}"
|
|
165
|
+
for f in dataclasses.fields(connection)
|
|
166
|
+
)
|
|
167
|
+
return f"Probing — profile={board_profile} protocol={protocol} {fields}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def render_pn532_profiles_output() -> str:
|
|
171
|
+
by_protocol: dict[str, list[tuple[str, Any]]] = {}
|
|
172
|
+
for name, profile in PN532_PROFILES.items():
|
|
173
|
+
protocol = profile.default_protocol
|
|
174
|
+
by_protocol.setdefault(protocol, []).append((name, profile.connections[protocol]))
|
|
175
|
+
sections = []
|
|
176
|
+
for protocol, entries in by_protocol.items():
|
|
177
|
+
field_names = [f.name for f in dataclasses.fields(entries[0][1])]
|
|
178
|
+
headers = ["name", *field_names]
|
|
179
|
+
rows = [
|
|
180
|
+
[name, *("-" if getattr(conn, f) is None else getattr(conn, f) for f in field_names)]
|
|
181
|
+
for name, conn in entries
|
|
182
|
+
]
|
|
183
|
+
sections.append(f"Protocol: {protocol}\n\n" + table(headers, rows, indexed=True))
|
|
184
|
+
return "Available predefined board profiles:\n\n" + "\n\n".join(sections)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def render_pn532_select_output(profile: str) -> str:
|
|
188
|
+
defaults = PN532_PROFILES.get(cast(Any, profile))
|
|
189
|
+
if defaults is None:
|
|
190
|
+
return f"Board profile saved: {profile}"
|
|
191
|
+
protocol = defaults.default_protocol
|
|
192
|
+
connection = defaults.connections[protocol]
|
|
193
|
+
fields = " ".join(
|
|
194
|
+
f"{f.name}={getattr(connection, f.name) if getattr(connection, f.name) is not None else '-'}"
|
|
195
|
+
for f in dataclasses.fields(connection)
|
|
196
|
+
)
|
|
197
|
+
return "\n".join(
|
|
198
|
+
[
|
|
199
|
+
f"Board profile saved: {profile}",
|
|
200
|
+
f"Protocol: {protocol}",
|
|
201
|
+
f"Default pins — {fields}",
|
|
202
|
+
]
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def render_pn532_configure_output(
|
|
207
|
+
profile: str,
|
|
208
|
+
protocol: str,
|
|
209
|
+
connection: Pn532ConnectionParams,
|
|
210
|
+
field_values: dict[str, Optional[str]],
|
|
211
|
+
) -> str:
|
|
212
|
+
fields = " ".join(
|
|
213
|
+
f"{f.name}={field_values[f.name] if field_values[f.name] is not None else '-'}"
|
|
214
|
+
for f in dataclasses.fields(connection)
|
|
215
|
+
)
|
|
216
|
+
return "\n".join(
|
|
217
|
+
[
|
|
218
|
+
f"Board profile saved: {profile}",
|
|
219
|
+
f"Protocol: {protocol}",
|
|
220
|
+
f"Pins saved — {fields}",
|
|
221
|
+
]
|
|
222
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Literal, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Pn532ProfilesCommand(BaseModel):
|
|
7
|
+
type: Literal["pn532_profiles"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Pn532SelectCommand(BaseModel):
|
|
11
|
+
type: Literal["pn532_select"]
|
|
12
|
+
profile: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Pn532ProbeCommand(BaseModel):
|
|
16
|
+
type: Literal["pn532_probe"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_pn532_command(command: object) -> bool:
|
|
20
|
+
return isinstance(command, (Pn532ProfilesCommand, Pn532SelectCommand, Pn532ProbeCommand))
|
|
@@ -25,13 +25,17 @@ def build_jukebox(config: ResolvedJukeboxRuntimeConfig):
|
|
|
25
25
|
|
|
26
26
|
if config.reader_type == "pn532":
|
|
27
27
|
from jukebox.adapters.outbound.readers.pn532_reader_adapter import Pn532ReaderAdapter
|
|
28
|
+
from jukebox.pn532.profiles import SpiConnectionParams
|
|
28
29
|
|
|
29
30
|
if config.pn532_protocol == "spi":
|
|
31
|
+
conn = config.pn532_connection
|
|
32
|
+
if not isinstance(conn, SpiConnectionParams):
|
|
33
|
+
raise ValueError(f"Expected SpiConnectionParams for protocol 'spi', got {type(conn).__name__}")
|
|
30
34
|
reader = Pn532ReaderAdapter(
|
|
31
35
|
read_timeout_seconds=config.pn532_read_timeout_seconds,
|
|
32
|
-
spi_reset=
|
|
33
|
-
spi_cs=
|
|
34
|
-
spi_irq=
|
|
36
|
+
spi_reset=conn.reset,
|
|
37
|
+
spi_cs=conn.cs,
|
|
38
|
+
spi_irq=conn.irq,
|
|
35
39
|
)
|
|
36
40
|
else:
|
|
37
41
|
raise ValueError(f"Unsupported PN532 protocol: {config.pn532_protocol}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .profiles import (
|
|
2
|
+
PN532_PROFILES,
|
|
3
|
+
Pn532BoardProfile,
|
|
4
|
+
Pn532BoardProfileDefaults,
|
|
5
|
+
Pn532ConnectionParams,
|
|
6
|
+
Pn532Protocol,
|
|
7
|
+
SpiConnectionParams,
|
|
8
|
+
resolve_connection_params,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"PN532_PROFILES",
|
|
13
|
+
"Pn532BoardProfile",
|
|
14
|
+
"Pn532BoardProfileDefaults",
|
|
15
|
+
"Pn532ConnectionParams",
|
|
16
|
+
"Pn532Protocol",
|
|
17
|
+
"SpiConnectionParams",
|
|
18
|
+
"resolve_connection_params",
|
|
19
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
|
|
5
|
+
Pn532BoardProfile = Literal["waveshare_hat", "hiletgo_v3", "custom"]
|
|
6
|
+
|
|
7
|
+
Pn532Protocol = Literal["spi"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class SpiConnectionParams:
|
|
12
|
+
reset: Optional[int]
|
|
13
|
+
cs: Optional[int]
|
|
14
|
+
irq: Optional[int]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Today SpiConnectionParams only; Union[SpiConnectionParams, UartConnectionParams, ...]
|
|
18
|
+
# will be introduced when additional protocols are added.
|
|
19
|
+
Pn532ConnectionParams = SpiConnectionParams
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Pn532BoardProfileDefaults:
|
|
24
|
+
default_protocol: Pn532Protocol
|
|
25
|
+
connections: dict[Pn532Protocol, Pn532ConnectionParams]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
PN532_PROFILES: dict[Pn532BoardProfile, Pn532BoardProfileDefaults] = {
|
|
29
|
+
"waveshare_hat": Pn532BoardProfileDefaults(
|
|
30
|
+
default_protocol="spi",
|
|
31
|
+
connections={"spi": SpiConnectionParams(reset=20, cs=4, irq=None)},
|
|
32
|
+
),
|
|
33
|
+
"hiletgo_v3": Pn532BoardProfileDefaults(
|
|
34
|
+
default_protocol="spi",
|
|
35
|
+
connections={"spi": SpiConnectionParams(reset=None, cs=8, irq=None)},
|
|
36
|
+
),
|
|
37
|
+
"custom": Pn532BoardProfileDefaults(
|
|
38
|
+
default_protocol="spi",
|
|
39
|
+
connections={"spi": SpiConnectionParams(reset=None, cs=None, irq=None)},
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_connection_params(
|
|
45
|
+
board_profile: Pn532BoardProfile,
|
|
46
|
+
protocol: Pn532Protocol,
|
|
47
|
+
overrides: Pn532ConnectionParams,
|
|
48
|
+
) -> Pn532ConnectionParams:
|
|
49
|
+
"""Merge per-field overrides with the profile defaults for the given protocol.
|
|
50
|
+
|
|
51
|
+
A field value of None in *overrides* means "use the profile default".
|
|
52
|
+
The custom profile has None as its own default, so None is preserved.
|
|
53
|
+
"""
|
|
54
|
+
profile = PN532_PROFILES[board_profile]
|
|
55
|
+
if protocol not in profile.connections:
|
|
56
|
+
supported = list(profile.connections.keys())
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Protocol '{protocol}' is not supported by board profile '{board_profile}' (supported: {supported})"
|
|
59
|
+
)
|
|
60
|
+
defaults = profile.connections[protocol]
|
|
61
|
+
if not isinstance(overrides, type(defaults)):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Expected overrides of type {type(defaults).__name__} for protocol '{protocol}', "
|
|
64
|
+
f"got {type(overrides).__name__}"
|
|
65
|
+
)
|
|
66
|
+
merged = {
|
|
67
|
+
f.name: getattr(overrides, f.name) if getattr(overrides, f.name) is not None else getattr(defaults, f.name)
|
|
68
|
+
for f in dataclasses.fields(defaults)
|
|
69
|
+
}
|
|
70
|
+
return type(defaults)(**merged)
|
|
@@ -3,6 +3,7 @@ from typing import Literal, Optional
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
5
5
|
|
|
6
|
+
from jukebox.pn532.profiles import SpiConnectionParams
|
|
6
7
|
from jukebox.shared.timing import MIN_PAUSE_DELAY_SECONDS
|
|
7
8
|
|
|
8
9
|
from .runtime_validation import validate_resolved_jukebox_runtime_rules
|
|
@@ -272,9 +273,7 @@ class ResolvedJukeboxRuntimeConfig(StrictModel):
|
|
|
272
273
|
pn532_read_timeout_seconds: float
|
|
273
274
|
pn532_board_profile: Literal["waveshare_hat", "hiletgo_v3", "custom"]
|
|
274
275
|
pn532_protocol: Literal["spi"] = "spi"
|
|
275
|
-
|
|
276
|
-
pn532_spi_cs: Optional[int]
|
|
277
|
-
pn532_spi_irq: Optional[int]
|
|
276
|
+
pn532_connection: SpiConnectionParams
|
|
278
277
|
verbose: bool = False
|
|
279
278
|
|
|
280
279
|
@model_validator(mode="after")
|
|
@@ -5,7 +5,7 @@ from typing import Optional, Union, cast
|
|
|
5
5
|
|
|
6
6
|
from pydantic import ValidationError
|
|
7
7
|
|
|
8
|
-
from jukebox.pn532.profiles import
|
|
8
|
+
from jukebox.pn532.profiles import SpiConnectionParams, resolve_connection_params
|
|
9
9
|
from jukebox.shared.config_utils import get_current_tag_path
|
|
10
10
|
|
|
11
11
|
from .definitions import (
|
|
@@ -237,7 +237,8 @@ def _expand_path(path: str) -> str:
|
|
|
237
237
|
|
|
238
238
|
def _derive_pn532(effective_settings: AppSettings) -> JsonObject:
|
|
239
239
|
pn532 = effective_settings.jukebox.reader.pn532
|
|
240
|
-
|
|
240
|
+
overrides = SpiConnectionParams(reset=pn532.spi.reset, cs=pn532.spi.cs, irq=pn532.spi.irq)
|
|
241
|
+
resolved = resolve_connection_params(pn532.board_profile, pn532.protocol, overrides)
|
|
241
242
|
return {
|
|
242
243
|
"reader": {
|
|
243
244
|
"pn532": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
3
|
|
|
4
4
|
from pydantic import ValidationError
|
|
5
5
|
|
|
6
|
-
from jukebox.pn532.profiles import
|
|
6
|
+
from jukebox.pn532.profiles import SpiConnectionParams, resolve_connection_params
|
|
7
7
|
from jukebox.sonos.service import SonosService
|
|
8
8
|
|
|
9
9
|
from .entities import AppSettings, Pn532ReaderSettings, ResolvedJukeboxRuntimeConfig, ResolvedSonosGroupRuntime
|
|
@@ -39,7 +39,7 @@ class JukeboxRuntimeResolver:
|
|
|
39
39
|
pn532_read_timeout_seconds=effective_settings.jukebox.reader.pn532.read_timeout_seconds,
|
|
40
40
|
pn532_board_profile=effective_settings.jukebox.reader.pn532.board_profile,
|
|
41
41
|
pn532_protocol=effective_settings.jukebox.reader.pn532.protocol,
|
|
42
|
-
|
|
42
|
+
pn532_connection=_resolve_pn532_connection(effective_settings.jukebox.reader.pn532),
|
|
43
43
|
verbose=verbose,
|
|
44
44
|
)
|
|
45
45
|
except (ValidationError, ValueError) as err:
|
|
@@ -63,10 +63,6 @@ class JukeboxRuntimeResolver:
|
|
|
63
63
|
return None, None, None
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
return
|
|
69
|
-
"pn532_spi_reset": resolved.reset,
|
|
70
|
-
"pn532_spi_cs": resolved.cs,
|
|
71
|
-
"pn532_spi_irq": resolved.irq,
|
|
72
|
-
}
|
|
66
|
+
def _resolve_pn532_connection(pn532: Pn532ReaderSettings) -> SpiConnectionParams:
|
|
67
|
+
overrides = SpiConnectionParams(reset=pn532.spi.reset, cs=pn532.spi.cs, irq=pn532.spi.irq)
|
|
68
|
+
return resolve_connection_params(pn532.board_profile, pn532.protocol, overrides)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
def table(headers, rows, indexed=False):
|
|
2
|
+
if indexed:
|
|
3
|
+
headers = ["#"] + headers
|
|
4
|
+
rows = [[i + 1] + list(row) for i, row in enumerate(rows)]
|
|
5
|
+
|
|
6
|
+
cols = list(zip(headers, *rows))
|
|
7
|
+
widths = [max(len(str(x)) for x in col) for col in cols]
|
|
8
|
+
|
|
9
|
+
def fmt(row):
|
|
10
|
+
return " ".join(f"{str(val):<{widths[i]}}" for i, val in enumerate(row))
|
|
11
|
+
|
|
12
|
+
return "\n".join([fmt(headers), *map(fmt, rows)])
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Literal, Optional
|
|
3
|
-
|
|
4
|
-
Pn532BoardProfile = Literal["waveshare_hat", "hiletgo_v3", "custom"]
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@dataclass(frozen=True)
|
|
8
|
-
class Pn532BoardProfileDefaults:
|
|
9
|
-
reset: Optional[int]
|
|
10
|
-
cs: Optional[int]
|
|
11
|
-
irq: Optional[int]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
PN532_PROFILES: dict[Pn532BoardProfile, Pn532BoardProfileDefaults] = {
|
|
15
|
-
"waveshare_hat": Pn532BoardProfileDefaults(reset=20, cs=4, irq=None),
|
|
16
|
-
"hiletgo_v3": Pn532BoardProfileDefaults(reset=None, cs=8, irq=None),
|
|
17
|
-
"custom": Pn532BoardProfileDefaults(reset=None, cs=None, irq=None),
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def resolve_spi_pins(
|
|
22
|
-
board_profile: Pn532BoardProfile,
|
|
23
|
-
reset: Optional[int],
|
|
24
|
-
cs: Optional[int],
|
|
25
|
-
irq: Optional[int],
|
|
26
|
-
) -> Pn532BoardProfileDefaults:
|
|
27
|
-
profile = PN532_PROFILES[board_profile]
|
|
28
|
-
return Pn532BoardProfileDefaults(
|
|
29
|
-
reset=reset if reset is not None else profile.reset,
|
|
30
|
-
cs=cs if cs is not None else profile.cs,
|
|
31
|
-
irq=irq if irq is not None else profile.irq,
|
|
32
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api/current_tag_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/api/settings_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/inbound/interactive_cli_controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/outbound/json_library_adapter.py
RENAMED
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/adapters/outbound/text_current_tag_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/discstore/domain/use_cases/get_current_tag_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/sonos_discovery_adapter.py
RENAMED
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/adapters/outbound/text_current_tag_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/repositories/current_tag_repository.py
RENAMED
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/repositories/library_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/domain/use_cases/determine_current_tag_action.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev10 → gukebox-1.0.0.dev12}/jukebox/settings/selected_sonos_group_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|