whiskerless 0.1.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.
Files changed (92) hide show
  1. whiskerless-0.1.0/.editorconfig +14 -0
  2. whiskerless-0.1.0/.forgejo/workflows/ci.yml +56 -0
  3. whiskerless-0.1.0/.forgejo/workflows/publish.yml +87 -0
  4. whiskerless-0.1.0/.forgejo/workflows/release.yml +123 -0
  5. whiskerless-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +38 -0
  6. whiskerless-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. whiskerless-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  8. whiskerless-0.1.0/.github/ISSUE_TEMPLATE/protocol_contribution.yml +49 -0
  9. whiskerless-0.1.0/.github/workflows/hassfest.yml +23 -0
  10. whiskerless-0.1.0/.github/workflows/release-macos.yml +100 -0
  11. whiskerless-0.1.0/.gitignore +31 -0
  12. whiskerless-0.1.0/.renovaterc.json +21 -0
  13. whiskerless-0.1.0/CHANGELOG.md +35 -0
  14. whiskerless-0.1.0/CONTRIBUTING.md +109 -0
  15. whiskerless-0.1.0/LICENSE +21 -0
  16. whiskerless-0.1.0/PKG-INFO +207 -0
  17. whiskerless-0.1.0/README.md +173 -0
  18. whiskerless-0.1.0/conftest.py +16 -0
  19. whiskerless-0.1.0/custom_components/whiskerless/__init__.py +36 -0
  20. whiskerless-0.1.0/custom_components/whiskerless/binary_sensor.py +95 -0
  21. whiskerless-0.1.0/custom_components/whiskerless/button.py +75 -0
  22. whiskerless-0.1.0/custom_components/whiskerless/config_flow.py +74 -0
  23. whiskerless-0.1.0/custom_components/whiskerless/const.py +21 -0
  24. whiskerless-0.1.0/custom_components/whiskerless/coordinator.py +225 -0
  25. whiskerless-0.1.0/custom_components/whiskerless/devices/__init__.py +5 -0
  26. whiskerless-0.1.0/custom_components/whiskerless/devices/litter_robot_4.py +29 -0
  27. whiskerless-0.1.0/custom_components/whiskerless/diagnostics.py +33 -0
  28. whiskerless-0.1.0/custom_components/whiskerless/entity.py +52 -0
  29. whiskerless-0.1.0/custom_components/whiskerless/icons.json +37 -0
  30. whiskerless-0.1.0/custom_components/whiskerless/manifest.json +14 -0
  31. whiskerless-0.1.0/custom_components/whiskerless/number.py +82 -0
  32. whiskerless-0.1.0/custom_components/whiskerless/quality_scale.yaml +96 -0
  33. whiskerless-0.1.0/custom_components/whiskerless/select.py +93 -0
  34. whiskerless-0.1.0/custom_components/whiskerless/sensor.py +145 -0
  35. whiskerless-0.1.0/custom_components/whiskerless/strings.json +90 -0
  36. whiskerless-0.1.0/custom_components/whiskerless/switch.py +99 -0
  37. whiskerless-0.1.0/custom_components/whiskerless/time.py +84 -0
  38. whiskerless-0.1.0/custom_components/whiskerless/translations/en.json +90 -0
  39. whiskerless-0.1.0/docs/devices/litter-robot-4/commands.md +76 -0
  40. whiskerless-0.1.0/docs/devices/litter-robot-4/compatibility.md +64 -0
  41. whiskerless-0.1.0/docs/devices/litter-robot-4/protocol.md +97 -0
  42. whiskerless-0.1.0/docs/devices/litter-robot-4/registers.md +73 -0
  43. whiskerless-0.1.0/docs/how-it-works.md +65 -0
  44. whiskerless-0.1.0/docs/recovery.md +86 -0
  45. whiskerless-0.1.0/docs/reverse-engineering.md +73 -0
  46. whiskerless-0.1.0/docs/setup/certificates.md +106 -0
  47. whiskerless-0.1.0/docs/setup/home-assistant.md +121 -0
  48. whiskerless-0.1.0/docs/setup/mqtt-broker.md +114 -0
  49. whiskerless-0.1.0/examples/litter-robot-4/README.md +49 -0
  50. whiskerless-0.1.0/examples/litter-robot-4/automations.yaml +91 -0
  51. whiskerless-0.1.0/hacs.json +7 -0
  52. whiskerless-0.1.0/packaging/README.md +73 -0
  53. whiskerless-0.1.0/packaging/changelog-section.sh +13 -0
  54. whiskerless-0.1.0/packaging/entitlements.plist +17 -0
  55. whiskerless-0.1.0/packaging/forgejo-release.sh +36 -0
  56. whiskerless-0.1.0/packaging/github-release.sh +37 -0
  57. whiskerless-0.1.0/packaging/launcher.py +14 -0
  58. whiskerless-0.1.0/pyproject.toml +95 -0
  59. whiskerless-0.1.0/src/whiskerless/__init__.py +42 -0
  60. whiskerless-0.1.0/src/whiskerless/ble/__init__.py +24 -0
  61. whiskerless-0.1.0/src/whiskerless/ble/messages.py +121 -0
  62. whiskerless-0.1.0/src/whiskerless/ble/protobuf.py +102 -0
  63. whiskerless-0.1.0/src/whiskerless/ble/provision.py +195 -0
  64. whiskerless-0.1.0/src/whiskerless/ble/transport.py +121 -0
  65. whiskerless-0.1.0/src/whiskerless/cli.py +331 -0
  66. whiskerless-0.1.0/src/whiskerless/devices/__init__.py +1 -0
  67. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/__init__.py +41 -0
  68. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/client.py +289 -0
  69. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/codec.py +81 -0
  70. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/commands.py +198 -0
  71. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/const.py +165 -0
  72. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/link.py +116 -0
  73. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/models.py +215 -0
  74. whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/protocol.py +87 -0
  75. whiskerless-0.1.0/src/whiskerless/exceptions.py +50 -0
  76. whiskerless-0.1.0/src/whiskerless/mqtt.py +63 -0
  77. whiskerless-0.1.0/src/whiskerless/py.typed +0 -0
  78. whiskerless-0.1.0/src/whiskerless/safety.py +142 -0
  79. whiskerless-0.1.0/tests/integration/__init__.py +56 -0
  80. whiskerless-0.1.0/tests/integration/conftest.py +42 -0
  81. whiskerless-0.1.0/tests/integration/const.py +11 -0
  82. whiskerless-0.1.0/tests/integration/fixtures/lr4_state.json +34 -0
  83. whiskerless-0.1.0/tests/integration/test_config_flow.py +62 -0
  84. whiskerless-0.1.0/tests/integration/test_diagnostics.py +28 -0
  85. whiskerless-0.1.0/tests/integration/test_entities.py +46 -0
  86. whiskerless-0.1.0/tests/integration/test_init.py +46 -0
  87. whiskerless-0.1.0/tests/test_codec.py +65 -0
  88. whiskerless-0.1.0/tests/test_commands.py +54 -0
  89. whiskerless-0.1.0/tests/test_models.py +63 -0
  90. whiskerless-0.1.0/tests/test_protobuf.py +52 -0
  91. whiskerless-0.1.0/tests/test_protocol.py +57 -0
  92. whiskerless-0.1.0/tests/test_safety.py +63 -0
