diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 734788b..78615f4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "jpawlowski/hass.tibber_prices", - "image": "mcr.microsoft.com/devcontainers/python:3.14", + "image": "mcr.microsoft.com/devcontainers/base:debian", "postCreateCommand": "bash .devcontainer/setup-git.sh && scripts/setup/setup", "postStartCommand": "scripts/motd", "containerEnv": { @@ -63,9 +63,7 @@ "**/node_modules/**" ], "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.analysis.extraPaths": [ - "${workspaceFolder}/.venv/lib/python3.14/site-packages" - ], + "python.analysis.extraPaths": ["${workspaceFolder}/.venv/lib/python3.14/site-packages"], "python.terminal.activateEnvironment": true, "python.terminal.activateEnvInCurrentTerminal": true, "python.testing.pytestArgs": ["--no-cov"], @@ -142,6 +140,8 @@ }, "ghcr.io/devcontainers-extra/features/apt-packages:1": { "packages": [ + "autoconf", + "automake", "bat", "eza", "fd-find", @@ -154,9 +154,12 @@ "jo", "jq", "libpcap-dev", + "libssl-dev", + "libtool", "libturbojpeg0", "miller", "moreutils", + "pipx", "ripgrep", "shellcheck", "shfmt", diff --git a/.editorconfig b/.editorconfig index bf68552..8765191 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,7 @@ # top-most EditorConfig file root = true +# Default settings - AI-friendly baseline [*] charset = utf-8 end_of_line = lf @@ -9,12 +10,71 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 +# Python - Home Assistant & Ruff defaults (120 chars) [*.py] -# Python style aligns with Black indent_size = 4 +max_line_length = 120 +# YAML - Home Assistant configs, GitHub workflows +[*.{yaml,yml}] +indent_size = 2 + +# JSON - manifest.json, translations, etc. [*.json] indent_size = 2 +# Markdown - READMEs, docs (preserve AI formatting) [*.md] +indent_size = 2 trim_trailing_whitespace = false +max_line_length = off + +# TOML - pyproject.toml, Python packaging +[*.toml] +indent_size = 4 + +# Shell scripts - setup scripts, CI/CD +[*.{sh,bash}] +indent_size = 4 +end_of_line = lf + +# JavaScript/TypeScript - Frontend panel development +[*.{js,ts,jsx,tsx,mjs,cjs}] +indent_size = 2 + +# CSS/SCSS - Frontend styling +[*.{css,scss,sass}] +indent_size = 2 + +# HTML - Lovelace cards, frontend templates +[*.html] +indent_size = 2 + +# XML - Android Auto integration, etc. +[*.xml] +indent_size = 2 + +# Jinja2 templates - Home Assistant templates +[*.jinja,*.jinja2,*.j2] +indent_size = 2 + +# Makefiles require tabs +[Makefile] +indent_style = tab + +# GitHub-specific files +[.github/workflows/*.{yml,yaml}] +indent_size = 2 + +[.github/dependabot.{yml,yaml}] +indent_size = 2 + +# Docker files +[Dockerfile*] +indent_size = 2 + +[*.dockerignore] +indent_size = 2 + +[docker-compose*.{yml,yaml}] +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 249c3ba..9dafbd6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ # These are supported funding model platforms -github: [ jpawlowski ] +github: [jpawlowski] buy_me_a_coffee: jpawlowski diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 1a87995..240188f 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -3,69 +3,69 @@ name: "Bug report" description: "Report a bug with the custom integration" labels: ["bug"] body: -- type: markdown - attributes: - value: Before you open a new issue, search through the existing issues to see if others have had the same problem. -- type: input - attributes: - label: "Home Assistant version" - description: "The version of Home Assistant you are using" - placeholder: "2025.1.0" - validations: - required: true -- type: input - attributes: - label: "Integration version" - description: "The version of this custom integration you are using" - placeholder: "1.0.0" - validations: - required: false -- type: textarea - attributes: - label: "System Health details" - description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)" - validations: - required: false -- type: checkboxes - attributes: - label: Checklist - options: - - label: I have enabled debug logging for my installation. - required: true - - label: I have filled out the issue template to the best of my ability. - required: true - - label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue). - required: true - - label: This issue is not a duplicate issue of any [previous issues](https://github.com/jpawlowski/hass.tibber_prices/issues?q=is%3Aissue+label%3A%22Bug%22+).. - required: true -- type: textarea - attributes: - label: "Describe the issue" - description: "A clear and concise description of what the issue is." - validations: - required: true -- type: textarea - attributes: - label: Reproduction steps - description: "Without steps to reproduce, it will be hard to fix. It is very important that you fill out this part. Issues without it will be closed." - value: | - 1. - 2. - 3. - ... - validations: - required: true -- type: textarea - attributes: - label: "Debug logs" - description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue." - render: text - validations: - required: true + - type: markdown + attributes: + value: Before you open a new issue, search through the existing issues to see if others have had the same problem. + - type: input + attributes: + label: "Home Assistant version" + description: "The version of Home Assistant you are using" + placeholder: "2025.1.0" + validations: + required: true + - type: input + attributes: + label: "Integration version" + description: "The version of this custom integration you are using" + placeholder: "1.0.0" + validations: + required: false + - type: textarea + attributes: + label: "System Health details" + description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)" + validations: + required: false + - type: checkboxes + attributes: + label: Checklist + options: + - label: I have enabled debug logging for my installation. + required: true + - label: I have filled out the issue template to the best of my ability. + required: true + - label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue). + required: true + - label: This issue is not a duplicate issue of any [previous issues](https://github.com/jpawlowski/hass.tibber_prices/issues?q=is%3Aissue+label%3A%22Bug%22+).. + required: true + - type: textarea + attributes: + label: "Describe the issue" + description: "A clear and concise description of what the issue is." + validations: + required: true + - type: textarea + attributes: + label: Reproduction steps + description: "Without steps to reproduce, it will be hard to fix. It is very important that you fill out this part. Issues without it will be closed." + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: textarea + attributes: + label: "Debug logs" + description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue." + render: text + validations: + required: true -- type: textarea - attributes: - label: "Diagnostics dump" - description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)" - validations: - required: false + - type: textarea + attributes: + label: "Diagnostics dump" + description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)" + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ec4bb38..3ba13e0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1 @@ -blank_issues_enabled: false \ No newline at end of file +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 8ca0302..af70fc9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -3,45 +3,45 @@ name: "Feature request" description: "Suggest an idea for this custom integration" labels: ["Feature request"] body: -- type: markdown - attributes: - value: Before you open a new feature request, search through the existing feature requests to see if others have had the same idea. -- type: checkboxes - attributes: - label: Checklist - options: - - label: I have filled out the template to the best of my ability. - required: true - - label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request). - required: true - - label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/jpawlowski/hass.tibber_prices/issues?q=is%3Aissue+label%3A%22Feature+Request%22+). - required: true + - type: markdown + attributes: + value: Before you open a new feature request, search through the existing feature requests to see if others have had the same idea. + - type: checkboxes + attributes: + label: Checklist + options: + - label: I have filled out the template to the best of my ability. + required: true + - label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request). + required: true + - label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/jpawlowski/hass.tibber_prices/issues?q=is%3Aissue+label%3A%22Feature+Request%22+). + required: true -- type: textarea - attributes: - label: "Is your feature request related to a problem? Please describe." - description: "A clear and concise description of what the problem is." - placeholder: "I'm always frustrated when [...]" - validations: - required: true + - type: textarea + attributes: + label: "Is your feature request related to a problem? Please describe." + description: "A clear and concise description of what the problem is." + placeholder: "I'm always frustrated when [...]" + validations: + required: true -- type: textarea - attributes: - label: "Describe the solution you'd like" - description: "A clear and concise description of what you want to happen." - validations: - required: true + - type: textarea + attributes: + label: "Describe the solution you'd like" + description: "A clear and concise description of what you want to happen." + validations: + required: true -- type: textarea - attributes: - label: "Describe alternatives you've considered" - description: "A clear and concise description of any alternative solutions or features you've considered." - validations: - required: true + - type: textarea + attributes: + label: "Describe alternatives you've considered" + description: "A clear and concise description of any alternative solutions or features you've considered." + validations: + required: true -- type: textarea - attributes: - label: "Additional context" - description: "Add any other context or screenshots about the feature request here." - validations: - required: true + - type: textarea + attributes: + label: "Additional context" + description: "Add any other context or screenshots about the feature request here." + validations: + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bc7c9bd..9dfc07e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,3 +44,6 @@ updates: ignore: # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json - dependency-name: "homeassistant" + # pytest version is dictated by pytest-homeassistant-custom-component (pins an exact version) + # Bumping pytest independently breaks the dependency resolution + - dependency-name: "pytest" diff --git a/.github/instructions/commit-messages.instructions.md b/.github/instructions/commit-messages.instructions.md index 9e5ad3c..24f2433 100644 --- a/.github/instructions/commit-messages.instructions.md +++ b/.github/instructions/commit-messages.instructions.md @@ -41,9 +41,9 @@ In that case: - Prefer chore(...) or refactor(...) instead of fix(...), and/or - Add an explicit trailer in the commit body: - - Release-Notes: skip - - User-Impact: none - - Released-Bug: no + - Release-Notes: skip + - User-Impact: none + - Released-Bug: no Any one of these trailers is enough. diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 3722349..0f2f1ce 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -5,7 +5,7 @@ on: branches: - main paths: - - 'custom_components/tibber_prices/manifest.json' + - "custom_components/tibber_prices/manifest.json" permissions: contents: write @@ -22,7 +22,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - fetch-depth: 0 # Need full history for git describe + fetch-depth: 0 # Need full history for git describe - name: Extract version from manifest.json id: manifest diff --git a/.github/workflows/docusaurus.yml b/.github/workflows/docusaurus.yml index dff60a4..45f57a8 100644 --- a/.github/workflows/docusaurus.yml +++ b/.github/workflows/docusaurus.yml @@ -4,10 +4,10 @@ on: push: branches: [main] paths: - - 'docs/**' - - '.github/workflows/docusaurus.yml' + - "docs/**" + - ".github/workflows/docusaurus.yml" tags: - - 'v*.*.*' + - "v*.*.*" workflow_dispatch: # Concurrency control: cancel in-progress deployments @@ -31,7 +31,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - fetch-depth: 0 # Needed for version timestamps + fetch-depth: 0 # Needed for version timestamps - name: Detect prerelease tag (beta/rc) id: taginfo @@ -47,7 +47,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: 24 - cache: 'npm' + cache: "npm" cache-dependency-path: | docs/user/package-lock.json docs/developer/package-lock.json @@ -56,7 +56,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.14' + python-version: "3.14" - name: Verify sensor reference is up-to-date run: python3 scripts/docs/generate-sensor-reference --check diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 41fb717..fc1c792 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,14 +5,14 @@ on: branches: - "main" paths-ignore: - - 'docs/**' - - '.github/workflows/docusaurus.yml' + - "docs/**" + - ".github/workflows/docusaurus.yml" pull_request: branches: - "main" paths-ignore: - - 'docs/**' - - '.github/workflows/docusaurus.yml' + - "docs/**" + - ".github/workflows/docusaurus.yml" permissions: {} @@ -34,7 +34,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: version: "0.9.3" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4fe8406..b8bff69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,16 +3,16 @@ name: Generate Release Notes on: push: tags: - - 'v*.*.*' # Triggers on version tags like v1.0.0, v2.1.3, etc. + - "v*.*.*" # Triggers on version tags like v1.0.0, v2.1.3, etc. workflow_dispatch: inputs: tag: - description: 'Tag version to release (e.g., v0.3.0)' + description: "Tag version to release (e.g., v0.3.0)" required: true type: string permissions: - contents: write # Needed to create/update releases and push commits + contents: write # Needed to create/update releases and push commits jobs: # Note: We trust that validate.yml and lint.yml have already run on the @@ -103,13 +103,13 @@ jobs: release-notes: name: Generate and publish release notes runs-on: ubuntu-latest - needs: sync-manifest # Wait for manifest sync to complete + needs: sync-manifest # Wait for manifest sync to complete steps: - name: Checkout repository uses: actions/checkout@v6 with: - fetch-depth: 0 # Fetch all history for git-cliff - ref: main # Use updated main branch if manifest was synced + fetch-depth: 0 # Fetch all history for git-cliff + ref: main # Use updated main branch if manifest was synced - name: Get previous tag id: previoustag @@ -261,7 +261,7 @@ jobs: body: ${{ steps.release_notes.outputs.notes }} draft: false prerelease: ${{ contains(github.ref, 'b') }} - generate_release_notes: false # We provide our own + generate_release_notes: false # We provide our own env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index db12799..b588c96 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,14 +8,14 @@ on: branches: - main paths-ignore: - - 'docs/**' - - '.github/workflows/docusaurus.yml' + - "docs/**" + - ".github/workflows/docusaurus.yml" pull_request: branches: - main paths-ignore: - - 'docs/**' - - '.github/workflows/docusaurus.yml' + - "docs/**" + - ".github/workflows/docusaurus.yml" permissions: {} diff --git a/.markdownlint.json b/.markdownlint.json index efe7ed5..9216007 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,9 +1,20 @@ { "default": true, + "MD004": false, "MD013": false, - "MD033": false, + "MD036": false, "MD041": false, - "no-inline-html": false, - "line-length": false, - "first-line-heading": false + "no-trailing-punctuation": false, + "no-inline-html": { + "allowed_elements": ["br", "details", "summary", "img", "a", "kbd"] + }, + "code-block-style": { + "style": "fenced" + }, + "emphasis-style": { + "style": "underscore" + }, + "strong-style": { + "style": "asterisk" + } } diff --git a/.prettierignore b/.prettierignore index dd50e39..7209efb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,6 +11,12 @@ __pycache__/ env/ venv/ -# Ignore compiled YAML or generated docs -*.yaml -*.yml +# Ignore local HA dev instance config (not production code) +config/ + +# Ignore YAML schemas (structural files with specific formatting conventions) +schemas/yaml/ + +# Ignore Docusaurus documentation sites – they have their own toolchain +# and Prettier reformats
blocks inside lists in a way that breaks MDX +docs/ diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..86ffa46 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,37 @@ +# Prettier configuration for Home Assistant Custom Component Development +# Aligned with .editorconfig and .markdownlint.json + +printWidth: 120 +tabWidth: 2 +useTabs: false +semi: true +singleQuote: false +quoteProps: "as-needed" +trailingComma: "es5" +bracketSpacing: true +arrowParens: "always" +proseWrap: "preserve" +endOfLine: "lf" + +# File-specific overrides +overrides: + # Markdown - preserve formatting, avoid conflicts with markdownlint + - files: "*.md" + options: + proseWrap: "preserve" + printWidth: 120 + trailingComma: "none" + + # JSON - Home Assistant manifest, translations + - files: "*.json" + options: + tabWidth: 2 + trailingComma: "none" + + # JSONC - VS Code settings, devcontainer config + - files: "*.jsonc" + options: + tabWidth: 2 + trailingComma: "none" + + # YAML would go here, but it's in .prettierignore (handled by redhat.vscode-yaml) diff --git a/AGENTS.md b/AGENTS.md index 2126d77..d030f79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,11 +23,11 @@ When working with the codebase, Copilot MUST actively maintain consistency betwe - **This file** (`AGENTS.md`): AI/Developer long-term memory, patterns, conventions - **`docs/user/`**: Docusaurus site for end-users (installation, configuration, usage examples) - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` - **`docs/developer/`**: Docusaurus site for contributors (architecture, development guides) - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` - **`README.md`**: Project overview with links to documentation sites **Automatic Inconsistency Detection:** @@ -49,17 +49,17 @@ When working with the codebase, Copilot MUST actively maintain consistency betwe **When to discuss in chat vs. direct file changes:** - **Make direct changes when:** - - Clear, straightforward task (fix bug, add function, update config) - - Single approach is obvious - - User request is specific ("add X", "change Y to Z") - - Quick iteration is needed (user can review diff and iterate) + - Clear, straightforward task (fix bug, add function, update config) + - Single approach is obvious + - User request is specific ("add X", "change Y to Z") + - Quick iteration is needed (user can review diff and iterate) - **Discuss/show in chat first when:** - - Multiple valid approaches exist (architectural decision) - - Significant refactoring affecting many files - - Unclear requirements need clarification - - Trade-offs need discussion (performance vs. readability, etc.) - - User asks open-ended question ("how should we...", "what's the best way...") + - Multiple valid approaches exist (architectural decision) + - Significant refactoring affecting many files + - Unclear requirements need clarification + - Trade-offs need discussion (performance vs. readability, etc.) + - User asks open-ended question ("how should we...", "what's the best way...") **Goal:** Save time. File edits with VS Code tracking are fast for simple changes. Chat discussion is better for decisions requiring input before committing to an approach. @@ -204,30 +204,30 @@ Skip planning for: **Planning Document Lifecycle:** 1. **Planning Phase** (WIP in `/planning/`) - - Create `planning/-refactoring-plan.md` - - Iterate freely (git-ignored, no commit pressure) - - AI can help refine without polluting git history - - Multiple revisions until plan is solid + - Create `planning/-refactoring-plan.md` + - Iterate freely (git-ignored, no commit pressure) + - AI can help refine without polluting git history + - Multiple revisions until plan is solid 2. **Implementation Phase** (Active work) - - Use plan as reference during coding - - Update plan if issues discovered - - Track progress through phases - - Test after each phase + - Use plan as reference during coding + - Update plan if issues discovered + - Track progress through phases + - Test after each phase 3. **Completion Phase** (After implementation) - - **Option A**: Move to `docs/development/` if lasting value - - Example: `planning/module-splitting-plan.md` → `docs/development/module-splitting-plan.md` - - Update status to "✅ COMPLETED" - - Commit as historical reference + - **Option A**: Move to `docs/development/` if lasting value + - Example: `planning/module-splitting-plan.md` → `docs/development/module-splitting-plan.md` + - Update status to "✅ COMPLETED" + - Commit as historical reference - - **Option B**: Delete if superseded - - Plan served its purpose - - Code and AGENTS.md are source of truth + - **Option B**: Delete if superseded + - Plan served its purpose + - Code and AGENTS.md are source of truth - - **Option C**: Archive in `planning/archive/` - - Keep locally for "why we didn't do X" reference - - Don't commit (git-ignored) + - **Option C**: Archive in `planning/archive/` + - Keep locally for "why we didn't do X" reference + - Don't commit (git-ignored) **Required Planning Document Sections:** @@ -320,46 +320,46 @@ After successful refactoring: **Organized Packages:** 1. **`/utils/`** - Pure data transformation functions (stateless) - - `average.py` - Average and time-window calculations - - `price.py` - Price enrichment, volatility, rating calculations - - **Pattern**: Import as `from ..utils.average import function_name` + - `average.py` - Average and time-window calculations + - `price.py` - Price enrichment, volatility, rating calculations + - **Pattern**: Import as `from ..utils.average import function_name` 2. **`/entity_utils/`** - Entity-specific utilities - - `icons.py` - Dynamic icon selection logic - - `colors.py` - Icon color mapping - - `attributes.py` - Common attribute builders - - **Pattern**: Import as `from ..entity_utils import function_name` + - `icons.py` - Dynamic icon selection logic + - `colors.py` - Icon color mapping + - `attributes.py` - Common attribute builders + - **Pattern**: Import as `from ..entity_utils import function_name` 3. **`/coordinator/`** - DataUpdateCoordinator and related logic - - `core.py` - Main coordinator class - - `cache.py` - Persistent storage handling - - `data_transformation.py` - Raw data → enriched data - - `period_handlers/` - Period calculation sub-package - - **Pattern**: Coordinator-specific implementations + - `core.py` - Main coordinator class + - `cache.py` - Persistent storage handling + - `data_transformation.py` - Raw data → enriched data + - `period_handlers/` - Period calculation sub-package + - **Pattern**: Coordinator-specific implementations 4. **`/sensor/`** - Sensor platform package - - `core.py` - Entity class (1,268 lines - manages 80+ sensor types) - - `definitions.py` - Entity descriptions - - `helpers.py` - Sensor-specific helpers - - `calculators/` - Value calculation package (8 specialized calculators, 1,838 lines) - - `attributes/` - Attribute builders package (8 specialized modules, 1,209 lines) - - **Pattern**: Calculator Pattern (business logic separated from presentation) - - **Architecture**: Two-tier (Calculators handle computation → Attributes handle state presentation) + - `core.py` - Entity class (1,268 lines - manages 80+ sensor types) + - `definitions.py` - Entity descriptions + - `helpers.py` - Sensor-specific helpers + - `calculators/` - Value calculation package (8 specialized calculators, 1,838 lines) + - `attributes/` - Attribute builders package (8 specialized modules, 1,209 lines) + - **Pattern**: Calculator Pattern (business logic separated from presentation) + - **Architecture**: Two-tier (Calculators handle computation → Attributes handle state presentation) 5. **`/binary_sensor/`** - Binary sensor platform package - - Same structure as `/sensor/` + - Same structure as `/sensor/` 6. **`/config_flow_handlers/`** - Configuration flow package - - `user_flow.py` - Initial setup flow - - `subentry_flow.py` - Add additional homes - - `options_flow.py` - Reconfiguration - - `schemas.py` - Form schemas - - `validators.py` - Input validation + - `user_flow.py` - Initial setup flow + - `subentry_flow.py` - Add additional homes + - `options_flow.py` - Reconfiguration + - `schemas.py` - Form schemas + - `validators.py` - Input validation 7. **`/api/`** - External API communication - - `client.py` - GraphQL client - - `queries.py` - Query definitions - - `exceptions.py` - API-specific exceptions + - `client.py` - GraphQL client + - `queries.py` - Query definitions + - `exceptions.py` - API-specific exceptions **When Adding New Files:** @@ -386,74 +386,74 @@ After successful refactoring: **Key Patterns:** - **Dual translation system**: Standard HA translations in `/translations/` (config flow, UI strings per HA schema), supplemental in `/custom_translations/` (entity descriptions not supported by HA schema). Both must stay in sync. Use `async_load_translations()` and `async_load_standard_translations()` from `const.py`. When to use which: `/translations/` is bound to official HA schema requirements; anything else goes in `/custom_translations/` (requires manual translation loading). **Schema reference**: `/schemas/json/translation_schema.json` provides the structure for `/translations/*.json` files based on [HA's translation documentation](https://developers.home-assistant.io/docs/internationalization/core). - - **Select selector translations**: Use `selector.{translation_key}.options.{value}` structure (NOT `selector.select.{translation_key}`). Translation keys map to JSON in `/translations/*.json` following the HA schema structure. + - **Select selector translations**: Use `selector.{translation_key}.options.{value}` structure (NOT `selector.select.{translation_key}`). Translation keys map to JSON in `/translations/*.json` following the HA schema structure. - **CRITICAL Rules:** - - When using `translation_key`, pass options as **plain string list**, NOT `SelectOptionDict` - - Selector option keys MUST be lowercase: `[a-z0-9-_]+` pattern (Hassfest validation) - - Label parameter overrides translations (avoid when using translation_key) - - Use `SelectOptionDict` ONLY for dynamic/non-translatable options (no translation_key) + **CRITICAL Rules:** + - When using `translation_key`, pass options as **plain string list**, NOT `SelectOptionDict` + - Selector option keys MUST be lowercase: `[a-z0-9-_]+` pattern (Hassfest validation) + - Label parameter overrides translations (avoid when using translation_key) + - Use `SelectOptionDict` ONLY for dynamic/non-translatable options (no translation_key) - See `config_flow/schemas.py` for implementation examples. + See `config_flow/schemas.py` for implementation examples. - **Price data enrichment**: All quarter-hourly price intervals get augmented with `trailing_avg_24h`, `difference`, and `rating_level` fields via `enrich_price_info_with_differences()` in `utils/price.py`. This adds statistical analysis (24h trailing average, percentage difference from average, rating classification) to each 15-minute interval. See `utils/price.py` for enrichment logic. - **Sensor organization (refactored Nov 2025)**: The `sensor/` package uses **Calculator Pattern** for separation of concerns: - - **Calculator Package** (`sensor/calculators/`): 8 specialized calculators handle business logic (1,838 lines total) - - `base.py` - Abstract BaseCalculator with coordinator access - - `interval.py` - Single interval calculations (current/next/previous) - - `rolling_hour.py` - 5-interval rolling windows - - `daily_stat.py` - Calendar day min/max/avg statistics - - `window_24h.py` - Trailing/leading 24h windows - - `volatility.py` - Price volatility analysis - - `trend.py` - Complex trend analysis with caching (640 lines) - - `timing.py` - Best/peak price period timing - - `metadata.py` - Home/metering metadata - - **Attributes Package** (`sensor/attributes/`): 8 specialized modules handle state presentation (1,209 lines total) - - Modules match calculator types: `interval.py`, `daily_stat.py`, `window_24h.py`, `volatility.py`, `trend.py`, `timing.py`, `future.py`, `metadata.py` - - `__init__.py` - Routing logic + unified builders (`build_sensor_attributes`, `build_extra_state_attributes`) - - **Core Entity** (`sensor/core.py`): 1,268 lines managing 80+ sensor types - - Instantiates all calculators in `__init__` - - Delegates value calculations to appropriate calculator - - Uses unified handler methods: `_get_interval_value()`, `_get_rolling_hour_value()`, `_get_daily_stat_value()`, `_get_24h_window_value()` - - Handler mapping dictionary routes entity keys to value getters - - **Architecture Benefits**: 42% line reduction in core.py (2,170 → 1,268 lines), clear separation of concerns, improved testability, reusable components - - **See "Common Tasks" section** for detailed patterns and examples + - **Calculator Package** (`sensor/calculators/`): 8 specialized calculators handle business logic (1,838 lines total) + - `base.py` - Abstract BaseCalculator with coordinator access + - `interval.py` - Single interval calculations (current/next/previous) + - `rolling_hour.py` - 5-interval rolling windows + - `daily_stat.py` - Calendar day min/max/avg statistics + - `window_24h.py` - Trailing/leading 24h windows + - `volatility.py` - Price volatility analysis + - `trend.py` - Complex trend analysis with caching (640 lines) + - `timing.py` - Best/peak price period timing + - `metadata.py` - Home/metering metadata + - **Attributes Package** (`sensor/attributes/`): 8 specialized modules handle state presentation (1,209 lines total) + - Modules match calculator types: `interval.py`, `daily_stat.py`, `window_24h.py`, `volatility.py`, `trend.py`, `timing.py`, `future.py`, `metadata.py` + - `__init__.py` - Routing logic + unified builders (`build_sensor_attributes`, `build_extra_state_attributes`) + - **Core Entity** (`sensor/core.py`): 1,268 lines managing 80+ sensor types + - Instantiates all calculators in `__init__` + - Delegates value calculations to appropriate calculator + - Uses unified handler methods: `_get_interval_value()`, `_get_rolling_hour_value()`, `_get_daily_stat_value()`, `_get_24h_window_value()` + - Handler mapping dictionary routes entity keys to value getters + - **Architecture Benefits**: 42% line reduction in core.py (2,170 → 1,268 lines), clear separation of concerns, improved testability, reusable components + - **See "Common Tasks" section** for detailed patterns and examples - **Quarter-hour precision**: Entities update on 00/15/30/45-minute boundaries via `schedule_quarter_hour_refresh()` in `coordinator/listeners.py`, not just on data fetch intervals. Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` for absolute-time scheduling. Smart boundary tolerance (±2 seconds) in `sensor/helpers.py` → `round_to_nearest_quarter_hour()` handles HA scheduling jitter: if HA triggers at 14:59:58 → rounds to 15:00:00 (next interval), if HA restarts at 14:59:30 → stays at 14:45:00 (current interval). This ensures current price sensors update without waiting for the next API poll, while preventing premature data display during normal operation. - **Currency handling**: Multi-currency support with base/sub units (e.g., EUR/ct, NOK/øre) via `get_currency_info()` and `format_price_unit_*()` in `const.py`. - **Intelligent caching strategy**: Minimizes API calls while ensuring data freshness: - - User data cached for 24h (rarely changes) - - Price data validated against calendar day - cleared on midnight turnover to force fresh fetch - - Cache survives HA restarts via `Store` persistence - - API polling intensifies only when tomorrow's data expected (afternoons) - - Stale cache detection via `_is_cache_valid()` prevents using yesterday's data as today's + - User data cached for 24h (rarely changes) + - Price data validated against calendar day - cleared on midnight turnover to force fresh fetch + - Cache survives HA restarts via `Store` persistence + - API polling intensifies only when tomorrow's data expected (afternoons) + - Stale cache detection via `_is_cache_valid()` prevents using yesterday's data as today's **Multi-Layer Caching (Performance Optimization)**: The integration uses **4 distinct caching layers** with automatic invalidation: 1. **Persistent API Cache** (`coordinator/cache.py` → HA Storage): - - **What**: Raw price/user data from Tibber API (~50KB) - - **Lifetime**: Until midnight (price) or 24h (user) - - **Invalidation**: Automatic at 00:00 local, cache validation on load - - **Why**: Reduce API calls from every 15min to once per day, survive HA restarts + - **What**: Raw price/user data from Tibber API (~50KB) + - **Lifetime**: Until midnight (price) or 24h (user) + - **Invalidation**: Automatic at 00:00 local, cache validation on load + - **Why**: Reduce API calls from every 15min to once per day, survive HA restarts 2. **Translation Cache** (`const.py` → in-memory dicts): - - **What**: UI strings, entity descriptions (~5KB) - - **Lifetime**: Forever (until HA restart) - - **Invalidation**: Never (read-only after startup load) - - **Why**: Avoid file I/O on every entity attribute access (15+ times/hour) + - **What**: UI strings, entity descriptions (~5KB) + - **Lifetime**: Forever (until HA restart) + - **Invalidation**: Never (read-only after startup load) + - **Why**: Avoid file I/O on every entity attribute access (15+ times/hour) 3. **Config Dictionary Cache** (`coordinator/` modules): - - **What**: Parsed options dict (~1KB per module) - - **Lifetime**: Until `config_entry.options` change - - **Invalidation**: Explicit via `invalidate_config_cache()` on options update - - **Why**: Avoid ~30-40 `options.get()` calls per coordinator update (98% time saving) + - **What**: Parsed options dict (~1KB per module) + - **Lifetime**: Until `config_entry.options` change + - **Invalidation**: Explicit via `invalidate_config_cache()` on options update + - **Why**: Avoid ~30-40 `options.get()` calls per coordinator update (98% time saving) 4. **Period Calculation Cache** (`coordinator/periods.py`): - - **What**: Calculated best/peak price periods (~10KB) - - **Lifetime**: Until price data or config changes - - **Invalidation**: Automatic via hash comparison of inputs (timestamps + rating_levels + config) - - **Why**: Avoid expensive calculation (~100-500ms) when data unchanged (70% CPU saving) + - **What**: Calculated best/peak price periods (~10KB) + - **Lifetime**: Until price data or config changes + - **Invalidation**: Automatic via hash comparison of inputs (timestamps + rating_levels + config) + - **Why**: Avoid expensive calculation (~100-500ms) when data unchanged (70% CPU saving) **Cache Invalidation Coordination**: @@ -565,8 +565,8 @@ sensor/helpers/ ✗ (NO imports from calculators/) 1. **Calculator** computes value AND builds attribute dict 2. **Core** stores attributes in `cached_data` dict 3. **Attributes package** retrieves cached attributes via: - - `_add_cached_trend_attributes()` for trend sensors - - `_add_timing_or_volatility_attributes()` for volatility sensors + - `_add_cached_trend_attributes()` for trend sensors + - `_add_timing_or_volatility_attributes()` for volatility sensors **Example (Volatility):** @@ -671,25 +671,25 @@ Example: daily_min=10 ct, daily_avg=20 ct, flex=50%, min_distance=5% **Solutions Implemented (Nov 2025):** 1. **Hard Caps on Flex** (`coordinator/period_handlers/core.py`): - - `MAX_SAFE_FLEX = 0.50` (50% overall maximum) - - `MAX_OUTLIER_FLEX = 0.25` (25% for price spike detection) - - Warns users when base flex exceeds thresholds (INFO at 25%, WARNING at 30%) + - `MAX_SAFE_FLEX = 0.50` (50% overall maximum) + - `MAX_OUTLIER_FLEX = 0.25` (25% for price spike detection) + - Warns users when base flex exceeds thresholds (INFO at 25%, WARNING at 30%) 2. **Relaxation Increment Cap** (`coordinator/period_handlers/relaxation.py`): - - Maximum 3% increment per relaxation step (prevents explosion from high base flex) - - Example: Base flex 40% → increments as 43%, 46%, 49% (capped at 50%) - - Without cap: 40% × 1.25 = 50% step → reaches 100% in 6 steps + - Maximum 3% increment per relaxation step (prevents explosion from high base flex) + - Example: Base flex 40% → increments as 43%, 46%, 49% (capped at 50%) + - Without cap: 40% × 1.25 = 50% step → reaches 100% in 6 steps 3. **Dynamic Min_Distance Scaling** (`coordinator/period_handlers/level_filtering.py`): - - Reduces min_distance proportionally as flex increases above 20% - - Formula: `scale_factor = max(0.25, 1.0 - ((flex - 0.20) × 2.5))` - - Example: flex=30% → scale=0.75 → min_distance reduced by 25% - - Minimum scaling: 25% of original (prevents complete removal) + - Reduces min_distance proportionally as flex increases above 20% + - Formula: `scale_factor = max(0.25, 1.0 - ((flex - 0.20) × 2.5))` + - Example: flex=30% → scale=0.75 → min_distance reduced by 25% + - Minimum scaling: 25% of original (prevents complete removal) 4. **Enhanced Debug Logging** (`coordinator/period_handlers/period_building.py`): - - Tracks exact counts of intervals filtered by flex, min_distance, and level - - Shows which filter blocked the most candidates - - Enables diagnosis of configuration issues + - Tracks exact counts of intervals filtered by flex, min_distance, and level + - Shows which filter blocked the most candidates + - Enables diagnosis of configuration issues **Configuration Guidance:** @@ -777,15 +777,15 @@ When debugging period calculation issues: - Uses `uv` (modern, fast Python package manager) - **Always use `uv` commands**, not `pip` directly: - ```bash - # ✅ Correct - uv pip install - uv run pytest + ```bash + # ✅ Correct + uv pip install + uv run pytest - # ❌ Wrong - uses system Python - pip install - python -m pytest - ``` + # ❌ Wrong - uses system Python + pip install + python -m pytest + ``` **Development Scripts:** @@ -812,15 +812,15 @@ Some commands are available via compatibility aliases because Debian package nam 1. **Prefer development scripts** (they handle .venv automatically) 2. **If using Python directly**, use `.venv/bin/python` explicitly: - ```bash - .venv/bin/python -m pytest tests/ - .venv/bin/python -c "import homeassistant; print('OK')" - ``` + ```bash + .venv/bin/python -m pytest tests/ + .venv/bin/python -c "import homeassistant; print('OK')" + ``` 3. **For package management**, always use `uv`: - ```bash - uv pip list - uv pip install --upgrade homeassistant - ``` + ```bash + uv pip list + uv pip install --upgrade homeassistant + ``` **Debugging Environment Issues:** @@ -837,9 +837,9 @@ If you notice commands failing or missing dependencies: 1. **Check if running in DevContainer**: Ask user to run `ls /.dockerenv && echo "In container" || echo "NOT in container"` 2. **If NOT in container**: Suggest opening project in DevContainer (VS Code: "Reopen in Container") 3. **Why it matters**: - - `.venv` is located outside workspace (hardlink issues on bind-mounts) - - Development scripts expect container environment - - VS Code Python settings are container-specific + - `.venv` is located outside workspace (hardlink issues on bind-mounts) + - Development scripts expect container environment + - VS Code Python settings are container-specific **If user insists on local development without container**, warn that: @@ -931,10 +931,10 @@ When changes are complete and ready for testing: 1. **Ask user to test**, don't execute `./scripts/develop` yourself 2. **Provide specific test guidance** based on what changed in this session: - - Which UI screens to check (e.g., "Open config flow, step 3") - - What behavior to verify (e.g., "Dropdown should show translated values") - - What errors to watch for (e.g., "Check logs for JSON parsing errors") - - What NOT to test (if change is isolated, no need to test everything) + - Which UI screens to check (e.g., "Open config flow, step 3") + - What behavior to verify (e.g., "Dropdown should show translated values") + - What errors to watch for (e.g., "Check logs for JSON parsing errors") + - What NOT to test (if change is isolated, no need to test everything) 3. **Keep test guidance concise** - 3-5 bullet points max 4. **Focus on session changes only** - Don't suggest testing unrelated features @@ -976,9 +976,9 @@ When changes are complete and ready for testing: **Internal/unreleased fixes:** - If a fix never affected released users, mark commit body with one trailer so release notes can exclude it: - - `Release-Notes: skip` - - `User-Impact: none` - - `Released-Bug: no` + - `Release-Notes: skip` + - `User-Impact: none` + - `Released-Bug: no` - To check if introducing code was released, use: `./scripts/release/check-if-released ` ### Release Notes Generation @@ -986,54 +986,54 @@ When changes are complete and ready for testing: **Multiple Options Available:** 1. **Helper Script** (recommended, foolproof) - - Script: `./scripts/release/prepare VERSION` - - Bumps manifest.json version → commits → creates tag locally - - You review and push when ready - - Example: `./scripts/release/prepare 0.3.0` + - Script: `./scripts/release/prepare VERSION` + - Bumps manifest.json version → commits → creates tag locally + - You review and push when ready + - Example: `./scripts/release/prepare 0.3.0` 2. **Auto-Tag Workflow** (safety net) - - Workflow: `.github/workflows/auto-tag.yml` - - Triggers on manifest.json changes - - Automatically creates tag if it doesn't exist - - Prevents "forgot to tag" mistakes + - Workflow: `.github/workflows/auto-tag.yml` + - Triggers on manifest.json changes + - Automatically creates tag if it doesn't exist + - Prevents "forgot to tag" mistakes 3. **Local Script** (testing, preview, and updating releases) - - Script: `./scripts/release/generate-notes [FROM_TAG] [TO_TAG]` - - Parses Conventional Commits between tags - - Supports multiple backends (auto-detected): - - **AI-powered**: GitHub Copilot CLI (best, context-aware) - - **Template-based**: git-cliff (fast, reliable) - - **Manual**: grep/awk fallback (always works) - - **Auto-update feature**: If a GitHub release exists for TO_TAG, automatically offers to update release notes (interactive prompt) + - Script: `./scripts/release/generate-notes [FROM_TAG] [TO_TAG]` + - Parses Conventional Commits between tags + - Supports multiple backends (auto-detected): + - **AI-powered**: GitHub Copilot CLI (best, context-aware) + - **Template-based**: git-cliff (fast, reliable) + - **Manual**: grep/awk fallback (always works) + - **Auto-update feature**: If a GitHub release exists for TO_TAG, automatically offers to update release notes (interactive prompt) - **Usage examples:** + **Usage examples:** - ```bash - # Generate and preview notes - ./scripts/release/generate-notes v0.2.0 v0.3.0 + ```bash + # Generate and preview notes + ./scripts/release/generate-notes v0.2.0 v0.3.0 - # If release exists, you'll see: - # → Generated release notes - # → Detection: "A GitHub release exists for v0.3.0" - # → Prompt: "Do you want to update the release notes on GitHub? [y/N]" - # → Answer 'y' to auto-update, 'n' to skip + # If release exists, you'll see: + # → Generated release notes + # → Detection: "A GitHub release exists for v0.3.0" + # → Prompt: "Do you want to update the release notes on GitHub? [y/N]" + # → Answer 'y' to auto-update, 'n' to skip - # Force specific backend - RELEASE_NOTES_BACKEND=copilot ./scripts/release/generate-notes v0.2.0 v0.3.0 - ``` + # Force specific backend + RELEASE_NOTES_BACKEND=copilot ./scripts/release/generate-notes v0.2.0 v0.3.0 + ``` 4. **GitHub UI Button** (manual, PR-based) - - Uses `.github/release.yml` configuration - - Click "Generate release notes" when creating release - - Works best with PRs that have labels - - Direct commits appear in "Other Changes" category + - Uses `.github/release.yml` configuration + - Click "Generate release notes" when creating release + - Works best with PRs that have labels + - Direct commits appear in "Other Changes" category 5. **CI/CD Automation** (automatic on tag push) - - Workflow: `.github/workflows/release.yml` - - Triggers on version tags (v1.0.0, v2.1.3, etc.) - - Uses git-cliff backend (AI disabled in CI) - - Filters out version bump commits automatically - - Creates GitHub release automatically + - Workflow: `.github/workflows/release.yml` + - Triggers on version tags (v1.0.0, v2.1.3, etc.) + - Uses git-cliff backend (AI disabled in CI) + - Filters out version bump commits automatically + - Creates GitHub release automatically **Recommended Release Workflow:** @@ -1087,13 +1087,13 @@ If you want better release notes after the automated release: **Semantic Versioning Rules:** - **Pre-1.0 (0.x.y)**: - - Breaking changes → bump MINOR (0.x.0) - - New features → bump MINOR (0.x.0) - - Bug fixes → bump PATCH (0.0.x) + - Breaking changes → bump MINOR (0.x.0) + - New features → bump MINOR (0.x.0) + - Bug fixes → bump PATCH (0.0.x) - **Post-1.0 (x.y.z)**: - - Breaking changes → bump MAJOR (x.0.0) - - New features → bump MINOR (0.x.0) - - Bug fixes → bump PATCH (0.0.x) + - Breaking changes → bump MAJOR (x.0.0) + - New features → bump MINOR (0.x.0) + - Bug fixes → bump PATCH (0.0.x) **Alternative: Manual Bump (with Auto-Tag Safety Net):** @@ -1135,25 +1135,25 @@ USE_AI=false ./scripts/release/generate-notes **Backend Comparison:** - **GitHub Copilot CLI** (`copilot`): - - ✅ AI-powered semantic understanding - - ✅ Smart grouping of related commits into single release notes - - ✅ Interprets "Impact:" sections for user-friendly descriptions - - ✅ Multiple commits can be combined with all links: ([hash1](url1), [hash2](url2)) - - ⚠️ Uses premium request quota - - ⚠️ Output may vary between runs + - ✅ AI-powered semantic understanding + - ✅ Smart grouping of related commits into single release notes + - ✅ Interprets "Impact:" sections for user-friendly descriptions + - ✅ Multiple commits can be combined with all links: ([hash1](url1), [hash2](url2)) + - ⚠️ Uses premium request quota + - ⚠️ Output may vary between runs - **git-cliff** (template-based): - - ✅ Fast and consistent - - ✅ 1:1 commit to release note line mapping - - ✅ Highly configurable via `cliff.toml` - - ❌ No semantic understanding - - ❌ Cannot intelligently group related commits + - ✅ Fast and consistent + - ✅ 1:1 commit to release note line mapping + - ✅ Highly configurable via `cliff.toml` + - ❌ No semantic understanding + - ❌ Cannot intelligently group related commits - **manual** (grep/awk): - - ✅ Always available (no dependencies) - - ✅ Basic commit categorization - - ❌ No commit grouping - - ❌ Basic formatting only + - ✅ Always available (no dependencies) + - ✅ Basic commit categorization + - ❌ No commit grouping + - ❌ Basic formatting only **Output Format:** @@ -1469,14 +1469,14 @@ Calling `ruff` or `uv run ruff` directly can cause unintended side effects: 1. Run your custom `ruff` command 2. **Immediately after**, clean up any installation artifacts: - ```bash - # Use our cleanup script (uses both pip and uv pip for compatibility) - ./scripts/clean --minimal + ```bash + # Use our cleanup script (uses both pip and uv pip for compatibility) + ./scripts/clean --minimal - # Or manually: - pip uninstall -y tibber_prices 2>/dev/null || true - uv pip uninstall tibber_prices 2>/dev/null || true - ``` + # Or manually: + pip uninstall -y tibber_prices 2>/dev/null || true + uv pip uninstall tibber_prices 2>/dev/null || true + ``` 3. Ask user to restart HA: `./scripts/develop` @@ -1710,24 +1710,24 @@ We use **Pyright** for static type checking: **Log Level Strategy:** - **INFO Level** - User-facing results and high-level progress: - - Compact 1-line summaries (no multi-line blocks) - - Important results only (success/failure outcomes) - - No indentation (scannability) - - Example: `"Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0%"` - - Example: `"Day 2025-11-11: Success after 1 relaxation phase (2 periods)"` + - Compact 1-line summaries (no multi-line blocks) + - Important results only (success/failure outcomes) + - No indentation (scannability) + - Example: `"Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0%"` + - Example: `"Day 2025-11-11: Success after 1 relaxation phase (2 periods)"` - **DEBUG Level** - Detailed execution trace: - - Full context headers with all relevant configuration - - Step-by-step progression through logic - - Hierarchical indentation to show call depth/logic structure - - Intermediate results and calculations - - Example: `" Day 2025-11-11: Found 1 baseline period (need 2)"` - - Example: `" Phase 1: flex 20.25% + original filters"` + - Full context headers with all relevant configuration + - Step-by-step progression through logic + - Hierarchical indentation to show call depth/logic structure + - Intermediate results and calculations + - Example: `" Day 2025-11-11: Found 1 baseline period (need 2)"` + - Example: `" Phase 1: flex 20.25% + original filters"` - **WARNING Level** - Problems and unexpected states: - - Top-level important messages (no indentation) - - Clear indication of what went wrong - - Example: `"Day 2025-11-11: All relaxation phases exhausted, still only 1 period found"` + - Top-level important messages (no indentation) + - Clear indication of what went wrong + - Example: `"Day 2025-11-11: All relaxation phases exhausted, still only 1 period found"` **Hierarchical Indentation Pattern:** @@ -1806,8 +1806,8 @@ Public entry points → direct helpers (call order) → pure utilities. Prefix p - **Do NOT add legacy migration code** unless the change was already released in a version tag - **Check if released**: Use `./scripts/release/check-if-released ` to verify if code is in any `v*.*.*` tag - **Example**: If introducing breaking config change in commit `abc123`, run `./scripts/release/check-if-released abc123`: - - ✓ NOT RELEASED → No migration needed, just use new code - - ✗ ALREADY RELEASED → Migration may be needed for users upgrading from that version + - ✓ NOT RELEASED → No migration needed, just use new code + - ✗ ALREADY RELEASED → Migration may be needed for users upgrading from that version - **Rule**: Only add backwards compatibility for changes that shipped to users via HACS/GitHub releases - **Prefer breaking changes over complexity**: If migration code would be complex or clutter the codebase, prefer documenting the breaking change in release notes (Home Assistant style). Only add simple migrations (e.g., `.lower()` call, key rename) when trivial. @@ -1826,20 +1826,20 @@ Public entry points → direct helpers (call order) → pure utilities. Prefix p **Examples and use cases:** - **Regional context**: Tibber operates primarily in European markets (Norway, Sweden, Germany, Netherlands). Examples should reflect European context: - - ✅ Use cases: Heat pump, dishwasher, washing machine, electric vehicle charging, water heater - - ✅ Appliances: Common in European homes (heat pumps for heating/cooling, instantaneous water heaters) - - ✅ Energy patterns: European pricing structures (often lower overnight rates, higher daytime rates) - - ✅ Optimization strategies: ECO programs with long run times, heat pump defrost cycles, smart water heating - - ❌ Avoid: US-centric examples (central air conditioning as primary cooling, 240V dryers, different voltage standards) - - ❌ Avoid: US appliance behavior assumptions (e.g., dishwashers requiring hot water connection due to 120V limitations) + - ✅ Use cases: Heat pump, dishwasher, washing machine, electric vehicle charging, water heater + - ✅ Appliances: Common in European homes (heat pumps for heating/cooling, instantaneous water heaters) + - ✅ Energy patterns: European pricing structures (often lower overnight rates, higher daytime rates) + - ✅ Optimization strategies: ECO programs with long run times, heat pump defrost cycles, smart water heating + - ❌ Avoid: US-centric examples (central air conditioning as primary cooling, 240V dryers, different voltage standards) + - ❌ Avoid: US appliance behavior assumptions (e.g., dishwashers requiring hot water connection due to 120V limitations) - **Technical differences**: European appliances operate differently due to 230V power supply: - - Dishwashers: Built-in heaters, ECO programs (long duration, low energy), cold water connection standard - - Washing machines: Fast heating cycles, higher temperature options (60°C, 90°C programs common) - - Heat pumps: Primary heating source (not just cooling), complex defrost cycles, weather-dependent operation + - Dishwashers: Built-in heaters, ECO programs (long duration, low energy), cold water connection standard + - Washing machines: Fast heating cycles, higher temperature options (60°C, 90°C programs common) + - Heat pumps: Primary heating source (not just cooling), complex defrost cycles, weather-dependent operation - **Units and formats**: Use European conventions where appropriate: - - Prices: ct/kWh or øre/kWh (as provided by Tibber API) - - Time: 24-hour format (00:00-23:59) - - Dates: ISO 8601 format (YYYY-MM-DD) + - Prices: ct/kWh or øre/kWh (as provided by Tibber API) + - Time: 24-hour format (00:00-23:59) + - Dates: ISO 8601 format (YYYY-MM-DD) **Language style and tone:** @@ -1848,81 +1848,81 @@ Public entry points → direct helpers (call order) → pure utilities. Prefix p - **Documentation tone**: English documentation should use a friendly, approachable tone. Avoid overly formal constructions like "It is recommended that you..." - prefer "We recommend..." or "You can...". - **Imperative mood**: Use direct imperatives for instructions: "Configure the integration" not "You should configure the integration". - **Language-specific notes**: - - German: Use "du" (informal) and gender-neutral imperatives (e.g., "Konfiguriere" instead of "Konfigurieren Sie") - - Dutch: Use "je/jouw" (informal) instead of "u/uw" (formal) - - Swedish/Norwegian: Already use informal address by default (no formal "Ni"/"De" in modern usage) - - English: Already gender-neutral and appropriately informal + - German: Use "du" (informal) and gender-neutral imperatives (e.g., "Konfiguriere" instead of "Konfigurieren Sie") + - Dutch: Use "je/jouw" (informal) instead of "u/uw" (formal) + - Swedish/Norwegian: Already use informal address by default (no formal "Ni"/"De" in modern usage) + - English: Already gender-neutral and appropriately informal **User Documentation Quality:** When writing or updating user-facing documentation (`docs/user/docs/` or `docs/developer/docs/`), follow these principles learned from real user feedback: - **Clarity over completeness**: Users want to understand concepts, not read technical specifications - - ✅ Good: "Relaxation automatically loosens filters until enough periods are found" - - ❌ Bad: "The relaxation algorithm implements a 4×4 matrix strategy with multiplicative flex increments" + - ✅ Good: "Relaxation automatically loosens filters until enough periods are found" + - ❌ Bad: "The relaxation algorithm implements a 4×4 matrix strategy with multiplicative flex increments" - **Visual examples**: Use timeline diagrams, code blocks with comments, before/after comparisons - - ✅ Show what a "period" looks like on a 24-hour timeline - - ✅ Include automation examples with real entity names + - ✅ Show what a "period" looks like on a 24-hour timeline + - ✅ Include automation examples with real entity names - **Use-case driven**: Start with "what can I do with this?" not "how does it work internally" - - ✅ Structure: Quick Start → Common Scenarios → Configuration Guide → Advanced Topics - - ❌ Avoid: Starting with mathematical formulas or algorithm descriptions + - ✅ Structure: Quick Start → Common Scenarios → Configuration Guide → Advanced Topics + - ❌ Avoid: Starting with mathematical formulas or algorithm descriptions - **Practical troubleshooting**: Address real problems users encounter - - ✅ "No periods found → Try: increase flex from 15% to 20%" - - ❌ Avoid: Generic "check your configuration" without specific guidance + - ✅ "No periods found → Try: increase flex from 15% to 20%" + - ❌ Avoid: Generic "check your configuration" without specific guidance - **Progressive disclosure**: Basic concepts first, advanced details later - - ✅ Main doc covers 80% use cases in simple terms - - ✅ Link to advanced/technical docs for edge cases - - ❌ Don't mix basic explanations with deep technical details + - ✅ Main doc covers 80% use cases in simple terms + - ✅ Link to advanced/technical docs for edge cases + - ❌ Don't mix basic explanations with deep technical details - **When code changed significantly**: Verify documentation still matches - - If relaxation strategy changed from 3 phases to 4×4 matrix → documentation MUST reflect this - - If metadata format changed → update all examples showing attributes - - If per-day independence was added → explain why some days relax differently + - If relaxation strategy changed from 3 phases to 4×4 matrix → documentation MUST reflect this + - If metadata format changed → update all examples showing attributes + - If per-day independence was added → explain why some days relax differently **Documentation Writing Strategy:** Understanding **how** good documentation emerges is as important as knowing what makes it good: - **Live Understanding vs. Code Analysis** - - ✅ **DO:** Write docs during/after active development - - When implementing complex logic, document it while the "why" is fresh - - Use real examples from debugging sessions (actual logs, real data) - - Document decisions as they're made, not after the fact - - ❌ **DON'T:** Write docs from cold code analysis - - Reading code shows "what", not "why" - - Missing context: Which alternatives were considered? - - No user perspective: What's actually confusing? + - ✅ **DO:** Write docs during/after active development + - When implementing complex logic, document it while the "why" is fresh + - Use real examples from debugging sessions (actual logs, real data) + - Document decisions as they're made, not after the fact + - ❌ **DON'T:** Write docs from cold code analysis + - Reading code shows "what", not "why" + - Missing context: Which alternatives were considered? + - No user perspective: What's actually confusing? - **User Feedback Loop** - - Key insight: Documentation improves when users question it - - Pattern: - 1. User asks: "Does this still match the code?" - 2. AI realizes: "Oh, the 3-phase model is outdated" - 3. Together we trace through real behavior - 4. Documentation gets rewritten with correct mental model - - Why it works: User questions force critical thinking, real confusion points get addressed + - Key insight: Documentation improves when users question it + - Pattern: + 1. User asks: "Does this still match the code?" + 2. AI realizes: "Oh, the 3-phase model is outdated" + 3. Together we trace through real behavior + 4. Documentation gets rewritten with correct mental model + - Why it works: User questions force critical thinking, real confusion points get addressed - **Log-Driven Documentation** - - Observation: When logs explain logic clearly, documentation becomes easier - - Why: Logs show state transitions ("Baseline insufficient → Starting relaxation"), decisions ("Replaced period X with larger Y"), and are already written for humans - - Pattern: If you spent hours making logs clear → use that clarity in documentation too + - Observation: When logs explain logic clearly, documentation becomes easier + - Why: Logs show state transitions ("Baseline insufficient → Starting relaxation"), decisions ("Replaced period X with larger Y"), and are already written for humans + - Pattern: If you spent hours making logs clear → use that clarity in documentation too - **Concrete Examples > Abstract Descriptions** - - ✅ **Good:** "Day 2025-11-11 found 2 periods at flex=12.0% +volatility_any (stopped early, no need to try higher flex)" - - ❌ **Bad:** "The relaxation algorithm uses a configurable threshold multiplier with filter combination strategies" - - Use real data from debug sessions, show actual attribute values, demonstrate with timeline diagrams + - ✅ **Good:** "Day 2025-11-11 found 2 periods at flex=12.0% +volatility_any (stopped early, no need to try higher flex)" + - ❌ **Bad:** "The relaxation algorithm uses a configurable threshold multiplier with filter combination strategies" + - Use real data from debug sessions, show actual attribute values, demonstrate with timeline diagrams - **Context Accumulation in Long Sessions** - - Advantage: AI builds mental model incrementally, sees evolution of logic (not just final state), understands trade-offs - - Disadvantage of short sessions: Cold start every time, missing "why" context, documentation becomes spec-writing - - Lesson: Complex documentation benefits from focused, uninterrupted work with accumulated context + - Advantage: AI builds mental model incrementally, sees evolution of logic (not just final state), understands trade-offs + - Disadvantage of short sessions: Cold start every time, missing "why" context, documentation becomes spec-writing + - Lesson: Complex documentation benefits from focused, uninterrupted work with accumulated context - **Document the "Why", Not Just the "What"** - - Every complex pattern should answer: - 1. **What** does it do? (quick summary) - 2. **Why** was it designed this way? (alternatives considered) - 3. **How** does a user benefit? (practical impact) - 4. **When** does it fail? (known limitations) - - Example: "Replacement Logic: Larger periods replace smaller overlapping ones because users want ONE long cheap period, not multiple short overlapping ones." + - Every complex pattern should answer: + 1. **What** does it do? (quick summary) + 2. **Why** was it designed this way? (alternatives considered) + 3. **How** does a user benefit? (practical impact) + 4. **When** does it fail? (known limitations) + - Example: "Replacement Logic: Larger periods replace smaller overlapping ones because users want ONE long cheap period, not multiple short overlapping ones." ## Ruff Code Style Guidelines @@ -2422,31 +2422,31 @@ If the answer to any is "no", make the name more explicit. After the sensor.py refactoring (completed Nov 2025), sensors are organized by **calculation method** rather than feature type. Follow these steps: 1. **Determine calculation pattern** - Choose which group your sensor belongs to: - - **Interval-based**: Uses time offset from current interval (e.g., current/next/previous) - - **Rolling hour**: Aggregates 5-interval window (2 before + center + 2 after) - - **Daily statistics**: Min/max/avg within calendar day boundaries - - **24h windows**: Trailing/leading from current interval - - **Future forecast**: N-hour windows starting from next interval - - **Volatility**: Statistical analysis of price variation - - **Diagnostic**: System information and metadata + - **Interval-based**: Uses time offset from current interval (e.g., current/next/previous) + - **Rolling hour**: Aggregates 5-interval window (2 before + center + 2 after) + - **Daily statistics**: Min/max/avg within calendar day boundaries + - **24h windows**: Trailing/leading from current interval + - **Future forecast**: N-hour windows starting from next interval + - **Volatility**: Statistical analysis of price variation + - **Diagnostic**: System information and metadata **IMPORTANT — After adding/renaming entities**: Run `./scripts/docs/generate-sensor-reference` to regenerate the multi-language sensor reference table. The `scripts/check` and CI will fail if the reference is stale. 2. **Add entity description** to appropriate sensor group in `sensor/definitions.py`: - - `INTERVAL_PRICE_SENSORS`, `INTERVAL_LEVEL_SENSORS`, or `INTERVAL_RATING_SENSORS` - - `ROLLING_HOUR_PRICE_SENSORS`, `ROLLING_HOUR_LEVEL_SENSORS`, or `ROLLING_HOUR_RATING_SENSORS` - - `DAILY_STAT_SENSORS` - - `WINDOW_24H_SENSORS` - - `FUTURE_AVG_SENSORS` or `FUTURE_TREND_SENSORS` - - `VOLATILITY_SENSORS` - - `DIAGNOSTIC_SENSORS` + - `INTERVAL_PRICE_SENSORS`, `INTERVAL_LEVEL_SENSORS`, or `INTERVAL_RATING_SENSORS` + - `ROLLING_HOUR_PRICE_SENSORS`, `ROLLING_HOUR_LEVEL_SENSORS`, or `ROLLING_HOUR_RATING_SENSORS` + - `DAILY_STAT_SENSORS` + - `WINDOW_24H_SENSORS` + - `FUTURE_AVG_SENSORS` or `FUTURE_TREND_SENSORS` + - `VOLATILITY_SENSORS` + - `DIAGNOSTIC_SENSORS` 3. **Add handler mapping** in `sensor/core.py` → `_get_value_getter()` method: - - For interval-based: Use `_get_interval_value(interval_offset, value_type)` - - For rolling hour: Use `_get_rolling_hour_value(hour_offset, value_type)` - - For daily stats: Use `_get_daily_stat_value(day, stat_func)` - - For 24h windows: Use `_get_24h_window_value(stat_func)` - - For others: Implement specific handler if needed + - For interval-based: Use `_get_interval_value(interval_offset, value_type)` + - For rolling hour: Use `_get_rolling_hour_value(hour_offset, value_type)` + - For daily stats: Use `_get_daily_stat_value(day, stat_func)` + - For 24h windows: Use `_get_24h_window_value(stat_func)` + - For others: Implement specific handler if needed 4. **Add translation keys** to `/translations/en.json` and `/custom_translations/en.json` @@ -2461,24 +2461,24 @@ After the sensor.py refactoring (completed Nov 2025), sensors are organized by * The refactoring consolidated duplicate logic into unified methods in `sensor/core.py`: - **`_get_interval_value(interval_offset, value_type, in_euro=False)`** - - Replaces: `_get_interval_price_value()`, `_get_interval_level_value()`, `_get_interval_rating_value()` - - Handles: All interval-based sensors (current/next/previous) - - Returns: Price (float), level (str), or rating (str) based on value_type + - Replaces: `_get_interval_price_value()`, `_get_interval_level_value()`, `_get_interval_rating_value()` + - Handles: All interval-based sensors (current/next/previous) + - Returns: Price (float), level (str), or rating (str) based on value_type - **`_get_rolling_hour_value(hour_offset, value_type)`** - - Replaces: `_get_rolling_hour_average_value()`, `_get_rolling_hour_level_value()`, `_get_rolling_hour_rating_value()` - - Handles: All 5-interval rolling hour windows - - Returns: Aggregated value (average price, aggregated level/rating) + - Replaces: `_get_rolling_hour_average_value()`, `_get_rolling_hour_level_value()`, `_get_rolling_hour_rating_value()` + - Handles: All 5-interval rolling hour windows + - Returns: Aggregated value (average price, aggregated level/rating) - **`_get_daily_stat_value(day, stat_func)`** - - Replaces: `_get_statistics_value()` (calendar day portion) - - Handles: Min/max/avg for calendar days (today/tomorrow) - - Returns: Price in subunit currency units (cents/øre) + - Replaces: `_get_statistics_value()` (calendar day portion) + - Handles: Min/max/avg for calendar days (today/tomorrow) + - Returns: Price in subunit currency units (cents/øre) - **`_get_24h_window_value(stat_func)`** - - Replaces: `_get_average_value()`, `_get_minmax_value()` - - Handles: Trailing/leading 24h window statistics - - Returns: Price in subunit currency units (cents/øre) + - Replaces: `_get_average_value()`, `_get_minmax_value()` + - Handles: Trailing/leading 24h window statistics + - Returns: Price in subunit currency units (cents/øre) Legacy wrapper methods still exist for backward compatibility but will be removed in a future cleanup phase. @@ -2502,21 +2502,21 @@ Edit `utils/price.py` or `utils/average.py`. These are stateless pure functions The config flow is split into three separate flow handlers: 1. **User Flow** (`config_flow/user_flow.py`) - Initial setup and reauth - - `async_step_user()` - API token input - - `async_step_select_home()` - Home selection - - `async_step_reauth()` / `async_step_reauth_confirm()` - Reauth flow + - `async_step_user()` - API token input + - `async_step_select_home()` - Home selection + - `async_step_reauth()` / `async_step_reauth_confirm()` - Reauth flow 2. **Subentry Flow** (`config_flow/subentry_flow.py`) - Add additional homes - - `async_step_user()` - Select from available homes - - `async_step_init()` - Subentry options + - `async_step_user()` - Select from available homes + - `async_step_init()` - Subentry options 3. **Options Flow** (`config_flow/options_flow.py`) - Reconfiguration - - `async_step_init()` - General settings - - `async_step_current_interval_price_rating()` - Price rating thresholds - - `async_step_volatility()` - Volatility settings - - `async_step_best_price()` - Best price period settings - - `async_step_peak_price()` - Peak price period settings - - `async_step_price_trend()` - Price trend thresholds + - `async_step_init()` - General settings + - `async_step_current_interval_price_rating()` - Price rating thresholds + - `async_step_volatility()` - Volatility settings + - `async_step_best_price()` - Best price period settings + - `async_step_peak_price()` - Peak price period settings + - `async_step_price_trend()` - Price trend thresholds To add a new step: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1945758..4120cd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,14 +18,14 @@ For detailed developer documentation, see [docs/development/](docs/development/) 1. **Fork the repository** on GitHub 2. **Clone your fork**: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. **Open in DevContainer** (recommended): - - Open in VS Code - - Click "Reopen in Container" when prompted - - Or manually: `Ctrl+Shift+P` → "Dev Containers: Reopen in Container" + - Open in VS Code + - Click "Reopen in Container" when prompted + - Or manually: `Ctrl+Shift+P` → "Dev Containers: Reopen in Container" See [Development Setup](docs/development/setup.md) for detailed instructions. @@ -100,9 +100,9 @@ See `.github/instructions/commit-messages.instructions.md` for detailed commit-m 1. **Push your branch** to your fork 2. **Create a Pull Request** on GitHub with: - - Clear title describing the change - - Detailed description with context - - Reference related issues (`Fixes #123`) + - Clear title describing the change + - Detailed description with context + - Reference related issues (`Fixes #123`) 3. **Wait for review** and address feedback ### PR Requirements @@ -141,11 +141,11 @@ See [Coding Guidelines](docs/developer/docs/coding-guidelines.md) for complete d Documentation is organized in two Docusaurus sites: - **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation via `docs/user/sidebars.ts` + - Markdown files in `docs/user/docs/*.md` + - Navigation via `docs/user/sidebars.ts` - **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation via `docs/developer/sidebars.ts` + - Markdown files in `docs/developer/docs/*.md` + - Navigation via `docs/developer/sidebars.ts` **When adding new documentation:** diff --git a/README.md b/README.md index fd78930..547c7e2 100644 --- a/README.md +++ b/README.md @@ -128,32 +128,32 @@ The integration provides **100+ entities** across sensors, binary sensors, switc ```yaml automation: - - alias: "Start Dishwasher During Best Price Period" - trigger: - - platform: state - entity_id: binary_sensor.tibber_best_price_period - to: "on" - action: - - action: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Start Dishwasher During Best Price Period" + trigger: + - platform: state + entity_id: binary_sensor.tibber_best_price_period + to: "on" + action: + - action: switch.turn_on + target: + entity_id: switch.dishwasher ``` **Reduce heating when prices spike above average:** ```yaml automation: - - alias: "Reduce Heating During High Prices" - trigger: - - platform: numeric_state - entity_id: sensor.tibber_current_interval_price_rating - above: 20 # More than 20% above 24h average - action: - - action: climate.set_temperature - target: - entity_id: climate.living_room - data: - temperature: 19 + - alias: "Reduce Heating During High Prices" + trigger: + - platform: numeric_state + entity_id: sensor.tibber_current_interval_price_rating + above: 20 # More than 20% above 24h average + action: + - action: climate.set_temperature + target: + entity_id: climate.living_room + data: + temperature: 19 ``` 📖 **[More automations →](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples)** — EV charging, heat pump control, price notifications, and more diff --git a/custom_components/tibber_prices/manifest.json b/custom_components/tibber_prices/manifest.json index a91a98f..678b023 100644 --- a/custom_components/tibber_prices/manifest.json +++ b/custom_components/tibber_prices/manifest.json @@ -1,15 +1,11 @@ { "domain": "tibber_prices", "name": "Tibber Price Information & Ratings", - "codeowners": [ - "@jpawlowski" - ], + "codeowners": ["@jpawlowski"], "config_flow": true, "documentation": "https://github.com/jpawlowski/hass.tibber_prices", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues", - "requirements": [ - "aiofiles>=23.2.1" - ], + "requirements": ["aiofiles>=23.2.1"], "version": "0.31.0b1" } diff --git a/pyproject.toml b/pyproject.toml index 8085631..6a028c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,9 @@ -[build-system] -requires = ["setuptools==82.0.1"] -build-backend = "setuptools.build_meta" +# Custom Component pyproject.toml based on Home Assistant Core +# https://github.com/home-assistant/core/blob/dev/pyproject.toml +# +# Sections not included (HA Core specific): +# - [build-system] - Not published to PyPI (installed via HACS) +# - [tool.pylint.*] - Optional additional linting (Ruff is sufficient) [project] name = "tibber_prices" @@ -13,73 +16,276 @@ packages = ["custom_components.tibber_prices"] [tool.pyright] include = ["custom_components/tibber_prices"] exclude = [ - "**/node_modules", + "**/.*", "**/__pycache__", - "**/.git", - "**/.github", + "**/config", "**/docs", + "**/node_modules", "**/venv", - "**/.venv", ] venvPath = "." venv = ".venv" typeCheckingMode = "basic" - -[tool.ruff] -# Based on https://github.com/home-assistant/core/blob/dev/pyproject.toml -target-version = "py314" -line-length = 120 - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - # "ANN101", # Missing type annotation for `self` in method - "ANN401", # Dynamically typed expressions (typing.Any) are disallowed - "D203", # no-blank-line-before-class (incompatible with formatter) - "D212", # multi-line-summary-first-line (incompatible with formatter) - "COM812", # incompatible with formatter - "ISC001", # incompatible with formatter - "UP037", # quoted annotations; needed for TYPE_CHECKING forward references -] - -[tool.ruff.lint.per-file-ignores] -"tests/*" = [ - "S101", # assert is fine in tests - "PLR2004", # Magic values are fine in tests -] -"scripts/*" = [ - "T201", # print() is the correct output method for CLI scripts - "INP001", # scripts/ is not a Python package (no __init__.py) -] - -[tool.ruff.lint.flake8-pytest-style] -fixture-parentheses = false - -[tool.ruff.lint.pyupgrade] -keep-runtime-typing = true - -[tool.ruff.lint.mccabe] -max-complexity = 25 - -[tool.ruff.lint.isort] -force-single-line = false -known-first-party = ["custom_components", "homeassistant"] +reportUnusedImport = "none" +reportUnusedVariable = "none" +reportUnusedCoroutine = "none" +reportMissingTypeStubs = "none" [tool.pytest.ini_options] testpaths = ["tests"] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] +norecursedirs = [".git", "testing_config"] +log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_debug = true +asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" addopts = "-ra -q --strict-markers" markers = [ "unit: Unit tests (fast, no external dependencies)", "integration: Integration tests (may use coordinator/time service)", ] +filterwarnings = [ + # Treat warnings as errors to catch issues early + "error", + # Ignore specific warnings from third-party libraries as needed + # "ignore:.*custom_components.* is using deprecated.*:DeprecationWarning", +] + +[tool.coverage.run] +source = ["custom_components/tibber_prices"] +omit = ["tests/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "@overload", +] + +[tool.ruff] +# Based on https://github.com/home-assistant/core/blob/dev/pyproject.toml +required-version = ">=0.15.1" +target-version = "py314" +line-length = 120 + +[tool.ruff.lint] +select = [ + "A001", # Variable {name} is shadowing a Python builtin + "ASYNC", # flake8-async + "B002", # Python does not support the unary prefix increment + "B005", # Using .strip() with multi-character strings is misleading + "B007", # Loop control variable {name} not used within loop body + "B009", # Do not call getattr with a constant attribute value. It is not any safer than normal property access. + "B014", # Exception handler with duplicate exception + "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "B017", # pytest.raises(BaseException) should be considered evil + "B018", # Found useless attribute access. Either assign it to a variable or remove it. + "B023", # Function definition does not bind loop variable {name} + "B024", # `{name}` is an abstract base class, but it has no abstract methods or properties + "B025", # try-except* block with duplicate exception {name} + "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? + "B035", # Dictionary comprehension uses static key + "B904", # Use raise from to specify exception cause + "B905", # zip() without an explicit strict= parameter + "BLE", + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "F541", # f-string without any placeholders + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF007", # Prefer itertools.pairwise() over zip() when iterating over successive pairs + "RUF008", # Do not use mutable default values for dataclass attributes + "RUF010", # Use explicit conversion flag + "RUF013", # PEP 484 prohibits implicit Optional + "RUF016", # Slice in indexed access to type {value_type} uses type {index_type} instead of an integer + "RUF017", # Avoid quadratic list summation + "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access + "RUF020", # {never_like} | T is equivalent to T + "RUF021", # Parenthesize a and b expressions when chaining and and or together, to make the precedence clear + "RUF022", # Sort __all__ + "RUF023", # Sort __slots__ + "RUF024", # Do not pass mutable objects as values to dict.fromkeys + "RUF026", # default_factory is a positional-only argument to defaultdict + "RUF030", # print() call in assert statement is likely unintentional + "RUF032", # Decimal() called with float literal argument + "RUF033", # __post_init__ method with argument defaults + "RUF034", # Useless if-else condition + "RUF059", # unused-unpacked-variable + "RUF100", # Unused `noqa` directive + "RUF101", # noqa directives that use redirected rule codes + "RUF200", # Failed to parse pyproject.toml: {message} + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "T20", # flake8-print + "TC", # flake8-type-checking + "TID", # Tidy imports + "TRY", # tryceratops + "UP", # pyupgrade + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call + "W", # pycodestyle +] + +ignore = [ + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "ASYNC109", # Async function definition with a `timeout` parameter Use `asyncio.timeout` instead + "ASYNC110", # Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop + "ASYNC240", # Use an async function for entering the file system + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D417", # Missing argument descriptions in docstring - to allow documenting only non-obvious parameters + "E501", # line too long + + "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW0108", # Unnecessary lambda wrapping a function call; can often be replaced by the function itself + "PLW1641", # __eq__ without __hash__ + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception + "PT018", # Assertion should be broken down into multiple parts + "RUF001", # String contains ambiguous unicode character. + "RUF002", # Docstring contains ambiguous unicode character. + "RUF003", # Comment contains ambiguous unicode character. + "RUF015", # Prefer next(...) over single element slice + "SIM102", # Use a single if statement instead of nested if statements + "SIM103", # Return the condition {condition} directly + "SIM108", # Use ternary operator {contents} instead of if-else-block + "SIM115", # Use context handler for opening files + + # Moving imports into type-checking blocks can mess with pytest.patch() + "TC001", # Move application import {} into a type-checking block + "TC002", # Move third-party import {} into a type-checking block + "TC003", # Move standard library import {} into a type-checking block + # Quotes for typing.cast generally not necessary, only for performance critical paths + "TC006", # Add quotes to type expression in typing.cast() + + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` + + "UP046", # Non PEP 695 generic class + "UP047", # Non PEP 696 generic function + "UP049", # Avoid private type parameter names + + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D203", + "D206", + "D212", + "D300", + "Q", + "COM812", + "COM819", + "ISC001", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605", + + "FURB116", +] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +# Commonly used Home Assistant imports +voluptuous = "vol" +"homeassistant.helpers.config_validation" = "cv" +"homeassistant.helpers.device_registry" = "dr" +"homeassistant.helpers.entity_registry" = "er" +"homeassistant.util.dt" = "dt_util" + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"async_timeout".msg = "use asyncio.timeout instead" +"pytz".msg = "use zoneinfo instead" +"tests".msg = "You should not import tests" + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = ["custom_components", "homeassistant"] +combine-as-imports = true +split-on-trailing-comma = false + +[tool.ruff.lint.per-file-ignores] +"script/*" = [ + "T20", # print() allowed in scripts + "INP001", # Implicit namespace package (scripts are not a package) +] +"tests/*" = [ + "S101", # assert is fine in tests + "PLR2004", # Magic values are fine in tests + "D", # Docstrings not required in tests + "PTH", # Use pathlib - temporary exemption for tests +] + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.pydocstyle] +convention = "google" +property-decorators = ["propcache.api.cached_property"] + +[tool.ruff.lint.pyupgrade] +keep-runtime-typing = true [project.optional-dependencies] -test = [ - "pytest>=9.0.3", - "pytest-asyncio>=1.3.0", - "pytest-homeassistant-custom-component>=0.13.323", -] +test = ["pytest-homeassistant-custom-component>=0.13.323"] diff --git a/schemas/json/manifest_schema.json b/schemas/json/manifest_schema.json index e307c07..eaa48e2 100644 --- a/schemas/json/manifest_schema.json +++ b/schemas/json/manifest_schema.json @@ -84,15 +84,7 @@ "description": "The integration type.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type", "type": "string", "default": "hub", - "enum": [ - "device", - "entity", - "hardware", - "helper", - "hub", - "service", - "system" - ] + "enum": ["device", "entity", "hardware", "helper", "hub", "service", "system"] }, "config_flow": { "description": "Whether the integration is configurable from the UI.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#config-flow", @@ -375,14 +367,7 @@ "iot_class": { "description": "The IoT class of the integration, describing how the integration connects to the device or service.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#iot-class", "type": "string", - "enum": [ - "assumed_state", - "cloud_polling", - "cloud_push", - "local_polling", - "local_push", - "calculated" - ] + "enum": ["assumed_state", "cloud_polling", "cloud_push", "local_polling", "local_push", "calculated"] }, "single_config_entry": { "description": "Whether the integration only supports a single config entry.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#single-config-entry-only", @@ -395,14 +380,7 @@ } }, "additionalProperties": false, - "required": [ - "domain", - "name", - "codeowners", - "documentation", - "issue_tracker", - "version" - ], + "required": ["domain", "name", "codeowners", "documentation", "issue_tracker", "version"], "dependencies": { "mqtt": { "anyOf": [