pyplusportal 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.
- pyplusportal-0.1.0/.claude/hooks/format-python.sh +15 -0
- pyplusportal-0.1.0/.claude/hooks/gates-before-push.sh +33 -0
- pyplusportal-0.1.0/.claude/hooks/gates.sh +25 -0
- pyplusportal-0.1.0/.claude/hooks/no-account-data.sh +69 -0
- pyplusportal-0.1.0/.claude/hooks/no-direct-push-to-main.sh +39 -0
- pyplusportal-0.1.0/.claude/settings.json +35 -0
- pyplusportal-0.1.0/.claude/skills/release/SKILL.md +47 -0
- pyplusportal-0.1.0/.claude/skills/verify/SKILL.md +57 -0
- pyplusportal-0.1.0/.env.example +15 -0
- pyplusportal-0.1.0/.githooks/pre-push +20 -0
- pyplusportal-0.1.0/.github/rulesets/main.json +66 -0
- pyplusportal-0.1.0/.github/workflows/ci.yml +91 -0
- pyplusportal-0.1.0/.github/workflows/claude-review.yml +67 -0
- pyplusportal-0.1.0/.github/workflows/claude.yml +65 -0
- pyplusportal-0.1.0/.github/workflows/release.yml +63 -0
- pyplusportal-0.1.0/.gitignore +37 -0
- pyplusportal-0.1.0/.python-version +1 -0
- pyplusportal-0.1.0/CLAUDE.md +110 -0
- pyplusportal-0.1.0/LICENSE +21 -0
- pyplusportal-0.1.0/PKG-INFO +198 -0
- pyplusportal-0.1.0/README.md +177 -0
- pyplusportal-0.1.0/RELEASING.md +67 -0
- pyplusportal-0.1.0/custom_components/plusportal/__init__.py +39 -0
- pyplusportal-0.1.0/custom_components/plusportal/brand/icon.png +0 -0
- pyplusportal-0.1.0/custom_components/plusportal/brand/icon@2x.png +0 -0
- pyplusportal-0.1.0/custom_components/plusportal/config_flow.py +246 -0
- pyplusportal-0.1.0/custom_components/plusportal/const.py +35 -0
- pyplusportal-0.1.0/custom_components/plusportal/coordinator.py +174 -0
- pyplusportal-0.1.0/custom_components/plusportal/diagnostics.py +116 -0
- pyplusportal-0.1.0/custom_components/plusportal/manifest.json +19 -0
- pyplusportal-0.1.0/custom_components/plusportal/sensor.py +400 -0
- pyplusportal-0.1.0/custom_components/plusportal/statistics.py +185 -0
- pyplusportal-0.1.0/custom_components/plusportal/strings.json +98 -0
- pyplusportal-0.1.0/custom_components/plusportal/tariff.py +68 -0
- pyplusportal-0.1.0/custom_components/plusportal/translations/de.json +98 -0
- pyplusportal-0.1.0/custom_components/plusportal/translations/en.json +98 -0
- pyplusportal-0.1.0/docker-compose.dev.yml +23 -0
- pyplusportal-0.1.0/docs/AGENTIC-SDLC.md +85 -0
- pyplusportal-0.1.0/docs/specs/README.md +38 -0
- pyplusportal-0.1.0/docs/specs/cost.md +56 -0
- pyplusportal-0.1.0/docs/specs/extraction.md +131 -0
- pyplusportal-0.1.0/docs/specs/integration.md +154 -0
- pyplusportal-0.1.0/docs/specs/safety.md +35 -0
- pyplusportal-0.1.0/docs/superpowers/plans/2026-07-25-cli-help-plan.md +118 -0
- pyplusportal-0.1.0/docs/superpowers/specs/2026-07-25-plusportal-ha-integration-design.md +372 -0
- pyplusportal-0.1.0/hacs.json +4 -0
- pyplusportal-0.1.0/pyproject.toml +111 -0
- pyplusportal-0.1.0/pytest-ha.ini +13 -0
- pyplusportal-0.1.0/scripts/apply-ruleset.sh +29 -0
- pyplusportal-0.1.0/scripts/check_version_bump.py +109 -0
- pyplusportal-0.1.0/scripts/ha-dev.sh +152 -0
- pyplusportal-0.1.0/scripts/ha_prune.py +83 -0
- pyplusportal-0.1.0/scripts/ha_state.py +69 -0
- pyplusportal-0.1.0/src/pyplusportal/__init__.py +41 -0
- pyplusportal-0.1.0/src/pyplusportal/cli.py +728 -0
- pyplusportal-0.1.0/src/pyplusportal/client.py +436 -0
- pyplusportal-0.1.0/src/pyplusportal/const.py +53 -0
- pyplusportal-0.1.0/src/pyplusportal/cost.py +225 -0
- pyplusportal-0.1.0/src/pyplusportal/exceptions.py +28 -0
- pyplusportal-0.1.0/src/pyplusportal/json.py +41 -0
- pyplusportal-0.1.0/src/pyplusportal/models.py +421 -0
- pyplusportal-0.1.0/tests/__init__.py +1 -0
- pyplusportal-0.1.0/tests/conftest.py +62 -0
- pyplusportal-0.1.0/tests/fixtures/diagram_config.json +45 -0
- pyplusportal-0.1.0/tests/fixtures/diagram_result_july2026.json +608 -0
- pyplusportal-0.1.0/tests/fixtures/overview.json +40 -0
- pyplusportal-0.1.0/tests/fixtures/session.json +34 -0
- pyplusportal-0.1.0/tests/fixtures/user_item_list.json +192 -0
- pyplusportal-0.1.0/tests/test_cli.py +607 -0
- pyplusportal-0.1.0/tests/test_client.py +268 -0
- pyplusportal-0.1.0/tests/test_cost.py +332 -0
- pyplusportal-0.1.0/tests/test_intervals.py +246 -0
- pyplusportal-0.1.0/tests/test_json.py +58 -0
- pyplusportal-0.1.0/tests/test_live.py +173 -0
- pyplusportal-0.1.0/tests/test_models.py +283 -0
- pyplusportal-0.1.0/tests/test_packaging.py +124 -0
- pyplusportal-0.1.0/tests/test_readings.py +317 -0
- pyplusportal-0.1.0/tests/test_traceability.py +105 -0
- pyplusportal-0.1.0/tests_ha/__init__.py +1 -0
- pyplusportal-0.1.0/tests_ha/conftest.py +138 -0
- pyplusportal-0.1.0/tests_ha/test_config_flow.py +221 -0
- pyplusportal-0.1.0/tests_ha/test_sensor.py +552 -0
- pyplusportal-0.1.0/tests_ha/test_statistics.py +269 -0
- pyplusportal-0.1.0/uv.lock +8016 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Formats and auto-fixes a Python file right after it is written, so the
|
|
3
|
+
# `ruff format --check` gate never fails for something a tool can fix.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
payload=$(cat)
|
|
7
|
+
file=$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("file_path",""))' 2>/dev/null)
|
|
8
|
+
|
|
9
|
+
case "$file" in (*.py) ;; (*) exit 0 ;; esac
|
|
10
|
+
[ -f "$file" ] || exit 0
|
|
11
|
+
|
|
12
|
+
cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 0
|
|
13
|
+
uv run ruff check --fix -q "$file" >/dev/null 2>&1
|
|
14
|
+
uv run ruff format -q "$file" >/dev/null 2>&1
|
|
15
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Refuses `git push` when the gates CI runs are not green.
|
|
3
|
+
#
|
|
4
|
+
# The git pre-push hook already does this for pushes from a shell. This one
|
|
5
|
+
# covers pushes an agent makes, and gives it the failure output directly so it
|
|
6
|
+
# can fix the cause instead of retrying.
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
payload=$(cat)
|
|
10
|
+
command=$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' 2>/dev/null)
|
|
11
|
+
|
|
12
|
+
case "$command" in
|
|
13
|
+
(*"git push"*) ;;
|
|
14
|
+
(*) exit 0 ;;
|
|
15
|
+
esac
|
|
16
|
+
# The pre-push git hook handles the shell path; skip when it is being bypassed
|
|
17
|
+
# deliberately, so --no-verify keeps meaning what it says.
|
|
18
|
+
case "$command" in (*--no-verify*) exit 0 ;; esac
|
|
19
|
+
|
|
20
|
+
cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 0
|
|
21
|
+
|
|
22
|
+
if ! output=$(./.claude/hooks/gates.sh 2>&1); then
|
|
23
|
+
{
|
|
24
|
+
echo "Blocked: main must stay green, and the gates are not."
|
|
25
|
+
echo
|
|
26
|
+
echo "$output"
|
|
27
|
+
echo
|
|
28
|
+
echo "Fix these before pushing. Do not bypass with --no-verify unless the"
|
|
29
|
+
echo "user has asked for a deliberately red push."
|
|
30
|
+
} >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Runs every gate CI runs. Used by the pre-push hook and the `verify` skill, so
|
|
3
|
+
# a red pipeline is discovered here rather than on GitHub.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
6
|
+
|
|
7
|
+
fail=0
|
|
8
|
+
run() {
|
|
9
|
+
local label="$1"; shift
|
|
10
|
+
if out=$("$@" 2>&1); then
|
|
11
|
+
printf ' ok %s\n' "$label"
|
|
12
|
+
else
|
|
13
|
+
printf ' FAIL %s\n' "$label"
|
|
14
|
+
printf '%s\n' "$out" | tail -15 | sed 's/^/ /'
|
|
15
|
+
fail=1
|
|
16
|
+
fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
echo "gates:"
|
|
20
|
+
run "ruff check" uv run ruff check src tests tests_ha custom_components
|
|
21
|
+
run "ruff format" uv run ruff format --check src tests tests_ha custom_components
|
|
22
|
+
run "mypy" uv run --group ha mypy
|
|
23
|
+
run "library tests" uv run pytest -q
|
|
24
|
+
run "ha tests" uv run --group ha pytest -c pytest-ha.ini -q
|
|
25
|
+
exit $fail
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Blocks commits that would put real account data into the repository.
|
|
3
|
+
#
|
|
4
|
+
# This exists because it already happened: a meter number, a customer number
|
|
5
|
+
# and a portal user id reached tracked files, including — of all places — the
|
|
6
|
+
# test that proves the redaction helper works. They were caught by hand, one
|
|
7
|
+
# step before the repository went public. This hook is the part that does not
|
|
8
|
+
# depend on someone remembering.
|
|
9
|
+
#
|
|
10
|
+
# Reads a Claude Code PreToolUse payload on stdin. Exit 2 blocks the tool call
|
|
11
|
+
# and returns stderr to the model.
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
|
|
14
|
+
payload=$(cat)
|
|
15
|
+
command=$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' 2>/dev/null)
|
|
16
|
+
|
|
17
|
+
# Only interested in commits. Everything else passes straight through.
|
|
18
|
+
case "$command" in
|
|
19
|
+
*"git commit"*) ;;
|
|
20
|
+
*) exit 0 ;;
|
|
21
|
+
esac
|
|
22
|
+
|
|
23
|
+
cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 0
|
|
24
|
+
staged=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null)
|
|
25
|
+
[ -z "$staged" ] && exit 0
|
|
26
|
+
|
|
27
|
+
# Patterns for values that identify a real account or connection point.
|
|
28
|
+
# Deliberately shaped, not a list of known values: a new meter number must trip
|
|
29
|
+
# this too.
|
|
30
|
+
findings=$(printf '%s\n' "$staged" | while IFS= read -r file; do
|
|
31
|
+
# Patterns are wrapped in balanced parens: inside $( ), an unbalanced ')'
|
|
32
|
+
# from a case pattern terminates the substitution and breaks the parser.
|
|
33
|
+
case "$file" in
|
|
34
|
+
(uv.lock|*.png|*.lock) continue ;; # hashes collide with digit runs
|
|
35
|
+
(.claude/hooks/no-account-data.sh) continue ;; # this file describes the patterns
|
|
36
|
+
(docs/specs/safety.md) continue ;; # so does the spec
|
|
37
|
+
esac
|
|
38
|
+
[ -f "$file" ] || continue
|
|
39
|
+
|
|
40
|
+
git diff --cached -U0 -- "$file" | grep '^+' | grep -v '^+++' | \
|
|
41
|
+
grep -nEi \
|
|
42
|
+
-e '[0-9][A-Z]{3}[0-9]{10}' \
|
|
43
|
+
-e '\b1[0-9]{9}\b' \
|
|
44
|
+
-e '\b0003[0-9]{2}\b' \
|
|
45
|
+
-e 'plusportal\.de' \
|
|
46
|
+
-e 'PLUSPORTAL_PASSWORD=.+' \
|
|
47
|
+
| grep -viE '123456|1ABC0000000000|1XYZ9876543210|1000000000|<tenant>' \
|
|
48
|
+
| sed "s|^| $file: |"
|
|
49
|
+
done)
|
|
50
|
+
|
|
51
|
+
if [ -n "$findings" ]; then
|
|
52
|
+
cat >&2 <<EOF
|
|
53
|
+
Blocked: the staged changes look like they contain real account data.
|
|
54
|
+
|
|
55
|
+
$findings
|
|
56
|
+
|
|
57
|
+
Nothing identifying a real account belongs in this repository — no meter
|
|
58
|
+
numbers, customer numbers, portal user ids, real tenant numbers or portal
|
|
59
|
+
hostnames. Use fictional values (tenant 123456, meter 1ABC0000000000*,
|
|
60
|
+
user 1000000000) and take real ones from the environment; see
|
|
61
|
+
docs/specs/safety.md.
|
|
62
|
+
|
|
63
|
+
If this is a false positive, say so and the check can be narrowed. Do not
|
|
64
|
+
work around it by unstaging the file and committing it another way.
|
|
65
|
+
EOF
|
|
66
|
+
exit 2
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
exit 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Refuses a direct push to main. Every change arrives as a pull request.
|
|
3
|
+
#
|
|
4
|
+
# GitHub enforces this too — the ruleset on main requires one — but its refusal
|
|
5
|
+
# comes after the push has been attempted and reads like a permissions error.
|
|
6
|
+
# This says what to do instead, before anything is sent.
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
payload=$(cat)
|
|
10
|
+
command=$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' 2>/dev/null)
|
|
11
|
+
|
|
12
|
+
case "$command" in
|
|
13
|
+
(*"git push"*) ;;
|
|
14
|
+
(*) exit 0 ;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 0
|
|
18
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
19
|
+
|
|
20
|
+
# Pushing main, either by being on it or by naming it as the refspec.
|
|
21
|
+
targets_main=0
|
|
22
|
+
[ "$branch" = "main" ] && targets_main=1
|
|
23
|
+
case "$command" in (*" main"*|*":main"*|*"HEAD:main"*) targets_main=1 ;; esac
|
|
24
|
+
[ "$targets_main" = "0" ] && exit 0
|
|
25
|
+
|
|
26
|
+
cat >&2 <<EOF
|
|
27
|
+
Blocked: main takes pull requests only.
|
|
28
|
+
|
|
29
|
+
Work on a branch and open a pull request instead:
|
|
30
|
+
|
|
31
|
+
git switch -c <type>/<short-description>
|
|
32
|
+
git push -u origin <branch>
|
|
33
|
+
gh pr create --fill
|
|
34
|
+
|
|
35
|
+
Branch names follow the change type — feat/, fix/, chore/, docs/, refactor/ —
|
|
36
|
+
because the release notes and the version bump are derived from it. See
|
|
37
|
+
docs/AGENTIC-SDLC.md.
|
|
38
|
+
EOF
|
|
39
|
+
exit 2
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Bash",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/no-account-data.sh"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/no-direct-push-to-main.sh"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/gates-before-push.sh"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"PostToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Write|Edit",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-python.sh"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release
|
|
3
|
+
description: Use when cutting a release of this project — bumping the version, publishing pyplusportal to PyPI and shipping the integration through HACS.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Release
|
|
7
|
+
|
|
8
|
+
Two artefacts move together: the library on PyPI and the integration that pins it. Three
|
|
9
|
+
places carry the version and `tests/test_packaging.py` ties them to each other; the release
|
|
10
|
+
workflow adds the git tag to the same chain, so a mismatch fails before anything publishes.
|
|
11
|
+
|
|
12
|
+
## Bump
|
|
13
|
+
|
|
14
|
+
All three, in one commit:
|
|
15
|
+
|
|
16
|
+
- `pyproject.toml` → `[project] version`
|
|
17
|
+
- `custom_components/plusportal/manifest.json` → `version`
|
|
18
|
+
- `custom_components/plusportal/manifest.json` → `requirements` pin
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run pytest tests/test_packaging.py # proves they agree
|
|
22
|
+
./.claude/hooks/gates.sh # everything else
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Tag
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git commit -am "Release X.Y.Z"
|
|
29
|
+
git tag vX.Y.Z
|
|
30
|
+
git push origin main --tags
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The workflow verifies the tag matches the project version, runs both suites, builds,
|
|
34
|
+
publishes to PyPI through Trusted Publishing and creates the GitHub release. No API token
|
|
35
|
+
is stored anywhere; see RELEASING.md for the one-time PyPI setup.
|
|
36
|
+
|
|
37
|
+
## Before the first public release
|
|
38
|
+
|
|
39
|
+
- The repository must be public, or HACS validation cannot fetch `hacs.json` and
|
|
40
|
+
`manifest.json` — it fetches them unauthenticated.
|
|
41
|
+
- Apply the branch ruleset once that is true: `scripts/apply-ruleset.sh`.
|
|
42
|
+
- `pyplusportal` must still be free on PyPI.
|
|
43
|
+
|
|
44
|
+
## Do not
|
|
45
|
+
|
|
46
|
+
Publish on the user's behalf without being asked. Tagging is theirs to trigger; a release
|
|
47
|
+
is public and cannot be unpublished from PyPI.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify
|
|
3
|
+
description: Use when a change to this repository needs verifying before commit or push — runs every gate CI runs, then exercises the affected layer for real. Also use when CI is red and you need to reproduce it locally.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify
|
|
7
|
+
|
|
8
|
+
Green gates are necessary, not sufficient. The gates prove the code is consistent;
|
|
9
|
+
exercising it proves it does the right thing.
|
|
10
|
+
|
|
11
|
+
## 1. Gates — always
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
./.claude/hooks/gates.sh
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
ruff check, ruff format, mypy strict, both test suites. Exactly what CI runs, so a red
|
|
18
|
+
pipeline shows up here first. All five must pass; the pre-push hook refuses otherwise.
|
|
19
|
+
|
|
20
|
+
## 2. Exercise the layer you touched
|
|
21
|
+
|
|
22
|
+
**Client or CLI** — run it against the real portal. The offline suite uses recorded
|
|
23
|
+
fixtures, so it cannot catch a changed API:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run pytest -m live # reconciles three figures against the portal
|
|
27
|
+
uv run pyplusportal overview
|
|
28
|
+
uv run pyplusportal readings --from <date> --to <date>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`-m live` skips silently without `.env`. If it skips, you have not verified anything —
|
|
32
|
+
say so rather than reporting success.
|
|
33
|
+
|
|
34
|
+
**Integration** — the HA suite covers logic, not appearance. For anything a user sees
|
|
35
|
+
(config flow wording, entity names, dashboard behaviour), load it in a real Home Assistant
|
|
36
|
+
and look. If you cannot, say which parts are unverified.
|
|
37
|
+
|
|
38
|
+
**Statistics** — the failure mode is a plausible-looking wrong graph. Check the numbers,
|
|
39
|
+
not just that rows were written.
|
|
40
|
+
|
|
41
|
+
## 3. When a test passes the moment you write it
|
|
42
|
+
|
|
43
|
+
Mutate the implementation and confirm the test fails. This repository has shipped three
|
|
44
|
+
assertions that could not fail: two matched text argparse prints anyway, one asserted a
|
|
45
|
+
value equals itself. Each was caught in review, not by the suite.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# stub the function the test targets, run just that test, expect red, restore
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Report the mutation you tried and what happened. "Tests pass" is not evidence when the
|
|
52
|
+
test is new.
|
|
53
|
+
|
|
54
|
+
## 4. Report honestly
|
|
55
|
+
|
|
56
|
+
State what you ran, what passed, and what you did not verify. A skipped live suite, an
|
|
57
|
+
unexercised UI path and an untested error branch are all worth one sentence each.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copy to .env and fill in. Never commit .env.
|
|
2
|
+
# Tenant number from your portal URL: https://123456.plusportal.de -> 123456
|
|
3
|
+
PLUSPORTAL_TENANT=123456
|
|
4
|
+
# Alternatively the full base URL (takes precedence over PLUSPORTAL_TENANT):
|
|
5
|
+
# PLUSPORTAL_BASE_URL=https://123456.plusportal.de
|
|
6
|
+
|
|
7
|
+
# Portal login — either the e-mail address or the customer number.
|
|
8
|
+
PLUSPORTAL_USERNAME=
|
|
9
|
+
PLUSPORTAL_PASSWORD=
|
|
10
|
+
|
|
11
|
+
# Tariff (all optional, needed only for the `cost` command):
|
|
12
|
+
# PLUSPORTAL_ENERGY_PRICE_CT=32.5 # energy price in ct/kWh; required by 'cost'
|
|
13
|
+
# PLUSPORTAL_BASE_PRICE_EUR=120.00 # annual standing charge in EUR; used by 'cost'
|
|
14
|
+
# PLUSPORTAL_MONTHLY_ADVANCE_EUR=85.00 # monthly advance payment in EUR; used by 'cost'
|
|
15
|
+
# PLUSPORTAL_BILLING_YEAR_START=01-01 # billing year start as MM-DD; default 01-01
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Refuses to push when the gates CI runs would fail.
|
|
3
|
+
#
|
|
4
|
+
# GitHub-side enforcement (a required-status-checks ruleset on main) needs a
|
|
5
|
+
# public repository or GitHub Pro, and is blocked while this repo is private.
|
|
6
|
+
# .github/rulesets/main.json holds it ready; scripts/apply-ruleset.sh applies it
|
|
7
|
+
# the moment that changes. Until then this hook is what keeps main green.
|
|
8
|
+
#
|
|
9
|
+
# Bypass with --no-verify only when you intend to push something red, and say so.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
12
|
+
|
|
13
|
+
if ! ./.claude/hooks/gates.sh; then
|
|
14
|
+
cat >&2 <<'EOF'
|
|
15
|
+
|
|
16
|
+
Push refused: the gates above are what CI runs, and they are not green.
|
|
17
|
+
Fix them, or push with --no-verify if you deliberately want a red main.
|
|
18
|
+
EOF
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "main must stay green",
|
|
3
|
+
"target": "branch",
|
|
4
|
+
"enforcement": "active",
|
|
5
|
+
"conditions": {
|
|
6
|
+
"ref_name": {
|
|
7
|
+
"include": [
|
|
8
|
+
"~DEFAULT_BRANCH"
|
|
9
|
+
],
|
|
10
|
+
"exclude": []
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"rules": [
|
|
14
|
+
{
|
|
15
|
+
"type": "deletion"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "non_fast_forward"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "pull_request",
|
|
22
|
+
"parameters": {
|
|
23
|
+
"required_approving_review_count": 0,
|
|
24
|
+
"dismiss_stale_reviews_on_push": false,
|
|
25
|
+
"require_code_owner_review": false,
|
|
26
|
+
"require_last_push_approval": false,
|
|
27
|
+
"required_review_thread_resolution": false,
|
|
28
|
+
"automatic_copilot_code_review_enabled": false,
|
|
29
|
+
"allowed_merge_methods": [
|
|
30
|
+
"squash",
|
|
31
|
+
"merge",
|
|
32
|
+
"rebase"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "required_status_checks",
|
|
38
|
+
"parameters": {
|
|
39
|
+
"strict_required_status_checks_policy": true,
|
|
40
|
+
"required_status_checks": [
|
|
41
|
+
{
|
|
42
|
+
"context": "Lint and types"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"context": "Library tests (Python 3.12)"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"context": "Library tests (Python 3.13)"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"context": "Home Assistant integration tests"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"context": "Home Assistant manifest"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"context": "HACS validation"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"context": "Version bump"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: Lint and types
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
# Pinned exactly: setup-uv publishes floating major tags only up to v7.
|
|
20
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
- run: uv sync --group ha
|
|
24
|
+
- name: ruff
|
|
25
|
+
run: uv run ruff check src tests tests_ha custom_components
|
|
26
|
+
- name: ruff format
|
|
27
|
+
run: uv run ruff format --check src tests tests_ha custom_components
|
|
28
|
+
- name: mypy
|
|
29
|
+
run: uv run --group ha mypy
|
|
30
|
+
|
|
31
|
+
test-library:
|
|
32
|
+
name: Library tests (Python ${{ matrix.python }})
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
python: ["3.12", "3.13"]
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
41
|
+
with:
|
|
42
|
+
enable-cache: true
|
|
43
|
+
- run: uv sync --python ${{ matrix.python }}
|
|
44
|
+
# The `live` marker is excluded by default, so no credentials are needed
|
|
45
|
+
# and CI never talks to a real customer portal.
|
|
46
|
+
- run: uv run pytest -q --cov --cov-report=term --cov-fail-under=90
|
|
47
|
+
|
|
48
|
+
test-integration:
|
|
49
|
+
name: Home Assistant integration tests
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v7
|
|
53
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
54
|
+
with:
|
|
55
|
+
enable-cache: true
|
|
56
|
+
- run: uv sync --group ha
|
|
57
|
+
- run: uv run --group ha pytest -c pytest-ha.ini -q
|
|
58
|
+
|
|
59
|
+
hassfest:
|
|
60
|
+
name: Home Assistant manifest
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v7
|
|
64
|
+
- uses: home-assistant/actions/hassfest@master
|
|
65
|
+
|
|
66
|
+
version:
|
|
67
|
+
name: Version bump
|
|
68
|
+
# Shipped code under an unchanged version means Home Assistant and HACS
|
|
69
|
+
# keep serving the old one with nothing to indicate why.
|
|
70
|
+
if: github.event_name == 'pull_request'
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/checkout@v7
|
|
74
|
+
with:
|
|
75
|
+
fetch-depth: 0
|
|
76
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
77
|
+
- run: uv run python scripts/check_version_bump.py origin/${{ github.base_ref }}
|
|
78
|
+
|
|
79
|
+
hacs:
|
|
80
|
+
name: HACS validation
|
|
81
|
+
# HACS fetches hacs.json and manifest.json without authentication, which
|
|
82
|
+
# 404s on a private repository — the check reports them as invalid rather
|
|
83
|
+
# than unreachable. It runs as soon as the repository is public; skipping
|
|
84
|
+
# is honest, marking it continue-on-error would hide real failures later.
|
|
85
|
+
if: ${{ !github.event.repository.private }}
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v7
|
|
89
|
+
- uses: hacs/action@main
|
|
90
|
+
with:
|
|
91
|
+
category: integration
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: Claude review
|
|
2
|
+
|
|
3
|
+
# Reviews every pull request without needing a mention. Advisory: it comments,
|
|
4
|
+
# it does not gate. The gates are the CI checks in ci.yml.
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
types: [opened, synchronize]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: claude-review-${{ github.event.pull_request.number }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
review:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 20
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
pull-requests: write
|
|
20
|
+
id-token: write
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
# A missing optional credential should not paint the pull request red.
|
|
27
|
+
# `secrets` is not available in a job-level `if`, so the check happens
|
|
28
|
+
# here and the review step keys off it.
|
|
29
|
+
- id: credential
|
|
30
|
+
env:
|
|
31
|
+
TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
32
|
+
run: |
|
|
33
|
+
if [ -z "$TOKEN" ]; then
|
|
34
|
+
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
35
|
+
echo "::notice::No CLAUDE_CODE_OAUTH_TOKEN configured; skipping the review."
|
|
36
|
+
else
|
|
37
|
+
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- if: steps.credential.outputs.present == 'true'
|
|
41
|
+
uses: anthropics/claude-code-action@v1
|
|
42
|
+
with:
|
|
43
|
+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
44
|
+
prompt: |
|
|
45
|
+
Review this pull request against the project's own standards, which
|
|
46
|
+
are in CLAUDE.md and docs/specs/.
|
|
47
|
+
|
|
48
|
+
Weight these, in order:
|
|
49
|
+
|
|
50
|
+
1. Correctness against the specs. Does a changed behaviour still
|
|
51
|
+
match the requirement it cites, and was the spec updated when the
|
|
52
|
+
behaviour changed?
|
|
53
|
+
2. Tests that cannot fail. This project has shipped three assertions
|
|
54
|
+
that passed no matter what the implementation did. If a test looks
|
|
55
|
+
like it might be vacuous, say which mutation would leave it green.
|
|
56
|
+
3. The three portal traps in CLAUDE.md: interval timestamps mark the
|
|
57
|
+
END, the fine series is power in kW, only period=month gives local
|
|
58
|
+
midnight. Each is silent when wrong.
|
|
59
|
+
4. Account data. Nothing identifying a real meter, customer or tenant
|
|
60
|
+
may enter the repository.
|
|
61
|
+
5. Decimal where money or a running sum is involved.
|
|
62
|
+
|
|
63
|
+
Be specific and cite file:line. Do not restate the diff. If it is
|
|
64
|
+
sound, say so briefly rather than inventing findings.
|
|
65
|
+
claude_args: |
|
|
66
|
+
--max-turns 20
|
|
67
|
+
--model claude-opus-5
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Claude
|
|
2
|
+
|
|
3
|
+
# Responds to @claude in issues, pull requests and review comments.
|
|
4
|
+
# Requires the Claude GitHub App on the repository and an ANTHROPIC_API_KEY
|
|
5
|
+
# secret; see docs/AGENTIC-SDLC.md.
|
|
6
|
+
on:
|
|
7
|
+
issue_comment:
|
|
8
|
+
types: [created]
|
|
9
|
+
pull_request_review_comment:
|
|
10
|
+
types: [created]
|
|
11
|
+
pull_request_review:
|
|
12
|
+
types: [submitted]
|
|
13
|
+
issues:
|
|
14
|
+
types: [opened, assigned]
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: claude-${{ github.event.issue.number || github.event.pull_request.number }}
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
claude:
|
|
22
|
+
if: |
|
|
23
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
24
|
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
25
|
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
26
|
+
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 30
|
|
29
|
+
permissions:
|
|
30
|
+
contents: write
|
|
31
|
+
pull-requests: write
|
|
32
|
+
issues: write
|
|
33
|
+
id-token: write
|
|
34
|
+
steps:
|
|
35
|
+
# Checked out so CLAUDE.md, .claude/ and docs/specs/ are available —
|
|
36
|
+
# they are what keep the agent's changes consistent with the project.
|
|
37
|
+
- uses: actions/checkout@v7
|
|
38
|
+
with:
|
|
39
|
+
fetch-depth: 0
|
|
40
|
+
|
|
41
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
42
|
+
with:
|
|
43
|
+
enable-cache: true
|
|
44
|
+
- run: uv sync --group ha
|
|
45
|
+
|
|
46
|
+
# Without the credential the action fails with a validation error that
|
|
47
|
+
# says nothing about which repository setting is missing.
|
|
48
|
+
- id: credential
|
|
49
|
+
env:
|
|
50
|
+
TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
51
|
+
run: |
|
|
52
|
+
if [ -z "$TOKEN" ]; then
|
|
53
|
+
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
54
|
+
echo "::notice::No CLAUDE_CODE_OAUTH_TOKEN configured; @claude is inactive."
|
|
55
|
+
else
|
|
56
|
+
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
- if: steps.credential.outputs.present == 'true'
|
|
60
|
+
uses: anthropics/claude-code-action@v1
|
|
61
|
+
with:
|
|
62
|
+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
63
|
+
claude_args: |
|
|
64
|
+
--max-turns 30
|
|
65
|
+
--model claude-opus-5
|