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.
- whiskerless-0.1.0/.editorconfig +14 -0
- whiskerless-0.1.0/.forgejo/workflows/ci.yml +56 -0
- whiskerless-0.1.0/.forgejo/workflows/publish.yml +87 -0
- whiskerless-0.1.0/.forgejo/workflows/release.yml +123 -0
- whiskerless-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +38 -0
- whiskerless-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- whiskerless-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- whiskerless-0.1.0/.github/ISSUE_TEMPLATE/protocol_contribution.yml +49 -0
- whiskerless-0.1.0/.github/workflows/hassfest.yml +23 -0
- whiskerless-0.1.0/.github/workflows/release-macos.yml +100 -0
- whiskerless-0.1.0/.gitignore +31 -0
- whiskerless-0.1.0/.renovaterc.json +21 -0
- whiskerless-0.1.0/CHANGELOG.md +35 -0
- whiskerless-0.1.0/CONTRIBUTING.md +109 -0
- whiskerless-0.1.0/LICENSE +21 -0
- whiskerless-0.1.0/PKG-INFO +207 -0
- whiskerless-0.1.0/README.md +173 -0
- whiskerless-0.1.0/conftest.py +16 -0
- whiskerless-0.1.0/custom_components/whiskerless/__init__.py +36 -0
- whiskerless-0.1.0/custom_components/whiskerless/binary_sensor.py +95 -0
- whiskerless-0.1.0/custom_components/whiskerless/button.py +75 -0
- whiskerless-0.1.0/custom_components/whiskerless/config_flow.py +74 -0
- whiskerless-0.1.0/custom_components/whiskerless/const.py +21 -0
- whiskerless-0.1.0/custom_components/whiskerless/coordinator.py +225 -0
- whiskerless-0.1.0/custom_components/whiskerless/devices/__init__.py +5 -0
- whiskerless-0.1.0/custom_components/whiskerless/devices/litter_robot_4.py +29 -0
- whiskerless-0.1.0/custom_components/whiskerless/diagnostics.py +33 -0
- whiskerless-0.1.0/custom_components/whiskerless/entity.py +52 -0
- whiskerless-0.1.0/custom_components/whiskerless/icons.json +37 -0
- whiskerless-0.1.0/custom_components/whiskerless/manifest.json +14 -0
- whiskerless-0.1.0/custom_components/whiskerless/number.py +82 -0
- whiskerless-0.1.0/custom_components/whiskerless/quality_scale.yaml +96 -0
- whiskerless-0.1.0/custom_components/whiskerless/select.py +93 -0
- whiskerless-0.1.0/custom_components/whiskerless/sensor.py +145 -0
- whiskerless-0.1.0/custom_components/whiskerless/strings.json +90 -0
- whiskerless-0.1.0/custom_components/whiskerless/switch.py +99 -0
- whiskerless-0.1.0/custom_components/whiskerless/time.py +84 -0
- whiskerless-0.1.0/custom_components/whiskerless/translations/en.json +90 -0
- whiskerless-0.1.0/docs/devices/litter-robot-4/commands.md +76 -0
- whiskerless-0.1.0/docs/devices/litter-robot-4/compatibility.md +64 -0
- whiskerless-0.1.0/docs/devices/litter-robot-4/protocol.md +97 -0
- whiskerless-0.1.0/docs/devices/litter-robot-4/registers.md +73 -0
- whiskerless-0.1.0/docs/how-it-works.md +65 -0
- whiskerless-0.1.0/docs/recovery.md +86 -0
- whiskerless-0.1.0/docs/reverse-engineering.md +73 -0
- whiskerless-0.1.0/docs/setup/certificates.md +106 -0
- whiskerless-0.1.0/docs/setup/home-assistant.md +121 -0
- whiskerless-0.1.0/docs/setup/mqtt-broker.md +114 -0
- whiskerless-0.1.0/examples/litter-robot-4/README.md +49 -0
- whiskerless-0.1.0/examples/litter-robot-4/automations.yaml +91 -0
- whiskerless-0.1.0/hacs.json +7 -0
- whiskerless-0.1.0/packaging/README.md +73 -0
- whiskerless-0.1.0/packaging/changelog-section.sh +13 -0
- whiskerless-0.1.0/packaging/entitlements.plist +17 -0
- whiskerless-0.1.0/packaging/forgejo-release.sh +36 -0
- whiskerless-0.1.0/packaging/github-release.sh +37 -0
- whiskerless-0.1.0/packaging/launcher.py +14 -0
- whiskerless-0.1.0/pyproject.toml +95 -0
- whiskerless-0.1.0/src/whiskerless/__init__.py +42 -0
- whiskerless-0.1.0/src/whiskerless/ble/__init__.py +24 -0
- whiskerless-0.1.0/src/whiskerless/ble/messages.py +121 -0
- whiskerless-0.1.0/src/whiskerless/ble/protobuf.py +102 -0
- whiskerless-0.1.0/src/whiskerless/ble/provision.py +195 -0
- whiskerless-0.1.0/src/whiskerless/ble/transport.py +121 -0
- whiskerless-0.1.0/src/whiskerless/cli.py +331 -0
- whiskerless-0.1.0/src/whiskerless/devices/__init__.py +1 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/__init__.py +41 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/client.py +289 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/codec.py +81 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/commands.py +198 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/const.py +165 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/link.py +116 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/models.py +215 -0
- whiskerless-0.1.0/src/whiskerless/devices/litter_robot_4/protocol.py +87 -0
- whiskerless-0.1.0/src/whiskerless/exceptions.py +50 -0
- whiskerless-0.1.0/src/whiskerless/mqtt.py +63 -0
- whiskerless-0.1.0/src/whiskerless/py.typed +0 -0
- whiskerless-0.1.0/src/whiskerless/safety.py +142 -0
- whiskerless-0.1.0/tests/integration/__init__.py +56 -0
- whiskerless-0.1.0/tests/integration/conftest.py +42 -0
- whiskerless-0.1.0/tests/integration/const.py +11 -0
- whiskerless-0.1.0/tests/integration/fixtures/lr4_state.json +34 -0
- whiskerless-0.1.0/tests/integration/test_config_flow.py +62 -0
- whiskerless-0.1.0/tests/integration/test_diagnostics.py +28 -0
- whiskerless-0.1.0/tests/integration/test_entities.py +46 -0
- whiskerless-0.1.0/tests/integration/test_init.py +46 -0
- whiskerless-0.1.0/tests/test_codec.py +65 -0
- whiskerless-0.1.0/tests/test_commands.py +54 -0
- whiskerless-0.1.0/tests/test_models.py +63 -0
- whiskerless-0.1.0/tests/test_protobuf.py +52 -0
- whiskerless-0.1.0/tests/test_protocol.py +57 -0
- whiskerless-0.1.0/tests/test_safety.py +63 -0
|
@@ -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,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
|