@@ -0,0 +1,14 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = space
9
+
10
+ [*.py]
11
+ indent_size = 4
12
+
13
+ [*.{json,yaml,yml,toml,md}]
14
+ indent_size = 2
@@ -0,0 +1,56 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*.*.*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ quality:
16
+ name: Lint, type-check, test (library)
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
20
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Install
24
+ run: |
25
+ python -m pip install --upgrade pip
26
+ python -m pip install -e ".[dev,ble]"
27
+ - name: Ruff
28
+ run: ruff check src custom_components tests
29
+ - name: Mypy (library, strict)
30
+ run: mypy
31
+ - name: Pytest (library)
32
+ run: pytest -q
33
+
34
+ homeassistant:
35
+ name: Type-check + test the integration
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
39
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
40
+ with:
41
+ python-version: "3.13"
42
+ - name: Install
43
+ run: |
44
+ python -m pip install --upgrade pip
45
+ python -m pip install -e ".[dev,test-ha]"
46
+ - name: Mypy (integration, strict)
47
+ # --python-version 3.13: the integration uses PEP 695 syntax (HA runs 3.13);
48
+ # override pyproject's 3.11 library pin so the type alias / generic defs parse.
49
+ # --namespace-packages --explicit-package-bases: check it as
50
+ # custom_components.whiskerless so `from whiskerless import …` resolves to the
51
+ # installed library, not the same-named integration package (matches runtime).
52
+ run: >-
53
+ mypy --strict --python-version 3.13 --namespace-packages
54
+ --explicit-package-bases custom_components/whiskerless
55
+ - name: Pytest (integration)
56
+ run: pytest tests/integration
@@ -0,0 +1,87 @@
1
+ name: Publish
2
+
3
+ # Runs on the Forgejo source when a v* tag lands (from release.yml). It is the
4
+ # only place that can reach everything (Forgejo, the internal NAS, GitHub, PyPI),
5
+ # so it orchestrates the releases. The GitHub mirror's release-macos.yml appends
6
+ # the signed .pkg to the GitHub + public-Forgejo releases; the `nas-pkg` job here
7
+ # then bridges that .pkg over to the internal NAS (which GitHub can't reach).
8
+ on:
9
+ push:
10
+ tags: ["v*.*.*"]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ pypi:
17
+ name: Publish to PyPI
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
22
+ with:
23
+ python-version: "3.12"
24
+ - name: Build + upload
25
+ env:
26
+ TWINE_USERNAME: __token__
27
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
28
+ run: |
29
+ python -m pip install --upgrade build twine
30
+ python -m build
31
+ twine upload dist/*
32
+
33
+ releases:
34
+ name: Create releases + Linux binary (Forgejo, NAS, GitHub)
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
38
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
39
+ with:
40
+ python-version: "3.12"
41
+ - name: Build Linux binary
42
+ run: |
43
+ python -m pip install --upgrade pip pyinstaller
44
+ python -m pip install ".[ble]"
45
+ pyinstaller --onefile --name whiskerless-linux-x86_64 \
46
+ --collect-all bleak --collect-all aiomqtt packaging/launcher.py
47
+ - name: Release notes from CHANGELOG
48
+ run: bash packaging/changelog-section.sh "${GITHUB_REF_NAME#v}" > notes.md
49
+ - name: Create the three releases with the Linux binary
50
+ run: |
51
+ chmod +x packaging/forgejo-release.sh packaging/github-release.sh
52
+ BIN=dist/whiskerless-linux-x86_64
53
+ packaging/forgejo-release.sh forgejo.bryantserver.com \
54
+ "${{ secrets.CLUSTER_FORGEJO_REPO_WRITE_PAT }}" "$GITHUB_REF_NAME" notes.md "$BIN"
55
+ packaging/forgejo-release.sh forgejo.nas.bryantserver.com \
56
+ "${{ secrets.NAS_FORGEJO_REPO_WRITE_PAT }}" "$GITHUB_REF_NAME" notes.md "$BIN"
57
+ packaging/github-release.sh \
58
+ "${{ secrets.GH_REPO_WRITE_PAT }}" "$GITHUB_REF_NAME" notes.md "$BIN"
59
+
60
+ nas-pkg:
61
+ name: Bridge the macOS .pkg → NAS
62
+ needs: releases
63
+ runs-on: ubuntu-latest
64
+ steps:
65
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
66
+ - name: Wait for the .pkg on the public Forgejo release, then copy to NAS
67
+ run: |
68
+ set -euo pipefail
69
+ API=https://forgejo.bryantserver.com/api/v1/repos/SisyphusMD/whiskerless
70
+ # The .pkg is built + notarized on GitHub (slow), then appended to the
71
+ # public Forgejo release. Wait for it (up to 30 min), then copy to NAS.
72
+ urls=""
73
+ for _ in $(seq 1 180); do
74
+ urls=$(curl -sf "$API/releases/tags/$GITHUB_REF_NAME" \
75
+ | jq -r '.assets[]? | select(.name | endswith(".pkg")) | .browser_download_url' || true)
76
+ [ -n "$urls" ] && break
77
+ sleep 10
78
+ done
79
+ if [ -z "$urls" ]; then
80
+ echo "::warning::no .pkg on the Forgejo release after 30m; NAS release will lack it"
81
+ exit 0
82
+ fi
83
+ echo "$urls" | while read -r u; do [ -n "$u" ] && curl -sSL -O "$u"; done
84
+ bash packaging/changelog-section.sh "${GITHUB_REF_NAME#v}" > notes.md
85
+ chmod +x packaging/forgejo-release.sh
86
+ packaging/forgejo-release.sh forgejo.nas.bryantserver.com \
87
+ "${{ secrets.NAS_FORGEJO_REPO_WRITE_PAT }}" "$GITHUB_REF_NAME" notes.md *.pkg
@@ -0,0 +1,123 @@
1
+ name: Release
2
+
3
+ # Cut a release from main: pick the bump in the Forgejo UI. This advances the
4
+ # CHANGELOG, bumps every version string, runs the test gate, then commits + tags.
5
+ # The tag triggers publish.yml (PyPI + Linux binary + the forgejo/nas/github
6
+ # releases) and, on the GitHub mirror, the macOS .pkg job.
7
+ on:
8
+ workflow_dispatch:
9
+ inputs:
10
+ bump:
11
+ description: "Version bump"
12
+ required: true
13
+ type: choice
14
+ options: [patch, minor, major]
15
+
16
+ concurrency:
17
+ group: release
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ release:
22
+ name: Cut release
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - name: Refuse non-main dispatches
26
+ run: |
27
+ if [ "${{ github.ref_name }}" != "main" ]; then
28
+ echo "::error::Release must be dispatched from main; got '${{ github.ref_name }}'"
29
+ exit 1
30
+ fi
31
+
32
+ - name: Checkout (full history + tags)
33
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
34
+ with:
35
+ fetch-depth: 0
36
+ token: ${{ secrets.CLUSTER_FORGEJO_REPO_WRITE_PAT }}
37
+
38
+ - name: Configure git identity
39
+ run: |
40
+ git config user.name "forgejo-actions[bot]"
41
+ git config user.email "forgejo-actions[bot]@users.noreply.bryantserver.com"
42
+
43
+ - name: Compute next version
44
+ id: ver
45
+ env:
46
+ BUMP: ${{ inputs.bump }}
47
+ run: |
48
+ set -euo pipefail
49
+ PREV_TAG=$(git tag -l 'v*.*.*' --sort=-v:refname | head -n1 || true)
50
+ CURRENT="${PREV_TAG#v}"; CURRENT="${CURRENT:-0.0.0}"
51
+ MAJOR=$(echo "$CURRENT" | cut -d. -f1)
52
+ MINOR=$(echo "$CURRENT" | cut -d. -f2)
53
+ PATCH=$(echo "$CURRENT" | cut -d. -f3)
54
+ case "$BUMP" in
55
+ patch) PATCH=$((PATCH + 1)) ;;
56
+ minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
57
+ major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
58
+ esac
59
+ NEXT="${MAJOR}.${MINOR}.${PATCH}"
60
+ echo "version=${NEXT}" >> "$GITHUB_OUTPUT"
61
+ echo "tag=v${NEXT}" >> "$GITHUB_OUTPUT"
62
+ echo "Releasing ${NEXT} (bump=${BUMP}, previous=${PREV_TAG:-none})"
63
+
64
+ - name: Sanity guards
65
+ env:
66
+ VERSION: ${{ steps.ver.outputs.version }}
67
+ TAG: ${{ steps.ver.outputs.tag }}
68
+ run: |
69
+ set -euo pipefail
70
+ grep -qE '^## \[Unreleased\]' CHANGELOG.md || { echo "::error::Missing '## [Unreleased]'"; exit 1; }
71
+ ! grep -qE "^## \[${VERSION}\]" CHANGELOG.md || { echo "::error::CHANGELOG already has [${VERSION}]"; exit 1; }
72
+ ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null || { echo "::error::Tag ${TAG} exists"; exit 1; }
73
+
74
+ - name: Promote [Unreleased] in CHANGELOG
75
+ env:
76
+ VERSION: ${{ steps.ver.outputs.version }}
77
+ run: |
78
+ set -euo pipefail
79
+ DATE=$(date -u +%Y-%m-%d)
80
+ awk -v ver="$VERSION" -v date="$DATE" '
81
+ /^## \[Unreleased\]/ {
82
+ print "## [Unreleased]"; print ""; print "## [" ver "] - " date; next
83
+ }
84
+ { print }
85
+ ' CHANGELOG.md > CHANGELOG.md.new
86
+ mv CHANGELOG.md.new CHANGELOG.md
87
+
88
+ - name: Bump version strings
89
+ env:
90
+ VERSION: ${{ steps.ver.outputs.version }}
91
+ run: |
92
+ set -euo pipefail
93
+ V='[0-9]+\.[0-9]+\.[0-9]+'
94
+ sed -i -E "s/^version = \"$V\"/version = \"$VERSION\"/" pyproject.toml
95
+ sed -i -E "s/^__version__ = \"$V\"/__version__ = \"$VERSION\"/" src/whiskerless/__init__.py
96
+ sed -i -E "s/\"version\": \"$V\"/\"version\": \"$VERSION\"/" custom_components/whiskerless/manifest.json
97
+ sed -i -E "s/\"whiskerless==$V\"/\"whiskerless==$VERSION\"/" custom_components/whiskerless/manifest.json
98
+ git --no-pager diff
99
+
100
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
101
+ with:
102
+ python-version: "3.12"
103
+ - name: Test gate
104
+ run: |
105
+ python -m pip install --upgrade pip
106
+ python -m pip install -e ".[dev,ble]"
107
+ ruff check src custom_components tests
108
+ mypy
109
+ pytest -q
110
+
111
+ - name: Commit, tag, push
112
+ env:
113
+ VERSION: ${{ steps.ver.outputs.version }}
114
+ TAG: ${{ steps.ver.outputs.tag }}
115
+ TOKEN: ${{ secrets.CLUSTER_FORGEJO_REPO_WRITE_PAT }}
116
+ run: |
117
+ set -euo pipefail
118
+ git add CHANGELOG.md pyproject.toml src/whiskerless/__init__.py custom_components/whiskerless/manifest.json
119
+ git commit -m "release: ${VERSION}"
120
+ git tag "${TAG}"
121
+ AUTH_B64=$(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')
122
+ git -c "http.extraheader=Authorization: Basic ${AUTH_B64}" push origin "HEAD:${{ github.ref_name }}"
123
+ git -c "http.extraheader=Authorization: Basic ${AUTH_B64}" push origin "${TAG}"
@@ -0,0 +1,38 @@
1
+ name: Bug report
2
+ description: Something isn't working
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: Thanks for reporting! Please include enough detail to reproduce.
8
+ - type: input
9
+ id: version
10
+ attributes:
11
+ label: whiskerless version
12
+ placeholder: "0.1.0 (and HA version if relevant)"
13
+ validations:
14
+ required: true
15
+ - type: dropdown
16
+ id: surface
17
+ attributes:
18
+ label: Which part?
19
+ options:
20
+ - Home Assistant integration
21
+ - CLI / library
22
+ - BLE re-provisioning
23
+ - Documentation
24
+ validations:
25
+ required: true
26
+ - type: input
27
+ id: firmware
28
+ attributes:
29
+ label: Robot firmware version
30
+ description: From the "Refresh"/version report, e.g. ESP 1.1.75.
31
+ placeholder: "1.1.75"
32
+ - type: textarea
33
+ id: what
34
+ attributes:
35
+ label: What happened?
36
+ description: What you did, what you expected, what you got. Include logs (redact your serial).
37
+ validations:
38
+ required: true
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Documentation
4
+ url: https://github.com/SisyphusMD/whiskerless/tree/main/docs
5
+ about: Setup, protocol reference, and recovery guides.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an improvement or a new capability
4
+ title: "[feature] "
5
+ labels: enhancement
6
+ ---
7
+
8
+ ## What would you like?
9
+
10
+ <!-- A clear description of the feature or improvement. -->
11
+
12
+ ## Why
13
+
14
+ <!-- The problem it solves or the use case it enables. -->
15
+
16
+ ## Notes
17
+
18
+ <!--
19
+ If it involves a new device action that isn't supported yet (power, empty, reset),
20
+ please use the "Protocol capture" template instead — a captured payload is what
21
+ unblocks those.
22
+ -->
@@ -0,0 +1,49 @@
1
+ name: Protocol finding (help crack the missing commands)
2
+ description: Share a captured command or a confirmed register so we can close an open item
3
+ labels: ["protocol"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Some actions are deliberately **not** exposed yet — power on/off, the empty
9
+ cycle, and the panel/drawer resets — because we couldn't pin their exact
10
+ register+value to safe, proven confidence (see
11
+ [compatibility.md](../../docs/devices/litter-robot-4/compatibility.md)).
12
+
13
+ The zero-risk way to crack one: subscribe to your own broker's
14
+ `prod/LR4/<serial>/command` topic, press the button in the **Whisker app**,
15
+ and capture the literal `{"serial","data":["0x02RRVVVV"]}` it publishes.
16
+ That gives the register+value with no motor or brick risk. Share it here!
17
+ - type: dropdown
18
+ id: action
19
+ attributes:
20
+ label: Which action did you capture?
21
+ options:
22
+ - powerOn
23
+ - powerOff
24
+ - emptyCycle
25
+ - shortResetPress (panel reset)
26
+ - reset waste drawer
27
+ - other / a new register
28
+ validations:
29
+ required: true
30
+ - type: input
31
+ id: firmware
32
+ attributes:
33
+ label: Robot firmware version
34
+ placeholder: "ESP 1.1.75"
35
+ validations:
36
+ required: true
37
+ - type: textarea
38
+ id: payload
39
+ attributes:
40
+ label: The captured command payload(s)
41
+ description: The exact `0x...` codes the app published, and how you captured them.
42
+ placeholder: '{"serial":"LR4Cxxxxxx","data":["0x02300001"]}'
43
+ validations:
44
+ required: true
45
+ - type: textarea
46
+ id: effect
47
+ attributes:
48
+ label: Observed effect
49
+ description: What the robot did, and any before/after state (e.g. unitPowerStatus, odometer counts).
@@ -0,0 +1,23 @@
1
+ name: Validate (hassfest)
2
+
3
+ # hassfest is a GitHub-ecosystem action that the Forgejo runner can't fetch (it
4
+ # resolves actions from data.forgejo.org, which has no home-assistant/actions). So
5
+ # it runs only on GitHub; the guard makes Forgejo skip the job — Forgejo reads
6
+ # .github/workflows too, but evaluates the job `if` before fetching the action.
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ pull_request:
11
+ branches: [main]
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ hassfest:
18
+ name: hassfest (HA manifest validation)
19
+ if: ${{ github.server_url == 'https://github.com' }}
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23
+ - uses: home-assistant/actions/hassfest@f4ca6f671bd429efb108c0f2fa0ae8af0215986c # master
@@ -0,0 +1,100 @@
1
+ name: Release (macOS pkg)
2
+
3
+ # The one job that needs a Mac, so it lives on the GitHub mirror (free hosted
4
+ # macOS runners). It builds the signed + notarized .pkg and appends it to the
5
+ # GitHub + public-Forgejo releases — the only two it can reach. Forgejo's
6
+ # publish.yml created those releases (with the Linux binary + notes) and bridges
7
+ # the .pkg on to the internal NAS. See packaging/README.md for the secrets.
8
+ on:
9
+ push:
10
+ tags: ["v*.*.*"]
11
+
12
+ permissions:
13
+ contents: write
14
+
15
+ jobs:
16
+ build:
17
+ name: Build signed pkg (${{ matrix.arch }})
18
+ # Forgejo Actions also reads .github/workflows, but has no macOS runner — so
19
+ # only run on GitHub (server_url is the Forgejo instance URL on Forgejo).
20
+ if: ${{ github.server_url == 'https://github.com' }}
21
+ runs-on: ${{ matrix.os }}
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ include:
26
+ - os: macos-14
27
+ arch: arm64
28
+ - os: macos-13
29
+ arch: x86_64
30
+ env:
31
+ APP_CERT_P12: ${{ secrets.MACOS_APP_CERT_P12 }}
32
+ INSTALLER_CERT_P12: ${{ secrets.MACOS_INSTALLER_CERT_P12 }}
33
+ CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
34
+ APP_IDENTITY: ${{ secrets.MACOS_APP_IDENTITY }}
35
+ INSTALLER_IDENTITY: ${{ secrets.MACOS_INSTALLER_IDENTITY }}
36
+ NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_KEY_P8 }}
37
+ NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
38
+ NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_ISSUER }}
39
+ steps:
40
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
41
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
42
+ with:
43
+ python-version: "3.12"
44
+ - name: Build the binary
45
+ run: |
46
+ python -m pip install --upgrade pip pyinstaller
47
+ python -m pip install ".[ble]"
48
+ pyinstaller --onefile --name whiskerless \
49
+ --collect-all bleak --collect-all aiomqtt packaging/launcher.py
50
+ - name: Import signing certificates
51
+ run: |
52
+ KEYCHAIN="$RUNNER_TEMP/build.keychain"
53
+ security create-keychain -p "" "$KEYCHAIN"
54
+ security default-keychain -s "$KEYCHAIN"
55
+ security unlock-keychain -p "" "$KEYCHAIN"
56
+ echo "$APP_CERT_P12" | base64 -d > app.p12
57
+ echo "$INSTALLER_CERT_P12" | base64 -d > installer.p12
58
+ security import app.p12 -k "$KEYCHAIN" -P "$CERT_PASSWORD" -T /usr/bin/codesign
59
+ security import installer.p12 -k "$KEYCHAIN" -P "$CERT_PASSWORD" -T /usr/bin/productsign
60
+ security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN"
61
+ - name: Sign, package, notarize, staple
62
+ run: |
63
+ set -euo pipefail
64
+ PKG="whiskerless-macos-${{ matrix.arch }}.pkg"
65
+ codesign --force --options runtime --timestamp \
66
+ --entitlements packaging/entitlements.plist --sign "$APP_IDENTITY" dist/whiskerless
67
+ mkdir -p root/usr/local/bin
68
+ cp dist/whiskerless root/usr/local/bin/whiskerless
69
+ pkgbuild --root root --identifier com.sisyphusmd.whiskerless \
70
+ --version "${GITHUB_REF_NAME#v}" --install-location / unsigned.pkg
71
+ productsign --sign "$INSTALLER_IDENTITY" unsigned.pkg "$PKG"
72
+ echo "$NOTARY_KEY_P8" | base64 -d > notary.p8
73
+ xcrun notarytool submit "$PKG" \
74
+ --key notary.p8 --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER" --wait
75
+ xcrun stapler staple "$PKG"
76
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
77
+ with:
78
+ name: pkg-${{ matrix.arch }}
79
+ path: whiskerless-macos-${{ matrix.arch }}.pkg
80
+
81
+ publish:
82
+ name: Append the signed .pkg to the GitHub + Forgejo releases
83
+ if: ${{ github.server_url == 'https://github.com' }}
84
+ needs: build
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
88
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
89
+ with:
90
+ path: pkgs
91
+ merge-multiple: true
92
+ - name: Release notes from CHANGELOG
93
+ run: bash packaging/changelog-section.sh "${GITHUB_REF_NAME#v}" > notes.md
94
+ - name: Append the .pkg to the GitHub + public-Forgejo releases
95
+ run: |
96
+ chmod +x packaging/github-release.sh packaging/forgejo-release.sh
97
+ packaging/github-release.sh "${{ secrets.GITHUB_TOKEN }}" \
98
+ "$GITHUB_REF_NAME" notes.md pkgs/*.pkg
99
+ packaging/forgejo-release.sh forgejo.bryantserver.com \
100
+ "${{ secrets.CLUSTER_FORGEJO_REPO_WRITE_PAT }}" "$GITHUB_REF_NAME" notes.md pkgs/*.pkg
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+
11
+ # Tooling caches
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .pytest_cache/
15
+ .coverage
16
+ htmlcov/
17
+
18
+ # Local scratch / research notes (not shipped)
19
+ .research/
20
+
21
+ # Operator-only mirror bootstrap (embeds private Forgejo hostnames) — keep local
22
+ /scripts/setup-mirrors.sh
23
+
24
+ # Secrets & device-specific material — NEVER commit these
25
+ *.pem
26
+ *.key
27
+ ca.crt
28
+ secrets.yaml
29
+
30
+ # OS
31
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:recommended"],
4
+ "gitAuthor": "Renovate Bot <renovate@users.noreply.github.com>",
5
+ "prConcurrentLimit": 0,
6
+ "prHourlyLimit": 0,
7
+ "separateMinorPatch": true,
8
+ "enabledManagers": ["pep621", "github-actions", "pip_requirements"],
9
+ "packageRules": [
10
+ {
11
+ "description": "Automerge patch + digest bumps; minor/major reviewed manually.",
12
+ "matchUpdateTypes": ["patch", "digest"],
13
+ "automerge": true
14
+ },
15
+ {
16
+ "description": "Keep the integration's pinned lib requirement in lockstep manually.",
17
+ "matchFileNames": ["custom_components/whiskerless/manifest.json"],
18
+ "enabled": false
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-06-29
10
+
11
+ ### Added
12
+
13
+ - **`whiskerless` Python library** — fully-local MQTT control + telemetry for the
14
+ Whisker Litter-Robot 4:
15
+ - LR4 wire codec, command catalog, and a typed state model that decodes both raw
16
+ firmware integers and cloud-style strings defensively.
17
+ - A push-first `LitterRobot4Client` with a self-healing MQTT connection and
18
+ write → read-back → retry for the firmware's commit-latency registers.
19
+ - Device-agnostic BLE (esp-idf protocomm) re-provisioning with a self-contained
20
+ pure-Python protobuf codec (no `protoc` build step).
21
+ - A `safety` guard that refuses brick/reset-class commands (`0xAC`/`0xA4`/`0xAD`)
22
+ unconditionally and gates motor / untraced commands.
23
+ - **`whiskerless` CLI** — `provision`, `monitor`, `state`, `read`, `set`,
24
+ `clean-cycle`, and a guarded raw `send`.
25
+ - **Home Assistant integration** (HACS) built to Platinum standards: fully async,
26
+ fully typed, `local_push`, with **MQTT discovery** (robots appear as Add/Ignore
27
+ cards), diagnostics, translations, and per-robot config entries (any number of
28
+ robots).
29
+ - **Documentation** — protocol reference, register map, command catalog,
30
+ compatibility matrix, setup guides, recovery guide, and the reverse-engineering
31
+ writeup.
32
+ - **Standalone CLI binaries** built on release for users who want the BLE
33
+ re-provisioner without installing Python.
34
+
35
+ [Unreleased]: https://github.com/SisyphusMD/whiskerless/commits/main