asher-cli 1.5.0__tar.gz → 1.6.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.5.0 → asher_cli-1.6.0}/.claude/skills/pylitterbot-ref/SKILL.md +1 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.github/workflows/bandit.yml +2 -2
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.github/workflows/ci.yml +3 -3
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.github/workflows/coverage.yml +1 -1
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.github/workflows/release.yml +4 -4
- {asher_cli-1.5.0 → asher_cli-1.6.0}/CHANGELOG.md +20 -9
- {asher_cli-1.5.0 → asher_cli-1.6.0}/CLAUDE.md +4 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/PKG-INFO +6 -7
- {asher_cli-1.5.0 → asher_cli-1.6.0}/README.md +2 -2
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/export.py +4 -4
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/faults.py +3 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/helpers.py +5 -5
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/commands/robot.py +1 -1
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/history.py +3 -3
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/CONTRIBUTING.md +1 -1
- {asher_cli-1.5.0 → asher_cli-1.6.0}/pyproject.toml +7 -8
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/conftest.py +2 -2
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_activity_labels.py +3 -3
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_app_pilot.py +2 -2
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_commands_pilot.py +2 -2
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_completion.py +2 -2
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_export.py +5 -5
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_faults.py +5 -1
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_headless.py +4 -4
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_history_view.py +14 -16
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_lr5_commands.py +2 -2
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_missing_robot_commands.py +3 -3
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_monitoring.py +3 -3
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_new_commands_pilot.py +3 -3
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/testhelpers.py +11 -11
- {asher_cli-1.5.0 → asher_cli-1.6.0}/uv.lock +675 -1076
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.claude/hooks/block-env.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.claude/settings.json +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.env.example +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.githooks/pre-push +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.github/FUNDING.yml +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.github/workflows/dependency-automerge.yml +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.gitignore +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.vscode/launch.json +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.vscode/settings.json +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/CODEOWNERS +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/LICENSE +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/app.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/__main__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/activity.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/config.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/constants.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/credentials.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/theme.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/core/updates.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/autostart.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/daemon.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/desktoptheme.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/launcher.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/notifications.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/tray.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/desktop/watcher.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/headless.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/mcp/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/mcp/bridge.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/mcp/config.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/robot/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/robot/adapters.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/robot/protocol.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/app.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/cats.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/commands/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/commands/base.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/commands/builtin.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/commands/lr5.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/commands/registry.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/commands/slash.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/completion.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/connection.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/loginflow.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/loginscreen.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/monitoring.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/style.tcss +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/asher/tui/ui.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/cliff.toml +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/ROADMAP.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/credentials.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/e2e-pilot.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/lr5-features.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/multi-pet.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/robot-commands.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/runtime-slash-commands.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/slash-command-spec.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/sleep-schedule.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/ui-ux.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/version-display.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/docs/roadmap-archive/websocket-updates.md +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/renovate.json +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/requirements.txt +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/__init__.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_auth.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_autostart.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_cats.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_config.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_credentials.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_daemon.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_desktoptheme.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_launcher.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_layering.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_mcp_command.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_mcp_config.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_notifications.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_robot_adapters.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_tray.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_ui.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_updates.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_version_command.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/tests/test_watcher.py +0 -0
- {asher_cli-1.5.0 → asher_cli-1.6.0}/watchrun.py +0 -0
|
@@ -20,6 +20,7 @@ robot.sleep_mode_enabled # bool — NOT "sleeping"
|
|
|
20
20
|
robot.panel_lock_enabled # bool — NOT "panel_lockout"
|
|
21
21
|
robot.night_light_mode_enabled # bool
|
|
22
22
|
robot.last_seen # datetime | None
|
|
23
|
+
robot.is_laser_dirty # bool — LR4 since 2025.7.0, LR5 always
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
## Robot methods (all async, await required)
|
|
@@ -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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
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@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
|
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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
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 .
|
|
@@ -30,11 +30,11 @@ jobs:
|
|
|
30
30
|
needs: lint
|
|
31
31
|
strategy:
|
|
32
32
|
matrix:
|
|
33
|
-
python-version: ["3.
|
|
33
|
+
python-version: ["3.11", "3.12"]
|
|
34
34
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
39
39
|
- run: uv sync --no-default-groups --group dev
|
|
40
40
|
- run: uv run pytest tests/ -v --tb=short
|
|
@@ -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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
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
|
|
@@ -19,9 +19,9 @@ jobs:
|
|
|
19
19
|
- name: Extract version from branch name
|
|
20
20
|
id: version
|
|
21
21
|
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
|
|
22
|
-
- uses: astral-sh/setup-uv@
|
|
22
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
23
23
|
- run: uv build
|
|
24
|
-
- uses: actions/upload-artifact@
|
|
24
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
25
25
|
with:
|
|
26
26
|
name: dist
|
|
27
27
|
path: dist/
|
|
@@ -34,7 +34,7 @@ jobs:
|
|
|
34
34
|
permissions:
|
|
35
35
|
id-token: write # needed for PyPI OIDC trusted publishing
|
|
36
36
|
steps:
|
|
37
|
-
- uses: actions/download-artifact@
|
|
37
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
38
38
|
with:
|
|
39
39
|
name: dist
|
|
40
40
|
path: dist/
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
contents: write # needed to create GitHub Releases
|
|
49
49
|
steps:
|
|
50
50
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
51
|
-
- uses: actions/download-artifact@
|
|
51
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
52
52
|
with:
|
|
53
53
|
name: dist
|
|
54
54
|
path: dist/
|
|
@@ -7,14 +7,27 @@ 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.6.0] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- *(faults)* Alert when LR4 laser sensors are dirty
|
|
15
|
+
|
|
16
|
+
### 🔧 Miscellaneous
|
|
17
|
+
|
|
18
|
+
- [**breaking**] Drop Python 3.10 support
|
|
19
|
+
|
|
10
20
|
## [1.5.0] - 2026-10-03
|
|
11
21
|
|
|
12
|
-
###
|
|
22
|
+
### 🐛 Bug Fixes
|
|
23
|
+
|
|
24
|
+
- *(mcp)* Don't overwrite a claude_desktop_config.json that can't be parsed
|
|
25
|
+
- *(mcp)* Cap mcp<2 when /mcp on installs the extra
|
|
26
|
+
|
|
27
|
+
### 👷 CI/CD
|
|
13
28
|
|
|
14
|
-
- *(
|
|
15
|
-
- *(
|
|
16
|
-
- *(ci)* Scope release token permissions to the jobs that need them
|
|
17
|
-
- *(ci)* Pin bandit to 1.9.4
|
|
29
|
+
- *(release)* Scope token permissions to the jobs that need them
|
|
30
|
+
- *(bandit)* Pin bandit to 1.9.4
|
|
18
31
|
|
|
19
32
|
## [1.4.1] - 2026-10-01
|
|
20
33
|
|
|
@@ -23,10 +36,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
23
36
|
- *(export)* Reach the full 30 days on the LR4
|
|
24
37
|
- *(tui)* Hang wrapped log lines under their message
|
|
25
38
|
|
|
26
|
-
### 🛡️ Security
|
|
27
|
-
|
|
28
|
-
- *(deps)* Refresh `uv.lock` to patch pyjwt and urllib3 CVEs (#101)
|
|
29
|
-
|
|
30
39
|
### ♻️ Refactor
|
|
31
40
|
|
|
32
41
|
- Group the asher package into core, robot, tui, desktop and mcp
|
|
@@ -41,6 +50,8 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
41
50
|
|
|
42
51
|
### 🔧 Miscellaneous
|
|
43
52
|
|
|
53
|
+
- *(release)* Cut v1.4.1
|
|
54
|
+
- *(release)* Cut v1.4.0
|
|
44
55
|
- Remove claude.yml
|
|
45
56
|
- Remove claude-code-review.yml
|
|
46
57
|
|
|
@@ -277,3 +277,7 @@ Regenerate with `changelog-release X.Y.Z`, never plain `changelog` — the workf
|
|
|
277
277
|
- Mock external deps with `unittest.mock.AsyncMock` for async robot/account methods
|
|
278
278
|
- `from pylitterbot import Account` is a local import inside `_connect_worker` — patch it at `pylitterbot.Account`, not `asher.tui.connection.Account`
|
|
279
279
|
- Coverage: ~76% overall; main gaps are async exception paths and `_connect_worker` auth flow
|
|
280
|
+
|
|
281
|
+
## Temp files and large downloads
|
|
282
|
+
|
|
283
|
+
`/tmp` is tmpfs on this machine, and so is Claude Code's scratchpad (it lives under `/tmp`), so anything written there is held in RAM. Put venvs, model and runtime downloads, extracted archives and build output on disk under `~/.cache/claude-scratch/<task>/` instead, and keep only small throwaway files in the scratchpad. An in-repo `.venv` or `target/` is already on disk and fine.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asher-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
|
@@ -33,21 +33,20 @@ Classifier: Development Status :: 4 - Beta
|
|
|
33
33
|
Classifier: Environment :: Console
|
|
34
34
|
Classifier: License :: OSI Approved :: MIT License
|
|
35
35
|
Classifier: Programming Language :: Python :: 3
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
37
36
|
Classifier: Programming Language :: Python :: 3.11
|
|
38
37
|
Classifier: Programming Language :: Python :: 3.12
|
|
39
38
|
Classifier: Topic :: Home Automation
|
|
40
|
-
Requires-Python: >=3.
|
|
39
|
+
Requires-Python: >=3.11
|
|
41
40
|
Requires-Dist: keyring>=24.0.0
|
|
42
41
|
Requires-Dist: plyer>=2.1.0
|
|
43
|
-
Requires-Dist: pylitterbot==2025.
|
|
42
|
+
Requires-Dist: pylitterbot==2025.7.0
|
|
44
43
|
Requires-Dist: python-dotenv>=1.0.0
|
|
45
44
|
Requires-Dist: rich>=13.0.0
|
|
46
45
|
Requires-Dist: textual>=0.47.0
|
|
47
46
|
Requires-Dist: tzlocal>=5.4
|
|
48
47
|
Provides-Extra: mcp
|
|
49
48
|
Requires-Dist: mcp<2; extra == 'mcp'
|
|
50
|
-
Requires-Dist: pylitterbot[mcp]==2025.
|
|
49
|
+
Requires-Dist: pylitterbot[mcp]==2025.7.0; extra == 'mcp'
|
|
51
50
|
Provides-Extra: tray
|
|
52
51
|
Requires-Dist: pillow>=10.0; extra == 'tray'
|
|
53
52
|
Requires-Dist: pygobject>=3.42; (sys_platform == 'linux') and extra == 'tray'
|
|
@@ -75,7 +74,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
75
74
|
- Live status bar — unit name, online/offline, drawer fill level, last activity, cat weight
|
|
76
75
|
- Human-readable robot status — translates raw API states into plain English (`Ready`, `Cleaning`, `Cat Detected`, `Drawer Full`, etc.)
|
|
77
76
|
- Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
|
|
78
|
-
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5
|
|
77
|
+
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults, dirty laser sensors (LR4/LR5), LR5 bonnet/drawer; press `d` to dismiss
|
|
79
78
|
- Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
|
|
80
79
|
- 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
|
|
81
80
|
- 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`
|
|
@@ -470,7 +469,7 @@ Scan settings live in `[tool.bandit]` in `pyproject.toml`, so local runs and the
|
|
|
470
469
|
Bandit workflow report identical findings. Results from `main` and pull requests
|
|
471
470
|
are published to the repository's **Security → Code scanning** tab.
|
|
472
471
|
|
|
473
|
-
CI runs on Python 3.
|
|
472
|
+
CI runs on Python 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.
|
|
474
473
|
|
|
475
474
|
## Changelog
|
|
476
475
|
|
|
@@ -19,7 +19,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
19
19
|
- Live status bar — unit name, online/offline, drawer fill level, last activity, cat weight
|
|
20
20
|
- Human-readable robot status — translates raw API states into plain English (`Ready`, `Cleaning`, `Cat Detected`, `Drawer Full`, etc.)
|
|
21
21
|
- Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
|
|
22
|
-
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5
|
|
22
|
+
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults, dirty laser sensors (LR4/LR5), LR5 bonnet/drawer; press `d` to dismiss
|
|
23
23
|
- Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
|
|
24
24
|
- 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
|
|
25
25
|
- 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`
|
|
@@ -414,7 +414,7 @@ Scan settings live in `[tool.bandit]` in `pyproject.toml`, so local runs and the
|
|
|
414
414
|
Bandit workflow report identical findings. Results from `main` and pull requests
|
|
415
415
|
are published to the repository's **Security → Code scanning** tab.
|
|
416
416
|
|
|
417
|
-
CI runs on Python 3.
|
|
417
|
+
CI runs on Python 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.
|
|
418
418
|
|
|
419
419
|
## Changelog
|
|
420
420
|
|
|
@@ -10,7 +10,7 @@ identical regardless of how it was invoked.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import csv
|
|
13
|
-
from datetime import datetime, timedelta,
|
|
13
|
+
from datetime import UTC, datetime, timedelta, tzinfo
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
@@ -69,7 +69,7 @@ def _filter_recent(acts: list[Any], cutoff: datetime) -> list[tuple[Any, datetim
|
|
|
69
69
|
if ts_dt is None:
|
|
70
70
|
continue
|
|
71
71
|
if ts_dt.tzinfo is None:
|
|
72
|
-
ts_dt = ts_dt.replace(tzinfo=
|
|
72
|
+
ts_dt = ts_dt.replace(tzinfo=UTC)
|
|
73
73
|
if ts_dt >= cutoff:
|
|
74
74
|
filtered.append((act, ts_dt))
|
|
75
75
|
filtered.sort(key=lambda x: x[1])
|
|
@@ -106,7 +106,7 @@ async def build_history_csv(robot: RobotProtocol, pets: list[Any], days: int, de
|
|
|
106
106
|
``ACTION_LABELS`` / ``activity_raw_text`` so events render identically to
|
|
107
107
|
the ``history`` command output.
|
|
108
108
|
"""
|
|
109
|
-
cutoff = datetime.now(tz=
|
|
109
|
+
cutoff = datetime.now(tz=UTC) - timedelta(days=days)
|
|
110
110
|
# Only the LR4's get_activity_history() takes a start time.
|
|
111
111
|
fetch: Any = robot.get_activity_history
|
|
112
112
|
try:
|
|
@@ -127,7 +127,7 @@ async def build_history_csv(robot: RobotProtocol, pets: list[Any], days: int, de
|
|
|
127
127
|
|
|
128
128
|
local_tz: tzinfo = get_localzone()
|
|
129
129
|
except Exception: # noqa: BLE001 — tz discovery is best-effort; fall back to UTC
|
|
130
|
-
local_tz =
|
|
130
|
+
local_tz = UTC
|
|
131
131
|
|
|
132
132
|
try:
|
|
133
133
|
with dest.open("w", newline="", encoding="utf-8") as f:
|
|
@@ -104,6 +104,9 @@ _MODEL_ENUM_FAULTS: dict[str, tuple[_EnumCheck, ...]] = {
|
|
|
104
104
|
|
|
105
105
|
_MODEL_BOOL_FAULTS: dict[str, tuple[_BoolCheck, ...]] = {
|
|
106
106
|
# LR3 has no component-fault attributes exposed by pylitterbot.
|
|
107
|
+
"LitterRobot4": (
|
|
108
|
+
_BoolCheck("is_laser_dirty", "LASER SENSOR DIRTY — clean globe", SEVERITY_WARN),
|
|
109
|
+
),
|
|
107
110
|
"LitterRobot5": (
|
|
108
111
|
_BoolCheck("is_bonnet_removed", "BONNET OPEN", SEVERITY_WARN),
|
|
109
112
|
_BoolCheck("is_laser_dirty", "LASER SENSOR DIRTY — clean globe", SEVERITY_WARN),
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
from datetime import datetime, time
|
|
7
|
+
from datetime import UTC, datetime, time
|
|
8
8
|
|
|
9
9
|
from rich.text import Text
|
|
10
10
|
|
|
@@ -42,8 +42,8 @@ def fmt_ago(dt: datetime | None) -> str:
|
|
|
42
42
|
if dt is None:
|
|
43
43
|
return "never"
|
|
44
44
|
if dt.tzinfo is None:
|
|
45
|
-
dt = dt.replace(tzinfo=
|
|
46
|
-
s = int((datetime.now(
|
|
45
|
+
dt = dt.replace(tzinfo=UTC)
|
|
46
|
+
s = int((datetime.now(UTC) - dt).total_seconds())
|
|
47
47
|
if s < 60:
|
|
48
48
|
return f"{s}s ago"
|
|
49
49
|
if s < 3600:
|
|
@@ -58,8 +58,8 @@ def fmt_until(dt: datetime | None) -> str:
|
|
|
58
58
|
if dt is None:
|
|
59
59
|
return "—"
|
|
60
60
|
if dt.tzinfo is None:
|
|
61
|
-
dt = dt.replace(tzinfo=
|
|
62
|
-
seconds = (dt - datetime.now(
|
|
61
|
+
dt = dt.replace(tzinfo=UTC)
|
|
62
|
+
seconds = (dt - datetime.now(UTC)).total_seconds()
|
|
63
63
|
stamp = dt.date().isoformat()
|
|
64
64
|
# Counted away from now in whole days, so 3 days and an hour ago reads as
|
|
65
65
|
# "3d" in either direction — timedelta.days alone floors that to 4.
|
|
@@ -22,7 +22,7 @@ vertically regardless of an event's age — every event shows its date.
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
from collections import Counter
|
|
25
|
-
from datetime import
|
|
25
|
+
from datetime import UTC, datetime
|
|
26
26
|
from typing import TYPE_CHECKING, Any
|
|
27
27
|
|
|
28
28
|
from rich.text import Text
|
|
@@ -61,7 +61,7 @@ def _localize(ts: object) -> datetime | None:
|
|
|
61
61
|
if not isinstance(ts, datetime):
|
|
62
62
|
return None
|
|
63
63
|
if ts.tzinfo is None:
|
|
64
|
-
ts = ts.replace(tzinfo=
|
|
64
|
+
ts = ts.replace(tzinfo=UTC)
|
|
65
65
|
return ts.astimezone(get_localzone())
|
|
66
66
|
|
|
67
67
|
|
|
@@ -69,7 +69,7 @@ def _ts_str(local_dt: datetime | None) -> str:
|
|
|
69
69
|
"""Fixed-width timestamp: ``MM/DD HH:MM`` this year, ``YYYY-MM-DD`` older."""
|
|
70
70
|
if local_dt is None:
|
|
71
71
|
return "?"
|
|
72
|
-
if local_dt.year == datetime.now(tz=
|
|
72
|
+
if local_dt.year == datetime.now(tz=UTC).year:
|
|
73
73
|
return local_dt.strftime("%m/%d %H:%M")
|
|
74
74
|
return local_dt.strftime("%Y-%m-%d")
|
|
75
75
|
|
|
@@ -51,7 +51,7 @@ uv run pytest --cov=asher --cov-report=term-missing
|
|
|
51
51
|
Per-file descriptions are in `CLAUDE.md` § Package structure. Pilot gotchas are
|
|
52
52
|
in `CLAUDE.md` § Testing notes.
|
|
53
53
|
|
|
54
|
-
CI matrix: Python 3.
|
|
54
|
+
CI matrix: Python 3.11 / 3.12 × Ubuntu / Windows / macOS.
|
|
55
55
|
|
|
56
56
|
### Coverage
|
|
57
57
|
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "asher-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.6.0"
|
|
4
4
|
description = "Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
8
|
keywords = ["litter-robot", "whisker", "tui", "terminal", "smart-home"]
|
|
9
9
|
classifiers = [
|
|
10
10
|
"Development Status :: 4 - Beta",
|
|
11
11
|
"Environment :: Console",
|
|
12
12
|
"License :: OSI Approved :: MIT License",
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
|
-
"Programming Language :: Python :: 3.10",
|
|
15
14
|
"Programming Language :: Python :: 3.11",
|
|
16
15
|
"Programming Language :: Python :: 3.12",
|
|
17
16
|
"Topic :: Home Automation",
|
|
18
17
|
]
|
|
19
18
|
dependencies = [
|
|
20
|
-
"pylitterbot==2025.
|
|
19
|
+
"pylitterbot==2025.7.0",
|
|
21
20
|
"textual>=0.47.0",
|
|
22
21
|
"rich>=13.0.0",
|
|
23
22
|
"python-dotenv>=1.0.0",
|
|
@@ -31,7 +30,7 @@ dependencies = [
|
|
|
31
30
|
# `from mcp.server.fastmcp import FastMCP`, and mcp 2.0 deleted that module.
|
|
32
31
|
# Cap it here, and in _ensure_mcp_extra (tui/commands/slash.py), until
|
|
33
32
|
# pylitterbot ships a 2.x-compatible release.
|
|
34
|
-
mcp = ["pylitterbot[mcp]==2025.
|
|
33
|
+
mcp = ["pylitterbot[mcp]==2025.7.0", "mcp<2"]
|
|
35
34
|
# PyGObject is what puts pystray on its AppIndicator backend. Without it pystray
|
|
36
35
|
# silently falls back to a legacy XEmbed icon, which KDE Plasma and GNOME no
|
|
37
36
|
# longer host at all — the watcher runs, draws an icon, and nothing shows it.
|
|
@@ -105,7 +104,7 @@ packages = ["asher"]
|
|
|
105
104
|
|
|
106
105
|
[tool.ruff]
|
|
107
106
|
line-length = 100
|
|
108
|
-
target-version = "
|
|
107
|
+
target-version = "py311"
|
|
109
108
|
# Ruff 0.16 added "*.md" to the default include list, which pulls the Python
|
|
110
109
|
# snippets in docs/ and .claude/skills/ into `ruff format`. Those are
|
|
111
110
|
# illustrative, not compiled, so keep the formatter scoped to real source.
|
|
@@ -125,14 +124,14 @@ known-first-party = ["asher"]
|
|
|
125
124
|
# ── Mypy ──────────────────────────────────────────────────────────────────────
|
|
126
125
|
|
|
127
126
|
[tool.mypy]
|
|
128
|
-
python_version = "3.
|
|
127
|
+
python_version = "3.11"
|
|
129
128
|
warn_return_any = true
|
|
130
129
|
ignore_missing_imports = true # pylitterbot and textual have incomplete stubs
|
|
131
130
|
|
|
132
131
|
# ── bump-my-version ───────────────────────────────────────────────────────────
|
|
133
132
|
|
|
134
133
|
[tool.bumpversion]
|
|
135
|
-
current_version = "1.
|
|
134
|
+
current_version = "1.6.0"
|
|
136
135
|
commit = true
|
|
137
136
|
tag = true
|
|
138
137
|
tag_name = "v{new_version}"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from unittest.mock import AsyncMock, MagicMock
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
@@ -25,7 +25,7 @@ def mock_robot():
|
|
|
25
25
|
r.panel_lock_enabled = False
|
|
26
26
|
r.night_light_mode_enabled = False
|
|
27
27
|
r.serial = "ABC123"
|
|
28
|
-
r.last_seen = datetime.now(
|
|
28
|
+
r.last_seen = datetime.now(UTC)
|
|
29
29
|
# Fault sources in their healthy/no-fault state (enums are truthy even
|
|
30
30
|
# when healthy, so they must be set explicitly to avoid false positives).
|
|
31
31
|
r.globe_motor_fault_status = GlobeMotorFaultStatus.NONE
|
|
@@ -7,7 +7,7 @@ about how ``action`` (``str | LitterBoxStatus``) is read.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from datetime import
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
11
|
from types import SimpleNamespace
|
|
12
12
|
|
|
13
13
|
from pylitterbot.enums import LitterBoxStatus
|
|
@@ -29,7 +29,7 @@ def _act(action, *, weight=None, pet_id=None, timestamp=None):
|
|
|
29
29
|
Using ``SimpleNamespace`` lets us attach them without subclassing.
|
|
30
30
|
"""
|
|
31
31
|
return SimpleNamespace(
|
|
32
|
-
timestamp=timestamp or datetime(2026, 6, 14, 12, 0, tzinfo=
|
|
32
|
+
timestamp=timestamp or datetime(2026, 6, 14, 12, 0, tzinfo=UTC),
|
|
33
33
|
action=action,
|
|
34
34
|
weight=weight,
|
|
35
35
|
pet_id=pet_id,
|
|
@@ -52,7 +52,7 @@ class TestActivityRawText:
|
|
|
52
52
|
|
|
53
53
|
def test_missing_action_attribute_falls_back_to_str_none(self):
|
|
54
54
|
# Defensive: a malformed object with no .action attr should not raise.
|
|
55
|
-
assert activity_raw_text(SimpleNamespace(timestamp=datetime.now(
|
|
55
|
+
assert activity_raw_text(SimpleNamespace(timestamp=datetime.now(UTC))) == "None"
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
# ── format_activity ──────────────────────────────────────────────────────────
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
@@ -23,7 +23,7 @@ def mock_robot():
|
|
|
23
23
|
robot.panel_lock_enabled = False
|
|
24
24
|
robot.night_light_mode_enabled = False
|
|
25
25
|
robot.serial = "LR12345"
|
|
26
|
-
robot.last_seen = datetime.now(
|
|
26
|
+
robot.last_seen = datetime.now(UTC)
|
|
27
27
|
robot.pet_weight = 10.5
|
|
28
28
|
robot.refresh = AsyncMock()
|
|
29
29
|
robot.start_cleaning = AsyncMock(return_value=True)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
@@ -24,7 +24,7 @@ def connected_app():
|
|
|
24
24
|
robot.panel_lock_enabled = False
|
|
25
25
|
robot.night_light_mode_enabled = False
|
|
26
26
|
robot.serial = "LR12345"
|
|
27
|
-
robot.last_seen = datetime.now(
|
|
27
|
+
robot.last_seen = datetime.now(UTC)
|
|
28
28
|
robot.pet_weight = 10.5
|
|
29
29
|
robot.refresh = AsyncMock()
|
|
30
30
|
robot.start_cleaning = AsyncMock(return_value=True)
|
|
@@ -19,7 +19,7 @@ so the command bar is live but no real cloud calls happen.
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
-
from datetime import
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
23
|
from types import SimpleNamespace
|
|
24
24
|
from unittest.mock import AsyncMock, MagicMock
|
|
25
25
|
|
|
@@ -72,7 +72,7 @@ def connected_app():
|
|
|
72
72
|
robot.panel_lock_enabled = False
|
|
73
73
|
robot.night_light_mode_enabled = False
|
|
74
74
|
robot.serial = "LR12345"
|
|
75
|
-
robot.last_seen = datetime.now(
|
|
75
|
+
robot.last_seen = datetime.now(UTC)
|
|
76
76
|
robot.pet_weight = 10.5
|
|
77
77
|
robot.refresh = AsyncMock()
|
|
78
78
|
robot.start_cleaning = AsyncMock(return_value=True)
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
11
|
import csv
|
|
12
|
-
from datetime import datetime, timedelta
|
|
12
|
+
from datetime import UTC, datetime, timedelta
|
|
13
13
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
14
14
|
|
|
15
15
|
import pytest
|
|
@@ -155,7 +155,7 @@ def _act(
|
|
|
155
155
|
pet_id: str | None = None,
|
|
156
156
|
) -> MagicMock:
|
|
157
157
|
act = MagicMock()
|
|
158
|
-
act.timestamp = datetime.now(
|
|
158
|
+
act.timestamp = datetime.now(UTC) - timedelta(days=days_ago)
|
|
159
159
|
act.action = MagicMock(text=action_text)
|
|
160
160
|
act.weight = weight
|
|
161
161
|
act.pet_id = pet_id
|
|
@@ -214,8 +214,8 @@ class TestBuildHistoryCsv:
|
|
|
214
214
|
robot = LitterRobot4(get_activity_history=AsyncMock(return_value=[]))
|
|
215
215
|
await build_history_csv(robot, [], 30, tmp_path / "out.csv")
|
|
216
216
|
start = robot.get_activity_history.await_args.kwargs["start"]
|
|
217
|
-
assert start.tzinfo is
|
|
218
|
-
age = datetime.now(
|
|
217
|
+
assert start.tzinfo is UTC
|
|
218
|
+
age = datetime.now(UTC) - start
|
|
219
219
|
assert timedelta(days=30) <= age < timedelta(days=30, seconds=5)
|
|
220
220
|
|
|
221
221
|
async def test_non_lr4_fetch_sends_no_start(self, tmp_path):
|
|
@@ -272,7 +272,7 @@ class TestBuildHistoryCsv:
|
|
|
272
272
|
|
|
273
273
|
async def test_naive_timestamp_treated_as_utc(self, tmp_path):
|
|
274
274
|
act = MagicMock()
|
|
275
|
-
act.timestamp = datetime.now(
|
|
275
|
+
act.timestamp = datetime.now(UTC).replace(tzinfo=None) - timedelta(days=1)
|
|
276
276
|
act.action = MagicMock(text="Ready")
|
|
277
277
|
act.weight = None
|
|
278
278
|
act.pet_id = None
|
|
@@ -139,8 +139,12 @@ class TestLR4ComponentFaults:
|
|
|
139
139
|
assert check_faults(_lr4(usb_fault_status=UsbFaultStatus.NONE)) == []
|
|
140
140
|
assert check_faults(_lr4(usb_fault_status=UsbFaultStatus.CLEAR)) == []
|
|
141
141
|
|
|
142
|
+
def test_laser_dirty_is_warn(self):
|
|
143
|
+
faults = check_faults(_lr4(is_laser_dirty=True))
|
|
144
|
+
assert faults == [Fault("LASER SENSOR DIRTY — clean globe", SEVERITY_WARN)]
|
|
145
|
+
|
|
142
146
|
def test_lr4_ignores_lr5_only_bool_attrs(self):
|
|
143
|
-
"""LR4 allowlist must not check bonnet/
|
|
147
|
+
"""LR4 allowlist must not check bonnet/gas/drawer-removed."""
|
|
144
148
|
r = _lr4()
|
|
145
149
|
r.is_bonnet_removed = True
|
|
146
150
|
r.is_gas_sensor_fault_detected = True
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import csv
|
|
16
16
|
import json
|
|
17
|
-
from datetime import datetime, time, timedelta
|
|
17
|
+
from datetime import UTC, datetime, time, timedelta
|
|
18
18
|
from types import SimpleNamespace
|
|
19
19
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
20
20
|
|
|
@@ -49,7 +49,7 @@ def _robot(*, serial: str = "S1", name: str = "Asher", model: str = "LitterRobot
|
|
|
49
49
|
robot.waste_drawer_level = 42.0
|
|
50
50
|
robot.litter_level = 71.0
|
|
51
51
|
robot.pet_weight = 9.1
|
|
52
|
-
robot.last_seen = datetime.now(
|
|
52
|
+
robot.last_seen = datetime.now(UTC)
|
|
53
53
|
robot.night_light_mode = None
|
|
54
54
|
robot.night_light_mode_enabled = False
|
|
55
55
|
robot.panel_brightness = None
|
|
@@ -91,7 +91,7 @@ def _session(robot=None, pets=None, output=None) -> Session:
|
|
|
91
91
|
|
|
92
92
|
def _act(*, days_ago: float = 1, action_text: str = "Clean Cycle Complete", weight=9.1):
|
|
93
93
|
act = MagicMock()
|
|
94
|
-
act.timestamp = datetime.now(
|
|
94
|
+
act.timestamp = datetime.now(UTC) - timedelta(days=days_ago)
|
|
95
95
|
act.action = MagicMock(text=action_text)
|
|
96
96
|
act.weight = weight
|
|
97
97
|
act.pet_id = None
|
|
@@ -687,7 +687,7 @@ class TestSleepScheduleWrites:
|
|
|
687
687
|
|
|
688
688
|
class TestFilterReminder:
|
|
689
689
|
async def test_info_shows_the_next_filter_date_when_the_model_has_one(self):
|
|
690
|
-
due = datetime.now(
|
|
690
|
+
due = datetime.now(UTC) + timedelta(days=23, hours=1)
|
|
691
691
|
robot = _robot(model="LitterRobot5", next_filter_replacement_date=due)
|
|
692
692
|
result = await COMMANDS_BY_NAME["info"].handler(_session(robot), [])
|
|
693
693
|
assert result.data["filter_due"] == f"{due.date().isoformat()} (in 23d)"
|