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.
Files changed (84) hide show
  1. pyplusportal-0.1.0/.claude/hooks/format-python.sh +15 -0
  2. pyplusportal-0.1.0/.claude/hooks/gates-before-push.sh +33 -0
  3. pyplusportal-0.1.0/.claude/hooks/gates.sh +25 -0
  4. pyplusportal-0.1.0/.claude/hooks/no-account-data.sh +69 -0
  5. pyplusportal-0.1.0/.claude/hooks/no-direct-push-to-main.sh +39 -0
  6. pyplusportal-0.1.0/.claude/settings.json +35 -0
  7. pyplusportal-0.1.0/.claude/skills/release/SKILL.md +47 -0
  8. pyplusportal-0.1.0/.claude/skills/verify/SKILL.md +57 -0
  9. pyplusportal-0.1.0/.env.example +15 -0
  10. pyplusportal-0.1.0/.githooks/pre-push +20 -0
  11. pyplusportal-0.1.0/.github/rulesets/main.json +66 -0
  12. pyplusportal-0.1.0/.github/workflows/ci.yml +91 -0
  13. pyplusportal-0.1.0/.github/workflows/claude-review.yml +67 -0
  14. pyplusportal-0.1.0/.github/workflows/claude.yml +65 -0
  15. pyplusportal-0.1.0/.github/workflows/release.yml +63 -0
  16. pyplusportal-0.1.0/.gitignore +37 -0
  17. pyplusportal-0.1.0/.python-version +1 -0
  18. pyplusportal-0.1.0/CLAUDE.md +110 -0
  19. pyplusportal-0.1.0/LICENSE +21 -0
  20. pyplusportal-0.1.0/PKG-INFO +198 -0
  21. pyplusportal-0.1.0/README.md +177 -0
  22. pyplusportal-0.1.0/RELEASING.md +67 -0
  23. pyplusportal-0.1.0/custom_components/plusportal/__init__.py +39 -0
  24. pyplusportal-0.1.0/custom_components/plusportal/brand/icon.png +0 -0
  25. pyplusportal-0.1.0/custom_components/plusportal/brand/icon@2x.png +0 -0
  26. pyplusportal-0.1.0/custom_components/plusportal/config_flow.py +246 -0
  27. pyplusportal-0.1.0/custom_components/plusportal/const.py +35 -0
  28. pyplusportal-0.1.0/custom_components/plusportal/coordinator.py +174 -0
  29. pyplusportal-0.1.0/custom_components/plusportal/diagnostics.py +116 -0
  30. pyplusportal-0.1.0/custom_components/plusportal/manifest.json +19 -0
  31. pyplusportal-0.1.0/custom_components/plusportal/sensor.py +400 -0
  32. pyplusportal-0.1.0/custom_components/plusportal/statistics.py +185 -0
  33. pyplusportal-0.1.0/custom_components/plusportal/strings.json +98 -0
  34. pyplusportal-0.1.0/custom_components/plusportal/tariff.py +68 -0
  35. pyplusportal-0.1.0/custom_components/plusportal/translations/de.json +98 -0
  36. pyplusportal-0.1.0/custom_components/plusportal/translations/en.json +98 -0
  37. pyplusportal-0.1.0/docker-compose.dev.yml +23 -0
  38. pyplusportal-0.1.0/docs/AGENTIC-SDLC.md +85 -0
  39. pyplusportal-0.1.0/docs/specs/README.md +38 -0
  40. pyplusportal-0.1.0/docs/specs/cost.md +56 -0
  41. pyplusportal-0.1.0/docs/specs/extraction.md +131 -0
  42. pyplusportal-0.1.0/docs/specs/integration.md +154 -0
  43. pyplusportal-0.1.0/docs/specs/safety.md +35 -0
  44. pyplusportal-0.1.0/docs/superpowers/plans/2026-07-25-cli-help-plan.md +118 -0
  45. pyplusportal-0.1.0/docs/superpowers/specs/2026-07-25-plusportal-ha-integration-design.md +372 -0
  46. pyplusportal-0.1.0/hacs.json +4 -0
  47. pyplusportal-0.1.0/pyproject.toml +111 -0
  48. pyplusportal-0.1.0/pytest-ha.ini +13 -0
  49. pyplusportal-0.1.0/scripts/apply-ruleset.sh +29 -0
  50. pyplusportal-0.1.0/scripts/check_version_bump.py +109 -0
  51. pyplusportal-0.1.0/scripts/ha-dev.sh +152 -0
  52. pyplusportal-0.1.0/scripts/ha_prune.py +83 -0
  53. pyplusportal-0.1.0/scripts/ha_state.py +69 -0
  54. pyplusportal-0.1.0/src/pyplusportal/__init__.py +41 -0
  55. pyplusportal-0.1.0/src/pyplusportal/cli.py +728 -0
  56. pyplusportal-0.1.0/src/pyplusportal/client.py +436 -0
  57. pyplusportal-0.1.0/src/pyplusportal/const.py +53 -0
  58. pyplusportal-0.1.0/src/pyplusportal/cost.py +225 -0
  59. pyplusportal-0.1.0/src/pyplusportal/exceptions.py +28 -0
  60. pyplusportal-0.1.0/src/pyplusportal/json.py +41 -0
  61. pyplusportal-0.1.0/src/pyplusportal/models.py +421 -0
  62. pyplusportal-0.1.0/tests/__init__.py +1 -0
  63. pyplusportal-0.1.0/tests/conftest.py +62 -0
  64. pyplusportal-0.1.0/tests/fixtures/diagram_config.json +45 -0
  65. pyplusportal-0.1.0/tests/fixtures/diagram_result_july2026.json +608 -0
  66. pyplusportal-0.1.0/tests/fixtures/overview.json +40 -0
  67. pyplusportal-0.1.0/tests/fixtures/session.json +34 -0
  68. pyplusportal-0.1.0/tests/fixtures/user_item_list.json +192 -0
  69. pyplusportal-0.1.0/tests/test_cli.py +607 -0
  70. pyplusportal-0.1.0/tests/test_client.py +268 -0
  71. pyplusportal-0.1.0/tests/test_cost.py +332 -0
  72. pyplusportal-0.1.0/tests/test_intervals.py +246 -0
  73. pyplusportal-0.1.0/tests/test_json.py +58 -0
  74. pyplusportal-0.1.0/tests/test_live.py +173 -0
  75. pyplusportal-0.1.0/tests/test_models.py +283 -0
  76. pyplusportal-0.1.0/tests/test_packaging.py +124 -0
  77. pyplusportal-0.1.0/tests/test_readings.py +317 -0
  78. pyplusportal-0.1.0/tests/test_traceability.py +105 -0
  79. pyplusportal-0.1.0/tests_ha/__init__.py +1 -0
  80. pyplusportal-0.1.0/tests_ha/conftest.py +138 -0
  81. pyplusportal-0.1.0/tests_ha/test_config_flow.py +221 -0
  82. pyplusportal-0.1.0/tests_ha/test_sensor.py +552 -0
  83. pyplusportal-0.1.0/tests_ha/test_statistics.py +269 -0
  84. 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