asher-cli 1.2.0__tar.gz → 1.3.0__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.
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/workflows/bandit.yml +2 -2
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/workflows/ci.yml +2 -2
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/workflows/claude-code-review.yml +1 -1
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/workflows/claude.yml +1 -1
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/workflows/coverage.yml +1 -1
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/workflows/release.yml +1 -1
- {asher_cli-1.2.0 → asher_cli-1.3.0}/CHANGELOG.md +15 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/CLAUDE.md +2 -2
- {asher_cli-1.2.0 → asher_cli-1.3.0}/PKG-INFO +10 -5
- {asher_cli-1.2.0 → asher_cli-1.3.0}/README.md +9 -4
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/__main__.py +18 -1
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/activity_labels.py +7 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/commands/__init__.py +105 -34
- asher_cli-1.3.0/asher/constants.py +61 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/headless.py +78 -24
- asher_cli-1.3.0/asher/helpers.py +182 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/monitoring/__init__.py +5 -4
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/robot_adapters.py +149 -6
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/ROADMAP.md +51 -29
- {asher_cli-1.2.0 → asher_cli-1.3.0}/pyproject.toml +2 -2
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_activity_labels.py +21 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_headless.py +169 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_lr5_commands.py +110 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_missing_robot_commands.py +139 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_monitoring.py +62 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_robot_adapters.py +192 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_version_command.py +12 -0
- asher_cli-1.3.0/tests/testhelpers.py +245 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/uv.lock +265 -258
- asher_cli-1.2.0/asher/constants.py +0 -24
- asher_cli-1.2.0/asher/helpers.py +0 -71
- asher_cli-1.2.0/tests/testhelpers.py +0 -113
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.claude/hooks/block-env.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.claude/settings.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.env.example +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.githooks/pre-push +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/FUNDING.yml +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.gitignore +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.vscode/launch.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.vscode/settings.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/CODEOWNERS +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/LICENSE +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/app.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/app.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/auth.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/autostart.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/cats.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/commands/base.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/completion.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/connection/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/daemon.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/desktoptheme.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/export.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/faults.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/history_view.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/launcher.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/login_flow.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/mcp_bridge.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/mcp_config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/notifications.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/robot_protocol.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/slash-commands/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/theme.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/tray.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/ui/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/ui/style.tcss +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/updates.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/asher/watcher.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/cliff.toml +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/renovate.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/requirements.txt +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/test.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/conftest.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_app_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_auth.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_autostart.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_cats.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_commands_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_completion.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_connection.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_daemon.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_desktoptheme.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_export.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_faults.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_history_view.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_launcher.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_mcp_command.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_mcp_config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_new_commands_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_notifications.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_tray.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_ui.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_updates.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/tests/test_watcher.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.3.0}/watchrun.py +0 -0
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
34
|
-
- uses: astral-sh/setup-uv@
|
|
34
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
35
35
|
|
|
36
36
|
- name: Bandit scan
|
|
37
37
|
# --exit-zero: findings must not fail the job, or the upload step below
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
-c pyproject.toml -r . -f sarif -o bandit.sarif --exit-zero
|
|
42
42
|
|
|
43
43
|
- name: Upload SARIF to code scanning
|
|
44
|
-
uses: github/codeql-action/upload-sarif@
|
|
44
|
+
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
|
45
45
|
with:
|
|
46
46
|
sarif_file: bandit.sarif
|
|
47
47
|
category: bandit
|
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
21
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
22
|
-
- uses: astral-sh/setup-uv@
|
|
22
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
23
23
|
- run: uv sync --no-default-groups --group dev
|
|
24
24
|
- run: uv run ruff check .
|
|
25
25
|
- run: uv run ruff format --check .
|
|
@@ -35,6 +35,6 @@ jobs:
|
|
|
35
35
|
runs-on: ${{ matrix.os }}
|
|
36
36
|
steps:
|
|
37
37
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
38
|
-
- uses: astral-sh/setup-uv@
|
|
38
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
39
39
|
- run: uv sync --no-default-groups --group dev
|
|
40
40
|
- run: uv run pytest tests/ -v --tb=short
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
|
|
34
34
|
- name: Run Claude Code Review
|
|
35
35
|
id: claude-review
|
|
36
|
-
uses: anthropics/claude-code-action@
|
|
36
|
+
uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
|
|
37
37
|
with:
|
|
38
38
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
39
39
|
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
|
|
33
33
|
- name: Run Claude Code
|
|
34
34
|
id: claude
|
|
35
|
-
uses: anthropics/claude-code-action@
|
|
35
|
+
uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
|
|
36
36
|
with:
|
|
37
37
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
38
38
|
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
20
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
21
|
-
- uses: astral-sh/setup-uv@
|
|
21
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
22
22
|
- run: uv sync --no-default-groups --group dev
|
|
23
23
|
- name: Run tests with coverage
|
|
24
24
|
run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
|
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
- name: Extract version from branch name
|
|
21
21
|
id: version
|
|
22
22
|
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
|
|
23
|
-
- uses: astral-sh/setup-uv@
|
|
23
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
24
24
|
- run: uv build
|
|
25
25
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
26
26
|
with:
|
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-08-23
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- *(commands)* Close the remaining robot-command gaps in the roadmap
|
|
15
|
+
- *(monitoring)* Shorten cat-panel status labels that overflow the panel width
|
|
16
|
+
|
|
17
|
+
### 🐛 Bug Fixes
|
|
18
|
+
|
|
19
|
+
- *(monitoring)* Render readable status text in cat panel, not raw cloud code
|
|
20
|
+
|
|
21
|
+
### 🧪 Tests
|
|
22
|
+
|
|
23
|
+
- *(version)* Keep the /version pilot tests off the network
|
|
24
|
+
|
|
10
25
|
## [1.2.0] - 2026-08-16
|
|
11
26
|
|
|
12
27
|
### 🚀 Features
|
|
@@ -23,8 +23,8 @@ asher/
|
|
|
23
23
|
__init__.py
|
|
24
24
|
app.py AsherApp class (thin orchestrator — composes mixins)
|
|
25
25
|
auth.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
|
|
26
|
-
helpers.py fmt_ago(), drawer_bar(), ts(), robot_model() (pure, testable)
|
|
27
|
-
constants.py STATUS_COLORS, ROBOT_MODELS
|
|
26
|
+
helpers.py fmt_ago(), fmt_until(), drawer_bar(), ts(), robot_model(), status_text() + argument parsing shared by the TUI and headless surfaces: hex_colour(), parse_clock(), parse_day(), split_type_flag(), activity_type() (pure, testable)
|
|
27
|
+
constants.py STATUS_COLORS, CAT_PANEL_STATUS_LABELS, ACTIVITY_TYPES, ROBOT_MODELS
|
|
28
28
|
theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
|
|
29
29
|
config.py runtime settings persistence — load()/save()/update() over ~/.asher-cli/config.json; holds poll interval, cat-panel visibility/colour, active pet index, notification settings (non-secret UI prefs only; credentials stay in keyring)
|
|
30
30
|
notifications.py desktop toast + audible alert façade — plyer first, then the platform's own tool (osascript/notify-send); fire/beep are always-safe no-ops on failure/headless
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asher-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API
|
|
5
5
|
Project-URL: Homepage, https://github.com/karanshukla/asher-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/karanshukla/asher-cli
|
|
@@ -75,8 +75,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
75
75
|
- Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
|
|
76
76
|
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5: bonnet/laser/drawer); press `d` to dismiss
|
|
77
77
|
- Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
|
|
78
|
-
- Scrollable activity-history pager — `history [count|all]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
|
|
79
|
-
- Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto
|
|
78
|
+
- Scrollable activity-history pager — `history [count|all] [--type <kind>]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
|
|
79
|
+
- Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto|color <hex>`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule [set|disable]`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all] [--type <kind>]`, `export [days|month]`, `help`, `quit`
|
|
80
80
|
- Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/watch start|stop|status|enable|disable`, `/version`, `/mcp on|off|status`, `/exit`
|
|
81
81
|
- Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
|
|
82
82
|
- Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
|
|
@@ -142,18 +142,21 @@ The keyring is the only place an installed copy reads credentials from. Working
|
|
|
142
142
|
| `lock` / `unlock` | Toggle panel lockout |
|
|
143
143
|
| `sleep` / `wake` | Toggle sleep mode |
|
|
144
144
|
| `night-light on\|off\|auto` | Set night light mode |
|
|
145
|
+
| `night-light color <hex>` | Set the LR5 night light colour (`#RRGGBB`, or the 3-digit shorthand) |
|
|
145
146
|
| `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
|
|
146
147
|
| `panel-brightness <low\|medium\|high>` | Set control-panel brightness (LR4/LR5 only; shows current if omitted) |
|
|
147
148
|
| `wait-time <minutes>` | Set clean-cycle wait time (shows valid values / current if omitted) |
|
|
148
149
|
| `power on\|off` | Hard-power the unit on or off |
|
|
149
150
|
| `rename <new name>` | Rename the unit in the Whisker cloud |
|
|
150
151
|
| `insight [days\|month]` | Show cycle-usage statistics (default: 30 days) |
|
|
151
|
-
| `sleep-schedule` | Show the per-day sleep schedule
|
|
152
|
+
| `sleep-schedule` | Show the per-day sleep schedule |
|
|
153
|
+
| `sleep-schedule set <day\|all> <HH:MM> <HH:MM>` | Set the sleep → wake window (LR3: one window for every day, wake time fixed; LR4: read-only, use the Whisker app) |
|
|
154
|
+
| `sleep-schedule disable` | Turn every scheduled sleep window off |
|
|
152
155
|
| `privacy on\|off` | Toggle LR5 privacy mode |
|
|
153
156
|
| `volume <0-100>` | Set LR5 sound volume |
|
|
154
157
|
| `camera-audio on\|off` | Toggle LR5 camera audio |
|
|
155
158
|
| `drawer-reset` | Reset the LR5 waste drawer level indicator |
|
|
156
|
-
| `history [count\|all]` | Show recent activity in a scrollable pager (default: 50 events) |
|
|
159
|
+
| `history [count\|all] [--type <kind>]` | Show recent activity in a scrollable pager (default: 50 events); `--type cat\|clean\|litter\|offline\|…` filters on the LR5 |
|
|
157
160
|
| `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
|
|
158
161
|
| `clear` | Clear the log |
|
|
159
162
|
| `help` | Show command list |
|
|
@@ -193,6 +196,8 @@ asher clean start a clean cycle
|
|
|
193
196
|
asher night-light auto set night light mode
|
|
194
197
|
asher wait-time 7 set the clean-cycle wait time
|
|
195
198
|
asher history 20 recent activity, newest first
|
|
199
|
+
asher history 20 --type cat only cat visits (LR5)
|
|
200
|
+
asher sleep-schedule set all 22:00 07:00 sleep 22:00 → 07:00 every day
|
|
196
201
|
asher insight 7 cycle-usage statistics
|
|
197
202
|
asher export 7 --output ~/hist.csv activity history to CSV
|
|
198
203
|
```
|
|
@@ -19,8 +19,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
19
19
|
- Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
|
|
20
20
|
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5: bonnet/laser/drawer); press `d` to dismiss
|
|
21
21
|
- Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
|
|
22
|
-
- Scrollable activity-history pager — `history [count|all]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
|
|
23
|
-
- Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto
|
|
22
|
+
- Scrollable activity-history pager — `history [count|all] [--type <kind>]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
|
|
23
|
+
- Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto|color <hex>`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule [set|disable]`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all] [--type <kind>]`, `export [days|month]`, `help`, `quit`
|
|
24
24
|
- Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/watch start|stop|status|enable|disable`, `/version`, `/mcp on|off|status`, `/exit`
|
|
25
25
|
- Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
|
|
26
26
|
- Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
|
|
@@ -86,18 +86,21 @@ The keyring is the only place an installed copy reads credentials from. Working
|
|
|
86
86
|
| `lock` / `unlock` | Toggle panel lockout |
|
|
87
87
|
| `sleep` / `wake` | Toggle sleep mode |
|
|
88
88
|
| `night-light on\|off\|auto` | Set night light mode |
|
|
89
|
+
| `night-light color <hex>` | Set the LR5 night light colour (`#RRGGBB`, or the 3-digit shorthand) |
|
|
89
90
|
| `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
|
|
90
91
|
| `panel-brightness <low\|medium\|high>` | Set control-panel brightness (LR4/LR5 only; shows current if omitted) |
|
|
91
92
|
| `wait-time <minutes>` | Set clean-cycle wait time (shows valid values / current if omitted) |
|
|
92
93
|
| `power on\|off` | Hard-power the unit on or off |
|
|
93
94
|
| `rename <new name>` | Rename the unit in the Whisker cloud |
|
|
94
95
|
| `insight [days\|month]` | Show cycle-usage statistics (default: 30 days) |
|
|
95
|
-
| `sleep-schedule` | Show the per-day sleep schedule
|
|
96
|
+
| `sleep-schedule` | Show the per-day sleep schedule |
|
|
97
|
+
| `sleep-schedule set <day\|all> <HH:MM> <HH:MM>` | Set the sleep → wake window (LR3: one window for every day, wake time fixed; LR4: read-only, use the Whisker app) |
|
|
98
|
+
| `sleep-schedule disable` | Turn every scheduled sleep window off |
|
|
96
99
|
| `privacy on\|off` | Toggle LR5 privacy mode |
|
|
97
100
|
| `volume <0-100>` | Set LR5 sound volume |
|
|
98
101
|
| `camera-audio on\|off` | Toggle LR5 camera audio |
|
|
99
102
|
| `drawer-reset` | Reset the LR5 waste drawer level indicator |
|
|
100
|
-
| `history [count\|all]` | Show recent activity in a scrollable pager (default: 50 events) |
|
|
103
|
+
| `history [count\|all] [--type <kind>]` | Show recent activity in a scrollable pager (default: 50 events); `--type cat\|clean\|litter\|offline\|…` filters on the LR5 |
|
|
101
104
|
| `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
|
|
102
105
|
| `clear` | Clear the log |
|
|
103
106
|
| `help` | Show command list |
|
|
@@ -137,6 +140,8 @@ asher clean start a clean cycle
|
|
|
137
140
|
asher night-light auto set night light mode
|
|
138
141
|
asher wait-time 7 set the clean-cycle wait time
|
|
139
142
|
asher history 20 recent activity, newest first
|
|
143
|
+
asher history 20 --type cat only cat visits (LR5)
|
|
144
|
+
asher sleep-schedule set all 22:00 07:00 sleep 22:00 → 07:00 every day
|
|
140
145
|
asher insight 7 cycle-usage statistics
|
|
141
146
|
asher export 7 --output ~/hist.csv activity history to CSV
|
|
142
147
|
```
|
|
@@ -132,9 +132,26 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
132
132
|
metavar="PATH",
|
|
133
133
|
help="write to this path instead of ~/Downloads.",
|
|
134
134
|
)
|
|
135
|
+
if command.name == "history":
|
|
136
|
+
sub.add_argument(
|
|
137
|
+
"--type",
|
|
138
|
+
metavar="KIND",
|
|
139
|
+
help="only events of this kind (LR5 only), e.g. cat, clean, litter.",
|
|
140
|
+
)
|
|
135
141
|
return parser
|
|
136
142
|
|
|
137
143
|
|
|
144
|
+
def _handler_args(parsed: argparse.Namespace) -> list[str]:
|
|
145
|
+
"""Rebuild the word list a headless handler parses.
|
|
146
|
+
|
|
147
|
+
The handlers take raw words so the TUI and the CLI read the same grammar;
|
|
148
|
+
argparse only declares the flags it needs in order to document them in
|
|
149
|
+
``--help``, and they are folded back in here.
|
|
150
|
+
"""
|
|
151
|
+
kind = getattr(parsed, "type", None)
|
|
152
|
+
return [*parsed.args, "--type", kind] if kind else list(parsed.args)
|
|
153
|
+
|
|
154
|
+
|
|
138
155
|
def main() -> None:
|
|
139
156
|
args = _build_parser().parse_args()
|
|
140
157
|
|
|
@@ -157,7 +174,7 @@ def main() -> None:
|
|
|
157
174
|
asyncio.run(
|
|
158
175
|
run(
|
|
159
176
|
args.command,
|
|
160
|
-
args
|
|
177
|
+
_handler_args(args),
|
|
161
178
|
robot=args.robot,
|
|
162
179
|
output=args.output,
|
|
163
180
|
as_json=getattr(args, "json", False),
|
|
@@ -35,6 +35,13 @@ ACTION_LABELS: dict[str, tuple[str, str]] = {
|
|
|
35
35
|
"motor fault": ("Motor fault", theme.DANGER),
|
|
36
36
|
"pinch detect": ("Pinch detected", theme.DANGER),
|
|
37
37
|
"timing fault": ("Timing fault", theme.WARN),
|
|
38
|
+
# The LR5 activity endpoint reports SCREAMING_SNAKE type codes rather than
|
|
39
|
+
# the sentence-style text the LR3/LR4 history returns.
|
|
40
|
+
"pet_visit": ("Cat visit", theme.WARN),
|
|
41
|
+
"cat_detect": ("Cat detected", theme.WARN),
|
|
42
|
+
"cycle_completed": ("Clean cycle complete", theme.OK),
|
|
43
|
+
"cycle_interrupted": ("Cycle interrupted", theme.WARN),
|
|
44
|
+
"litter_low": ("Litter low", theme.WARN),
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
# Fallback colour for unknown event types — muted grey rather than crashing.
|
|
@@ -7,7 +7,7 @@ import contextlib
|
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
|
-
from datetime import time
|
|
10
|
+
from datetime import datetime, time
|
|
11
11
|
from importlib.metadata import PackageNotFoundError
|
|
12
12
|
from importlib.metadata import version as pkg_version
|
|
13
13
|
from pathlib import Path
|
|
@@ -26,8 +26,21 @@ from textual.widgets import Input, RichLog, Static
|
|
|
26
26
|
|
|
27
27
|
from .. import theme
|
|
28
28
|
from ..completion import enter_completes, render_completion, slash_matches
|
|
29
|
+
from ..constants import ACTIVITY_TYPES
|
|
29
30
|
from ..export import ExportError, build_history_csv, resolve_dest
|
|
30
|
-
from ..helpers import
|
|
31
|
+
from ..helpers import (
|
|
32
|
+
DAY_NAMES,
|
|
33
|
+
activity_type,
|
|
34
|
+
fmt_ago,
|
|
35
|
+
fmt_until,
|
|
36
|
+
hex_colour,
|
|
37
|
+
parse_clock,
|
|
38
|
+
parse_day,
|
|
39
|
+
robot_model,
|
|
40
|
+
split_type_flag,
|
|
41
|
+
status_text,
|
|
42
|
+
ts,
|
|
43
|
+
)
|
|
31
44
|
from ..history_view import HistoryScreen
|
|
32
45
|
from ..login_flow import LoginFlow, LoginState
|
|
33
46
|
from .base import Command, CommandRegistry, SlashCommand
|
|
@@ -43,22 +56,6 @@ def _fmt_wait_time(minutes: object) -> str:
|
|
|
43
56
|
return "—"
|
|
44
57
|
|
|
45
58
|
|
|
46
|
-
def _status_text(status: object) -> str:
|
|
47
|
-
"""Render a robot status as human-readable text.
|
|
48
|
-
|
|
49
|
-
``LitterBoxStatus`` is a plain ``Enum`` (not ``StrEnum``), so ``str()`` yields
|
|
50
|
-
the verbose ``"LitterBoxStatus.READY"``; the readable form ("Ready") lives in
|
|
51
|
-
its ``text`` property. Falls back to ``str()`` for plain strings, and to an
|
|
52
|
-
em dash when status is missing.
|
|
53
|
-
"""
|
|
54
|
-
if status is None:
|
|
55
|
-
return "—"
|
|
56
|
-
text = getattr(status, "text", None)
|
|
57
|
-
if isinstance(text, str):
|
|
58
|
-
return text
|
|
59
|
-
return str(status)
|
|
60
|
-
|
|
61
|
-
|
|
62
59
|
_POWER_LABELS = {"AC": "AC (mains)", "DC": "Battery", "NC": "Off/unknown"}
|
|
63
60
|
|
|
64
61
|
|
|
@@ -188,7 +185,7 @@ class StatusCommand(Command):
|
|
|
188
185
|
last_seen = getattr(app, "_last_cat_seen", None) or getattr(r, "last_seen", None)
|
|
189
186
|
rows = [
|
|
190
187
|
("Online", "yes" if getattr(r, "is_online", False) else "no"),
|
|
191
|
-
("Status",
|
|
188
|
+
("Status", status_text(getattr(r, "status", None))),
|
|
192
189
|
("Drawer", f"{float(getattr(r, 'waste_drawer_level', 0) or 0):.0f}%"),
|
|
193
190
|
("Last seen", fmt_ago(last_seen)),
|
|
194
191
|
("Cat weight", weight),
|
|
@@ -251,6 +248,9 @@ class InfoCommand(Command):
|
|
|
251
248
|
("Online", _yn(getattr(r, "is_online", False))),
|
|
252
249
|
("Last seen", fmt_ago(last_seen)),
|
|
253
250
|
]
|
|
251
|
+
filter_due = getattr(r, "next_filter_replacement_date", None)
|
|
252
|
+
if isinstance(filter_due, datetime):
|
|
253
|
+
rows.insert(-2, ("Filter due", fmt_until(filter_due)))
|
|
254
254
|
log = app.query_one("#log", RichLog)
|
|
255
255
|
for k, v in rows:
|
|
256
256
|
t = Text()
|
|
@@ -348,19 +348,22 @@ class WakeCommand(Command):
|
|
|
348
348
|
class NightLightCommand(Command):
|
|
349
349
|
name = "night-light"
|
|
350
350
|
aliases = ("nightlight", "nl")
|
|
351
|
-
description = "set night light mode"
|
|
351
|
+
description = "on|off|auto|color <hex> set night light mode"
|
|
352
352
|
requires_robot = True
|
|
353
353
|
|
|
354
354
|
@property
|
|
355
355
|
def display_name(self) -> str:
|
|
356
|
-
return "night-light
|
|
356
|
+
return "night-light"
|
|
357
357
|
|
|
358
358
|
async def run(self, app: AsherApp, args: list[str]) -> None:
|
|
359
359
|
if app._adapter is None:
|
|
360
360
|
return
|
|
361
361
|
arg = args[0].lower() if args else ""
|
|
362
|
+
if arg in ("color", "colour"):
|
|
363
|
+
await self._set_colour(app, args[1:])
|
|
364
|
+
return
|
|
362
365
|
if arg not in ("on", "off", "auto"):
|
|
363
|
-
app._log_warn("Usage: night-light on|off|auto")
|
|
366
|
+
app._log_warn("Usage: night-light on|off|auto|color <hex>")
|
|
364
367
|
return
|
|
365
368
|
ok, msg = await app._adapter.set_night_light(arg)
|
|
366
369
|
if ok:
|
|
@@ -375,6 +378,19 @@ class NightLightCommand(Command):
|
|
|
375
378
|
else:
|
|
376
379
|
app._log_warn(msg)
|
|
377
380
|
|
|
381
|
+
async def _set_colour(self, app: AsherApp, args: list[str]) -> None:
|
|
382
|
+
if app._adapter is None:
|
|
383
|
+
return
|
|
384
|
+
colour = hex_colour(args[0]) if args else None
|
|
385
|
+
if colour is None:
|
|
386
|
+
app._log_warn("Usage: night-light color <hex> e.g. night-light color #FF9E64")
|
|
387
|
+
return
|
|
388
|
+
ok, msg = await app._adapter.set_night_light_color(colour)
|
|
389
|
+
if ok:
|
|
390
|
+
app._log_ok(msg)
|
|
391
|
+
else:
|
|
392
|
+
app._log_warn(msg)
|
|
393
|
+
|
|
378
394
|
|
|
379
395
|
class NightLightBrightnessCommand(Command):
|
|
380
396
|
name = "night-light-brightness"
|
|
@@ -440,13 +456,14 @@ class PanelBrightnessCommand(Command):
|
|
|
440
456
|
class HistoryCommand(Command):
|
|
441
457
|
name = "history"
|
|
442
458
|
aliases = ("hist",)
|
|
443
|
-
description = "[count|all]
|
|
459
|
+
description = "[count|all] [--type <kind>] recent activity in a scrollable pager"
|
|
444
460
|
requires_robot = True
|
|
445
461
|
|
|
446
462
|
async def run(self, app: AsherApp, args: list[str]) -> None:
|
|
447
|
-
if app._robot is None:
|
|
463
|
+
if app._robot is None or app._adapter is None:
|
|
448
464
|
return
|
|
449
|
-
|
|
465
|
+
counts, wanted_type = split_type_flag(args)
|
|
466
|
+
raw = counts[0].lower() if counts else ""
|
|
450
467
|
if raw in ("all", "max"):
|
|
451
468
|
limit = 500
|
|
452
469
|
elif raw:
|
|
@@ -457,10 +474,15 @@ class HistoryCommand(Command):
|
|
|
457
474
|
return
|
|
458
475
|
else:
|
|
459
476
|
limit = 50
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
app.
|
|
477
|
+
|
|
478
|
+
wanted = activity_type(wanted_type)
|
|
479
|
+
if wanted_type is not None and wanted is None:
|
|
480
|
+
app._log_warn(f"Usage: history [count|all] --type <{'|'.join(ACTIVITY_TYPES)}>")
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
acts, problem = await app._adapter.get_history(limit, wanted)
|
|
484
|
+
if acts is None:
|
|
485
|
+
app._log_err(problem)
|
|
464
486
|
return
|
|
465
487
|
robot_name = getattr(app._robot, "name", "robot")
|
|
466
488
|
app.push_screen(HistoryScreen(acts, app._pets, robot_name))
|
|
@@ -616,9 +638,6 @@ class InsightCommand(Command):
|
|
|
616
638
|
log.write(t)
|
|
617
639
|
|
|
618
640
|
|
|
619
|
-
_DAY_NAMES = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
|
|
620
|
-
|
|
621
|
-
|
|
622
641
|
def _fmt_sleep_time(t: object) -> str:
|
|
623
642
|
if t is None:
|
|
624
643
|
return "—"
|
|
@@ -645,10 +664,16 @@ def _parse_sleep_day(day: Any) -> tuple[int, bool, object, object] | None:
|
|
|
645
664
|
return None
|
|
646
665
|
|
|
647
666
|
|
|
667
|
+
_EVERY_DAY = ("all", "every", "daily", "everyday")
|
|
668
|
+
_SCHEDULE_USAGE = (
|
|
669
|
+
"Usage: sleep-schedule · sleep-schedule set <day|all> <HH:MM> <HH:MM> · sleep-schedule disable"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
648
673
|
class SleepScheduleCommand(Command):
|
|
649
674
|
name = "sleep-schedule"
|
|
650
675
|
aliases = ("sleepschedule",)
|
|
651
|
-
description = "
|
|
676
|
+
description = "[set <day|all> <HH:MM> <HH:MM>|disable] per-day sleep windows"
|
|
652
677
|
requires_robot = True
|
|
653
678
|
|
|
654
679
|
@property
|
|
@@ -656,6 +681,52 @@ class SleepScheduleCommand(Command):
|
|
|
656
681
|
return "sleep-schedule"
|
|
657
682
|
|
|
658
683
|
async def run(self, app: AsherApp, args: list[str]) -> None:
|
|
684
|
+
action = args[0].lower() if args else ""
|
|
685
|
+
if action == "set":
|
|
686
|
+
await self._set_window(app, args[1:])
|
|
687
|
+
return
|
|
688
|
+
if action in ("disable", "off"):
|
|
689
|
+
await self._disable(app)
|
|
690
|
+
return
|
|
691
|
+
if action:
|
|
692
|
+
app._log_warn(_SCHEDULE_USAGE)
|
|
693
|
+
return
|
|
694
|
+
await self._show(app)
|
|
695
|
+
|
|
696
|
+
async def _set_window(self, app: AsherApp, args: list[str]) -> None:
|
|
697
|
+
if app._adapter is None:
|
|
698
|
+
return
|
|
699
|
+
if len(args) < 3:
|
|
700
|
+
app._log_warn("Usage: sleep-schedule set <day|all> <HH:MM> <HH:MM>")
|
|
701
|
+
return
|
|
702
|
+
day_arg = args[0].lower()
|
|
703
|
+
weekday = None if day_arg in _EVERY_DAY else parse_day(day_arg)
|
|
704
|
+
if weekday is None and day_arg not in _EVERY_DAY:
|
|
705
|
+
app._log_warn(f"Unknown day '{args[0]}' — use {'/'.join(DAY_NAMES)} or 'all'")
|
|
706
|
+
return
|
|
707
|
+
sleep, wake = parse_clock(args[1]), parse_clock(args[2])
|
|
708
|
+
if sleep is None or wake is None:
|
|
709
|
+
app._log_warn("Times must be 24-hour HH:MM — e.g. sleep-schedule set all 22:00 07:00")
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
ok, msg = await app._adapter.set_sleep_window(weekday, sleep, wake)
|
|
713
|
+
if not ok:
|
|
714
|
+
app._log_warn(msg)
|
|
715
|
+
return
|
|
716
|
+
app._log_ok(msg)
|
|
717
|
+
await app._refresh_status()
|
|
718
|
+
|
|
719
|
+
async def _disable(self, app: AsherApp) -> None:
|
|
720
|
+
if app._adapter is None:
|
|
721
|
+
return
|
|
722
|
+
ok, msg = await app._adapter.disable_sleep_schedule()
|
|
723
|
+
if not ok:
|
|
724
|
+
app._log_warn(msg)
|
|
725
|
+
return
|
|
726
|
+
app._log_ok(msg)
|
|
727
|
+
await app._refresh_status()
|
|
728
|
+
|
|
729
|
+
async def _show(self, app: AsherApp) -> None:
|
|
659
730
|
if app._robot is None:
|
|
660
731
|
return
|
|
661
732
|
try:
|
|
@@ -704,7 +775,7 @@ class SleepScheduleCommand(Command):
|
|
|
704
775
|
continue
|
|
705
776
|
idx, day_enabled, sleep_t, wake_t = parsed
|
|
706
777
|
|
|
707
|
-
name =
|
|
778
|
+
name = DAY_NAMES[idx]
|
|
708
779
|
t = Text()
|
|
709
780
|
t.append(f" {name} ", style=theme.MUTED)
|
|
710
781
|
if day_enabled:
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Module-level constants shared across the app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from . import theme
|
|
6
|
+
|
|
7
|
+
STATUS_COLORS: dict[str, str] = {
|
|
8
|
+
"Ready": theme.OK,
|
|
9
|
+
"Clean Cycle In Progress": theme.ACCENT,
|
|
10
|
+
"Empty Cycle": theme.ACCENT,
|
|
11
|
+
"Cat Detected": theme.WARN,
|
|
12
|
+
"Clean Cycle Paused": theme.WARN,
|
|
13
|
+
"Drawer Almost Full - 2 Cycles Left": theme.WARN,
|
|
14
|
+
"Drawer Almost Full - 1 Cycle Left": theme.WARN,
|
|
15
|
+
"Drawer Full": theme.DANGER,
|
|
16
|
+
"Off": theme.DANGER,
|
|
17
|
+
"Offline": theme.DANGER,
|
|
18
|
+
"Clean Cycle Complete": theme.OK,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Shortened LitterBoxStatus.text values for the cat panel only. #cat-status
|
|
22
|
+
# (asher/ui/style.tcss) is 26 columns wide with a 9-character key column,
|
|
23
|
+
# leaving 17 for the value — these .text values overflow that and wrap onto
|
|
24
|
+
# a second line. Only entries that would overflow are listed; everything
|
|
25
|
+
# else passes through status_text() unshortened. The status bar and the
|
|
26
|
+
# headless/`status` command still render the full text.
|
|
27
|
+
CAT_PANEL_STATUS_LABELS: dict[str, str] = {
|
|
28
|
+
"Clean Cycle In Progress": "Cycling",
|
|
29
|
+
"Clean Cycle Complete": "Cycle Complete",
|
|
30
|
+
"Clean Cycle Paused": "Cycle Paused",
|
|
31
|
+
"Cat Sensor Interrupted": "Sensor Interrupt",
|
|
32
|
+
"Drawer Almost Full - 2 Cycles Left": "Almost Full (2)",
|
|
33
|
+
"Drawer Almost Full - 1 Cycle Left": "Almost Full (1)",
|
|
34
|
+
"Dump + Home Position Fault": "Dump+Home Fault",
|
|
35
|
+
"Dump Position Fault": "Dump Pos Fault",
|
|
36
|
+
"Home Position Fault": "Home Pos Fault",
|
|
37
|
+
"Cat Sensor Fault At Startup": "Sensor (Startup)",
|
|
38
|
+
"Drawer Full At Startup": "Drawer (Startup)",
|
|
39
|
+
"Pinch Detect At Startup": "Pinch (Startup)",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Friendly `history --type` argument → the LR5 activity type the cloud filters
|
|
43
|
+
# on. Anything not listed is passed through uppercased, so a type this table
|
|
44
|
+
# doesn't know yet is still reachable without a release.
|
|
45
|
+
ACTIVITY_TYPES: dict[str, str] = {
|
|
46
|
+
"cat": "PET_VISIT",
|
|
47
|
+
"visit": "PET_VISIT",
|
|
48
|
+
"detect": "CAT_DETECT",
|
|
49
|
+
"clean": "CYCLE_COMPLETED",
|
|
50
|
+
"cycle": "CYCLE_COMPLETED",
|
|
51
|
+
"interrupted": "CYCLE_INTERRUPTED",
|
|
52
|
+
"litter": "LITTER_LOW",
|
|
53
|
+
"offline": "OFFLINE",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ROBOT_MODELS: dict[str, str] = {
|
|
57
|
+
"LitterRobot3": "LR3",
|
|
58
|
+
"LitterRobot4": "LR4",
|
|
59
|
+
"LitterRobot5": "LR5",
|
|
60
|
+
"FeederRobot": "Feeder",
|
|
61
|
+
}
|