docs(CONTRIBUTING): improve clarity and formatting in contribution guidelines

Refactor the contribution guidelines to enhance readability and consistency in formatting. Adjusted code blocks and list formatting for better visual structure.

Impact: Contributors will find it easier to follow the guidelines when contributing to the project.

---

docs(README): update automation examples for better readability

Reformatted YAML automation examples in the README to improve clarity and consistency. Indentation and structure were adjusted for better understanding.

Impact: Users will have clearer examples for setting up automations with the integration.

---

chore(manifest): streamline manifest file formatting

Consolidated formatting in the manifest file for consistency. Adjusted the codeowners and requirements sections for a cleaner look.

User-Impact: none

---

chore(pyproject): enhance project configuration for better linting and testing

Updated the pyproject.toml file to improve linting configurations and testing options. Added specific rules for ruff and pytest to align with project standards.

User-Impact: none

---

chore(manifest_schema): simplify JSON schema for integration manifest

Refined the manifest schema by consolidating enum definitions for better readability and maintenance.

User-Impact: none

---

chore(prettier): add Prettier configuration for consistent code formatting

Introduced a Prettier configuration file to standardize code formatting across the project, ensuring consistency in style.

User-Impact: none
This commit is contained in:
Julian Pawlowski 2026-04-14 15:35:16 +00:00
parent 9efa7809d0
commit 2e7ccc36c5
22 changed files with 894 additions and 594 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "jpawlowski/hass.tibber_prices", "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", "postCreateCommand": "bash .devcontainer/setup-git.sh && scripts/setup/setup",
"postStartCommand": "scripts/motd", "postStartCommand": "scripts/motd",
"containerEnv": { "containerEnv": {
@ -63,9 +63,7 @@
"**/node_modules/**" "**/node_modules/**"
], ],
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.analysis.extraPaths": [ "python.analysis.extraPaths": ["${workspaceFolder}/.venv/lib/python3.14/site-packages"],
"${workspaceFolder}/.venv/lib/python3.14/site-packages"
],
"python.terminal.activateEnvironment": true, "python.terminal.activateEnvironment": true,
"python.terminal.activateEnvInCurrentTerminal": true, "python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"], "python.testing.pytestArgs": ["--no-cov"],
@ -142,6 +140,8 @@
}, },
"ghcr.io/devcontainers-extra/features/apt-packages:1": { "ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": [ "packages": [
"autoconf",
"automake",
"bat", "bat",
"eza", "eza",
"fd-find", "fd-find",
@ -154,9 +154,12 @@
"jo", "jo",
"jq", "jq",
"libpcap-dev", "libpcap-dev",
"libssl-dev",
"libtool",
"libturbojpeg0", "libturbojpeg0",
"miller", "miller",
"moreutils", "moreutils",
"pipx",
"ripgrep", "ripgrep",
"shellcheck", "shellcheck",
"shfmt", "shfmt",

View file

@ -1,6 +1,7 @@
# top-most EditorConfig file # top-most EditorConfig file
root = true root = true
# Default settings - AI-friendly baseline
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
@ -9,12 +10,71 @@ trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
# Python - Home Assistant & Ruff defaults (120 chars)
[*.py] [*.py]
# Python style aligns with Black
indent_size = 4 indent_size = 4
max_line_length = 120
# YAML - Home Assistant configs, GitHub workflows
[*.{yaml,yml}]
indent_size = 2
# JSON - manifest.json, translations, etc.
[*.json] [*.json]
indent_size = 2 indent_size = 2
# Markdown - READMEs, docs (preserve AI formatting)
[*.md] [*.md]
indent_size = 2
trim_trailing_whitespace = false 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

2
.github/FUNDING.yml vendored
View file

@ -1,4 +1,4 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [ jpawlowski ] github: [jpawlowski]
buy_me_a_coffee: jpawlowski buy_me_a_coffee: jpawlowski

View file

@ -3,69 +3,69 @@ name: "Bug report"
description: "Report a bug with the custom integration" description: "Report a bug with the custom integration"
labels: ["bug"] labels: ["bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: Before you open a new issue, search through the existing issues to see if others have had the same problem. value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
- type: input - type: input
attributes: attributes:
label: "Home Assistant version" label: "Home Assistant version"
description: "The version of Home Assistant you are using" description: "The version of Home Assistant you are using"
placeholder: "2025.1.0" placeholder: "2025.1.0"
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: "Integration version" label: "Integration version"
description: "The version of this custom integration you are using" description: "The version of this custom integration you are using"
placeholder: "1.0.0" placeholder: "1.0.0"
validations: validations:
required: false required: false
- type: textarea - type: textarea
attributes: attributes:
label: "System Health details" 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)" description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)"
validations: validations:
required: false required: false
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Checklist label: Checklist
options: options:
- label: I have enabled debug logging for my installation. - label: I have enabled debug logging for my installation.
required: true required: true
- label: I have filled out the issue template to the best of my ability. - label: I have filled out the issue template to the best of my ability.
required: true required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue). - label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true 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+).. - 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 required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Describe the issue" label: "Describe the issue"
description: "A clear and concise description of what the issue is." description: "A clear and concise description of what the issue is."
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Reproduction steps 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." 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: | value: |
1. 1.
2. 2.
3. 3.
... ...
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Debug logs" 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." 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 render: text
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Diagnostics dump" label: "Diagnostics dump"
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)" description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"
validations: validations:
required: false required: false

View file

@ -1 +1 @@
blank_issues_enabled: false blank_issues_enabled: false

View file

@ -3,45 +3,45 @@ name: "Feature request"
description: "Suggest an idea for this custom integration" description: "Suggest an idea for this custom integration"
labels: ["Feature request"] labels: ["Feature request"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: Before you open a new feature request, search through the existing feature requests to see if others have had the same idea. 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 - type: checkboxes
attributes: attributes:
label: Checklist label: Checklist
options: options:
- label: I have filled out the template to the best of my ability. - label: I have filled out the template to the best of my ability.
required: true required: true
- label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request). - label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request).
required: true 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+). - 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 required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Is your feature request related to a problem? Please describe." label: "Is your feature request related to a problem? Please describe."
description: "A clear and concise description of what the problem is." description: "A clear and concise description of what the problem is."
placeholder: "I'm always frustrated when [...]" placeholder: "I'm always frustrated when [...]"
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Describe the solution you'd like" label: "Describe the solution you'd like"
description: "A clear and concise description of what you want to happen." description: "A clear and concise description of what you want to happen."
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Describe alternatives you've considered" label: "Describe alternatives you've considered"
description: "A clear and concise description of any alternative solutions or features you've considered." description: "A clear and concise description of any alternative solutions or features you've considered."
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Additional context" label: "Additional context"
description: "Add any other context or screenshots about the feature request here." description: "Add any other context or screenshots about the feature request here."
validations: validations:
required: true required: true

View file

@ -44,3 +44,6 @@ updates:
ignore: ignore:
# Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json
- dependency-name: "homeassistant" - 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"

View file

@ -41,9 +41,9 @@ In that case:
- Prefer chore(...) or refactor(...) instead of fix(...), and/or - Prefer chore(...) or refactor(...) instead of fix(...), and/or
- Add an explicit trailer in the commit body: - Add an explicit trailer in the commit body:
- Release-Notes: skip - Release-Notes: skip
- User-Impact: none - User-Impact: none
- Released-Bug: no - Released-Bug: no
Any one of these trailers is enough. Any one of these trailers is enough.

View file

@ -5,7 +5,7 @@ on:
branches: branches:
- main - main
paths: paths:
- 'custom_components/tibber_prices/manifest.json' - "custom_components/tibber_prices/manifest.json"
permissions: permissions:
contents: write contents: write
@ -22,7 +22,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with: 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 - name: Extract version from manifest.json
id: manifest id: manifest

View file

@ -4,10 +4,10 @@ on:
push: push:
branches: [main] branches: [main]
paths: paths:
- 'docs/**' - "docs/**"
- '.github/workflows/docusaurus.yml' - ".github/workflows/docusaurus.yml"
tags: tags:
- 'v*.*.*' - "v*.*.*"
workflow_dispatch: workflow_dispatch:
# Concurrency control: cancel in-progress deployments # Concurrency control: cancel in-progress deployments
@ -31,7 +31,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 # Needed for version timestamps fetch-depth: 0 # Needed for version timestamps
- name: Detect prerelease tag (beta/rc) - name: Detect prerelease tag (beta/rc)
id: taginfo id: taginfo
@ -47,7 +47,7 @@ jobs:
- uses: actions/setup-node@v6 - uses: actions/setup-node@v6
with: with:
node-version: 24 node-version: 24
cache: 'npm' cache: "npm"
cache-dependency-path: | cache-dependency-path: |
docs/user/package-lock.json docs/user/package-lock.json
docs/developer/package-lock.json docs/developer/package-lock.json
@ -56,7 +56,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: '3.14' python-version: "3.14"
- name: Verify sensor reference is up-to-date - name: Verify sensor reference is up-to-date
run: python3 scripts/docs/generate-sensor-reference --check run: python3 scripts/docs/generate-sensor-reference --check

View file

@ -5,14 +5,14 @@ on:
branches: branches:
- "main" - "main"
paths-ignore: paths-ignore:
- 'docs/**' - "docs/**"
- '.github/workflows/docusaurus.yml' - ".github/workflows/docusaurus.yml"
pull_request: pull_request:
branches: branches:
- "main" - "main"
paths-ignore: paths-ignore:
- 'docs/**' - "docs/**"
- '.github/workflows/docusaurus.yml' - ".github/workflows/docusaurus.yml"
permissions: {} permissions: {}
@ -34,7 +34,7 @@ jobs:
python-version: "3.14" python-version: "3.14"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with: with:
version: "0.9.3" version: "0.9.3"

View file

@ -3,16 +3,16 @@ name: Generate Release Notes
on: on:
push: push:
tags: 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: workflow_dispatch:
inputs: inputs:
tag: tag:
description: 'Tag version to release (e.g., v0.3.0)' description: "Tag version to release (e.g., v0.3.0)"
required: true required: true
type: string type: string
permissions: permissions:
contents: write # Needed to create/update releases and push commits contents: write # Needed to create/update releases and push commits
jobs: jobs:
# Note: We trust that validate.yml and lint.yml have already run on the # Note: We trust that validate.yml and lint.yml have already run on the
@ -103,13 +103,13 @@ jobs:
release-notes: release-notes:
name: Generate and publish release notes name: Generate and publish release notes
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: sync-manifest # Wait for manifest sync to complete needs: sync-manifest # Wait for manifest sync to complete
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 # Fetch all history for git-cliff fetch-depth: 0 # Fetch all history for git-cliff
ref: main # Use updated main branch if manifest was synced ref: main # Use updated main branch if manifest was synced
- name: Get previous tag - name: Get previous tag
id: previoustag id: previoustag
@ -261,7 +261,7 @@ jobs:
body: ${{ steps.release_notes.outputs.notes }} body: ${{ steps.release_notes.outputs.notes }}
draft: false draft: false
prerelease: ${{ contains(github.ref, 'b') }} prerelease: ${{ contains(github.ref, 'b') }}
generate_release_notes: false # We provide our own generate_release_notes: false # We provide our own
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -8,14 +8,14 @@ on:
branches: branches:
- main - main
paths-ignore: paths-ignore:
- 'docs/**' - "docs/**"
- '.github/workflows/docusaurus.yml' - ".github/workflows/docusaurus.yml"
pull_request: pull_request:
branches: branches:
- main - main
paths-ignore: paths-ignore:
- 'docs/**' - "docs/**"
- '.github/workflows/docusaurus.yml' - ".github/workflows/docusaurus.yml"
permissions: {} permissions: {}

View file

@ -1,9 +1,20 @@
{ {
"default": true, "default": true,
"MD004": false,
"MD013": false, "MD013": false,
"MD033": false, "MD036": false,
"MD041": false, "MD041": false,
"no-inline-html": false, "no-trailing-punctuation": false,
"line-length": false, "no-inline-html": {
"first-line-heading": false "allowed_elements": ["br", "details", "summary", "img", "a", "kbd"]
},
"code-block-style": {
"style": "fenced"
},
"emphasis-style": {
"style": "underscore"
},
"strong-style": {
"style": "asterisk"
}
} }

View file

@ -11,6 +11,12 @@ __pycache__/
env/ env/
venv/ venv/
# Ignore compiled YAML or generated docs # Ignore local HA dev instance config (not production code)
*.yaml config/
*.yml
# Ignore YAML schemas (structural files with specific formatting conventions)
schemas/yaml/
# Ignore Docusaurus documentation sites they have their own toolchain
# and Prettier reformats <details> blocks inside lists in a way that breaks MDX
docs/

37
.prettierrc.yaml Normal file
View file

@ -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)

660
AGENTS.md
View file

@ -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 - **This file** (`AGENTS.md`): AI/Developer long-term memory, patterns, conventions
- **`docs/user/`**: Docusaurus site for end-users (installation, configuration, usage examples) - **`docs/user/`**: Docusaurus site for end-users (installation, configuration, usage examples)
- Markdown files in `docs/user/docs/*.md` - Markdown files in `docs/user/docs/*.md`
- Navigation managed via `docs/user/sidebars.ts` - Navigation managed via `docs/user/sidebars.ts`
- **`docs/developer/`**: Docusaurus site for contributors (architecture, development guides) - **`docs/developer/`**: Docusaurus site for contributors (architecture, development guides)
- Markdown files in `docs/developer/docs/*.md` - Markdown files in `docs/developer/docs/*.md`
- Navigation managed via `docs/developer/sidebars.ts` - Navigation managed via `docs/developer/sidebars.ts`
- **`README.md`**: Project overview with links to documentation sites - **`README.md`**: Project overview with links to documentation sites
**Automatic Inconsistency Detection:** **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:** **When to discuss in chat vs. direct file changes:**
- **Make direct changes when:** - **Make direct changes when:**
- Clear, straightforward task (fix bug, add function, update config) - Clear, straightforward task (fix bug, add function, update config)
- Single approach is obvious - Single approach is obvious
- User request is specific ("add X", "change Y to Z") - User request is specific ("add X", "change Y to Z")
- Quick iteration is needed (user can review diff and iterate) - Quick iteration is needed (user can review diff and iterate)
- **Discuss/show in chat first when:** - **Discuss/show in chat first when:**
- Multiple valid approaches exist (architectural decision) - Multiple valid approaches exist (architectural decision)
- Significant refactoring affecting many files - Significant refactoring affecting many files
- Unclear requirements need clarification - Unclear requirements need clarification
- Trade-offs need discussion (performance vs. readability, etc.) - Trade-offs need discussion (performance vs. readability, etc.)
- User asks open-ended question ("how should we...", "what's the best way...") - 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. **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:** **Planning Document Lifecycle:**
1. **Planning Phase** (WIP in `/planning/`) 1. **Planning Phase** (WIP in `/planning/`)
- Create `planning/<feature>-refactoring-plan.md` - Create `planning/<feature>-refactoring-plan.md`
- Iterate freely (git-ignored, no commit pressure) - Iterate freely (git-ignored, no commit pressure)
- AI can help refine without polluting git history - AI can help refine without polluting git history
- Multiple revisions until plan is solid - Multiple revisions until plan is solid
2. **Implementation Phase** (Active work) 2. **Implementation Phase** (Active work)
- Use plan as reference during coding - Use plan as reference during coding
- Update plan if issues discovered - Update plan if issues discovered
- Track progress through phases - Track progress through phases
- Test after each phase - Test after each phase
3. **Completion Phase** (After implementation) 3. **Completion Phase** (After implementation)
- **Option A**: Move to `docs/development/` if lasting value - **Option A**: Move to `docs/development/` if lasting value
- Example: `planning/module-splitting-plan.md``docs/development/module-splitting-plan.md` - Example: `planning/module-splitting-plan.md``docs/development/module-splitting-plan.md`
- Update status to "✅ COMPLETED" - Update status to "✅ COMPLETED"
- Commit as historical reference - Commit as historical reference
- **Option B**: Delete if superseded - **Option B**: Delete if superseded
- Plan served its purpose - Plan served its purpose
- Code and AGENTS.md are source of truth - Code and AGENTS.md are source of truth
- **Option C**: Archive in `planning/archive/` - **Option C**: Archive in `planning/archive/`
- Keep locally for "why we didn't do X" reference - Keep locally for "why we didn't do X" reference
- Don't commit (git-ignored) - Don't commit (git-ignored)
**Required Planning Document Sections:** **Required Planning Document Sections:**
@ -320,46 +320,46 @@ After successful refactoring:
**Organized Packages:** **Organized Packages:**
1. **`/utils/`** - Pure data transformation functions (stateless) 1. **`/utils/`** - Pure data transformation functions (stateless)
- `average.py` - Average and time-window calculations - `average.py` - Average and time-window calculations
- `price.py` - Price enrichment, volatility, rating calculations - `price.py` - Price enrichment, volatility, rating calculations
- **Pattern**: Import as `from ..utils.average import function_name` - **Pattern**: Import as `from ..utils.average import function_name`
2. **`/entity_utils/`** - Entity-specific utilities 2. **`/entity_utils/`** - Entity-specific utilities
- `icons.py` - Dynamic icon selection logic - `icons.py` - Dynamic icon selection logic
- `colors.py` - Icon color mapping - `colors.py` - Icon color mapping
- `attributes.py` - Common attribute builders - `attributes.py` - Common attribute builders
- **Pattern**: Import as `from ..entity_utils import function_name` - **Pattern**: Import as `from ..entity_utils import function_name`
3. **`/coordinator/`** - DataUpdateCoordinator and related logic 3. **`/coordinator/`** - DataUpdateCoordinator and related logic
- `core.py` - Main coordinator class - `core.py` - Main coordinator class
- `cache.py` - Persistent storage handling - `cache.py` - Persistent storage handling
- `data_transformation.py` - Raw data → enriched data - `data_transformation.py` - Raw data → enriched data
- `period_handlers/` - Period calculation sub-package - `period_handlers/` - Period calculation sub-package
- **Pattern**: Coordinator-specific implementations - **Pattern**: Coordinator-specific implementations
4. **`/sensor/`** - Sensor platform package 4. **`/sensor/`** - Sensor platform package
- `core.py` - Entity class (1,268 lines - manages 80+ sensor types) - `core.py` - Entity class (1,268 lines - manages 80+ sensor types)
- `definitions.py` - Entity descriptions - `definitions.py` - Entity descriptions
- `helpers.py` - Sensor-specific helpers - `helpers.py` - Sensor-specific helpers
- `calculators/` - Value calculation package (8 specialized calculators, 1,838 lines) - `calculators/` - Value calculation package (8 specialized calculators, 1,838 lines)
- `attributes/` - Attribute builders package (8 specialized modules, 1,209 lines) - `attributes/` - Attribute builders package (8 specialized modules, 1,209 lines)
- **Pattern**: Calculator Pattern (business logic separated from presentation) - **Pattern**: Calculator Pattern (business logic separated from presentation)
- **Architecture**: Two-tier (Calculators handle computation → Attributes handle state presentation) - **Architecture**: Two-tier (Calculators handle computation → Attributes handle state presentation)
5. **`/binary_sensor/`** - Binary sensor platform package 5. **`/binary_sensor/`** - Binary sensor platform package
- Same structure as `/sensor/` - Same structure as `/sensor/`
6. **`/config_flow_handlers/`** - Configuration flow package 6. **`/config_flow_handlers/`** - Configuration flow package
- `user_flow.py` - Initial setup flow - `user_flow.py` - Initial setup flow
- `subentry_flow.py` - Add additional homes - `subentry_flow.py` - Add additional homes
- `options_flow.py` - Reconfiguration - `options_flow.py` - Reconfiguration
- `schemas.py` - Form schemas - `schemas.py` - Form schemas
- `validators.py` - Input validation - `validators.py` - Input validation
7. **`/api/`** - External API communication 7. **`/api/`** - External API communication
- `client.py` - GraphQL client - `client.py` - GraphQL client
- `queries.py` - Query definitions - `queries.py` - Query definitions
- `exceptions.py` - API-specific exceptions - `exceptions.py` - API-specific exceptions
**When Adding New Files:** **When Adding New Files:**
@ -386,74 +386,74 @@ After successful refactoring:
**Key Patterns:** **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). - **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:** **CRITICAL Rules:**
- When using `translation_key`, pass options as **plain string list**, NOT `SelectOptionDict` - When using `translation_key`, pass options as **plain string list**, NOT `SelectOptionDict`
- Selector option keys MUST be lowercase: `[a-z0-9-_]+` pattern (Hassfest validation) - Selector option keys MUST be lowercase: `[a-z0-9-_]+` pattern (Hassfest validation)
- Label parameter overrides translations (avoid when using translation_key) - Label parameter overrides translations (avoid when using translation_key)
- Use `SelectOptionDict` ONLY for dynamic/non-translatable options (no 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. - **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: - **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) - **Calculator Package** (`sensor/calculators/`): 8 specialized calculators handle business logic (1,838 lines total)
- `base.py` - Abstract BaseCalculator with coordinator access - `base.py` - Abstract BaseCalculator with coordinator access
- `interval.py` - Single interval calculations (current/next/previous) - `interval.py` - Single interval calculations (current/next/previous)
- `rolling_hour.py` - 5-interval rolling windows - `rolling_hour.py` - 5-interval rolling windows
- `daily_stat.py` - Calendar day min/max/avg statistics - `daily_stat.py` - Calendar day min/max/avg statistics
- `window_24h.py` - Trailing/leading 24h windows - `window_24h.py` - Trailing/leading 24h windows
- `volatility.py` - Price volatility analysis - `volatility.py` - Price volatility analysis
- `trend.py` - Complex trend analysis with caching (640 lines) - `trend.py` - Complex trend analysis with caching (640 lines)
- `timing.py` - Best/peak price period timing - `timing.py` - Best/peak price period timing
- `metadata.py` - Home/metering metadata - `metadata.py` - Home/metering metadata
- **Attributes Package** (`sensor/attributes/`): 8 specialized modules handle state presentation (1,209 lines total) - **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` - 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`) - `__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 - **Core Entity** (`sensor/core.py`): 1,268 lines managing 80+ sensor types
- Instantiates all calculators in `__init__` - Instantiates all calculators in `__init__`
- Delegates value calculations to appropriate calculator - 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()` - 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 - 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 - **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 - **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. - **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`. - **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: - **Intelligent caching strategy**: Minimizes API calls while ensuring data freshness:
- User data cached for 24h (rarely changes) - User data cached for 24h (rarely changes)
- Price data validated against calendar day - cleared on midnight turnover to force fresh fetch - Price data validated against calendar day - cleared on midnight turnover to force fresh fetch
- Cache survives HA restarts via `Store` persistence - Cache survives HA restarts via `Store` persistence
- API polling intensifies only when tomorrow's data expected (afternoons) - 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 - Stale cache detection via `_is_cache_valid()` prevents using yesterday's data as today's
**Multi-Layer Caching (Performance Optimization)**: **Multi-Layer Caching (Performance Optimization)**:
The integration uses **4 distinct caching layers** with automatic invalidation: The integration uses **4 distinct caching layers** with automatic invalidation:
1. **Persistent API Cache** (`coordinator/cache.py` → HA Storage): 1. **Persistent API Cache** (`coordinator/cache.py` → HA Storage):
- **What**: Raw price/user data from Tibber API (~50KB) - **What**: Raw price/user data from Tibber API (~50KB)
- **Lifetime**: Until midnight (price) or 24h (user) - **Lifetime**: Until midnight (price) or 24h (user)
- **Invalidation**: Automatic at 00:00 local, cache validation on load - **Invalidation**: Automatic at 00:00 local, cache validation on load
- **Why**: Reduce API calls from every 15min to once per day, survive HA restarts - **Why**: Reduce API calls from every 15min to once per day, survive HA restarts
2. **Translation Cache** (`const.py` → in-memory dicts): 2. **Translation Cache** (`const.py` → in-memory dicts):
- **What**: UI strings, entity descriptions (~5KB) - **What**: UI strings, entity descriptions (~5KB)
- **Lifetime**: Forever (until HA restart) - **Lifetime**: Forever (until HA restart)
- **Invalidation**: Never (read-only after startup load) - **Invalidation**: Never (read-only after startup load)
- **Why**: Avoid file I/O on every entity attribute access (15+ times/hour) - **Why**: Avoid file I/O on every entity attribute access (15+ times/hour)
3. **Config Dictionary Cache** (`coordinator/` modules): 3. **Config Dictionary Cache** (`coordinator/` modules):
- **What**: Parsed options dict (~1KB per module) - **What**: Parsed options dict (~1KB per module)
- **Lifetime**: Until `config_entry.options` change - **Lifetime**: Until `config_entry.options` change
- **Invalidation**: Explicit via `invalidate_config_cache()` on options update - **Invalidation**: Explicit via `invalidate_config_cache()` on options update
- **Why**: Avoid ~30-40 `options.get()` calls per coordinator update (98% time saving) - **Why**: Avoid ~30-40 `options.get()` calls per coordinator update (98% time saving)
4. **Period Calculation Cache** (`coordinator/periods.py`): 4. **Period Calculation Cache** (`coordinator/periods.py`):
- **What**: Calculated best/peak price periods (~10KB) - **What**: Calculated best/peak price periods (~10KB)
- **Lifetime**: Until price data or config changes - **Lifetime**: Until price data or config changes
- **Invalidation**: Automatic via hash comparison of inputs (timestamps + rating_levels + config) - **Invalidation**: Automatic via hash comparison of inputs (timestamps + rating_levels + config)
- **Why**: Avoid expensive calculation (~100-500ms) when data unchanged (70% CPU saving) - **Why**: Avoid expensive calculation (~100-500ms) when data unchanged (70% CPU saving)
**Cache Invalidation Coordination**: **Cache Invalidation Coordination**:
@ -565,8 +565,8 @@ sensor/helpers/ ✗ (NO imports from calculators/)
1. **Calculator** computes value AND builds attribute dict 1. **Calculator** computes value AND builds attribute dict
2. **Core** stores attributes in `cached_data` dict 2. **Core** stores attributes in `cached_data` dict
3. **Attributes package** retrieves cached attributes via: 3. **Attributes package** retrieves cached attributes via:
- `_add_cached_trend_attributes()` for trend sensors - `_add_cached_trend_attributes()` for trend sensors
- `_add_timing_or_volatility_attributes()` for volatility sensors - `_add_timing_or_volatility_attributes()` for volatility sensors
**Example (Volatility):** **Example (Volatility):**
@ -671,25 +671,25 @@ Example: daily_min=10 ct, daily_avg=20 ct, flex=50%, min_distance=5%
**Solutions Implemented (Nov 2025):** **Solutions Implemented (Nov 2025):**
1. **Hard Caps on Flex** (`coordinator/period_handlers/core.py`): 1. **Hard Caps on Flex** (`coordinator/period_handlers/core.py`):
- `MAX_SAFE_FLEX = 0.50` (50% overall maximum) - `MAX_SAFE_FLEX = 0.50` (50% overall maximum)
- `MAX_OUTLIER_FLEX = 0.25` (25% for price spike detection) - `MAX_OUTLIER_FLEX = 0.25` (25% for price spike detection)
- Warns users when base flex exceeds thresholds (INFO at 25%, WARNING at 30%) - Warns users when base flex exceeds thresholds (INFO at 25%, WARNING at 30%)
2. **Relaxation Increment Cap** (`coordinator/period_handlers/relaxation.py`): 2. **Relaxation Increment Cap** (`coordinator/period_handlers/relaxation.py`):
- Maximum 3% increment per relaxation step (prevents explosion from high base flex) - Maximum 3% increment per relaxation step (prevents explosion from high base flex)
- Example: Base flex 40% → increments as 43%, 46%, 49% (capped at 50%) - Example: Base flex 40% → increments as 43%, 46%, 49% (capped at 50%)
- Without cap: 40% × 1.25 = 50% step → reaches 100% in 6 steps - Without cap: 40% × 1.25 = 50% step → reaches 100% in 6 steps
3. **Dynamic Min_Distance Scaling** (`coordinator/period_handlers/level_filtering.py`): 3. **Dynamic Min_Distance Scaling** (`coordinator/period_handlers/level_filtering.py`):
- Reduces min_distance proportionally as flex increases above 20% - Reduces min_distance proportionally as flex increases above 20%
- Formula: `scale_factor = max(0.25, 1.0 - ((flex - 0.20) × 2.5))` - Formula: `scale_factor = max(0.25, 1.0 - ((flex - 0.20) × 2.5))`
- Example: flex=30% → scale=0.75 → min_distance reduced by 25% - Example: flex=30% → scale=0.75 → min_distance reduced by 25%
- Minimum scaling: 25% of original (prevents complete removal) - Minimum scaling: 25% of original (prevents complete removal)
4. **Enhanced Debug Logging** (`coordinator/period_handlers/period_building.py`): 4. **Enhanced Debug Logging** (`coordinator/period_handlers/period_building.py`):
- Tracks exact counts of intervals filtered by flex, min_distance, and level - Tracks exact counts of intervals filtered by flex, min_distance, and level
- Shows which filter blocked the most candidates - Shows which filter blocked the most candidates
- Enables diagnosis of configuration issues - Enables diagnosis of configuration issues
**Configuration Guidance:** **Configuration Guidance:**
@ -777,15 +777,15 @@ When debugging period calculation issues:
- Uses `uv` (modern, fast Python package manager) - Uses `uv` (modern, fast Python package manager)
- **Always use `uv` commands**, not `pip` directly: - **Always use `uv` commands**, not `pip` directly:
```bash ```bash
# ✅ Correct # ✅ Correct
uv pip install <package> uv pip install <package>
uv run pytest uv run pytest
# ❌ Wrong - uses system Python # ❌ Wrong - uses system Python
pip install <package> pip install <package>
python -m pytest python -m pytest
``` ```
**Development Scripts:** **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) 1. **Prefer development scripts** (they handle .venv automatically)
2. **If using Python directly**, use `.venv/bin/python` explicitly: 2. **If using Python directly**, use `.venv/bin/python` explicitly:
```bash ```bash
.venv/bin/python -m pytest tests/ .venv/bin/python -m pytest tests/
.venv/bin/python -c "import homeassistant; print('OK')" .venv/bin/python -c "import homeassistant; print('OK')"
``` ```
3. **For package management**, always use `uv`: 3. **For package management**, always use `uv`:
```bash ```bash
uv pip list uv pip list
uv pip install --upgrade homeassistant uv pip install --upgrade homeassistant
``` ```
**Debugging Environment Issues:** **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"` 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") 2. **If NOT in container**: Suggest opening project in DevContainer (VS Code: "Reopen in Container")
3. **Why it matters**: 3. **Why it matters**:
- `.venv` is located outside workspace (hardlink issues on bind-mounts) - `.venv` is located outside workspace (hardlink issues on bind-mounts)
- Development scripts expect container environment - Development scripts expect container environment
- VS Code Python settings are container-specific - VS Code Python settings are container-specific
**If user insists on local development without container**, warn that: **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 1. **Ask user to test**, don't execute `./scripts/develop` yourself
2. **Provide specific test guidance** based on what changed in this session: 2. **Provide specific test guidance** based on what changed in this session:
- Which UI screens to check (e.g., "Open config flow, step 3") - Which UI screens to check (e.g., "Open config flow, step 3")
- What behavior to verify (e.g., "Dropdown should show translated values") - 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 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) - What NOT to test (if change is isolated, no need to test everything)
3. **Keep test guidance concise** - 3-5 bullet points max 3. **Keep test guidance concise** - 3-5 bullet points max
4. **Focus on session changes only** - Don't suggest testing unrelated features 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:** **Internal/unreleased fixes:**
- If a fix never affected released users, mark commit body with one trailer so release notes can exclude it: - If a fix never affected released users, mark commit body with one trailer so release notes can exclude it:
- `Release-Notes: skip` - `Release-Notes: skip`
- `User-Impact: none` - `User-Impact: none`
- `Released-Bug: no` - `Released-Bug: no`
- To check if introducing code was released, use: `./scripts/release/check-if-released <commit-hash>` - To check if introducing code was released, use: `./scripts/release/check-if-released <commit-hash>`
### Release Notes Generation ### Release Notes Generation
@ -986,54 +986,54 @@ When changes are complete and ready for testing:
**Multiple Options Available:** **Multiple Options Available:**
1. **Helper Script** (recommended, foolproof) 1. **Helper Script** (recommended, foolproof)
- Script: `./scripts/release/prepare VERSION` - Script: `./scripts/release/prepare VERSION`
- Bumps manifest.json version → commits → creates tag locally - Bumps manifest.json version → commits → creates tag locally
- You review and push when ready - You review and push when ready
- Example: `./scripts/release/prepare 0.3.0` - Example: `./scripts/release/prepare 0.3.0`
2. **Auto-Tag Workflow** (safety net) 2. **Auto-Tag Workflow** (safety net)
- Workflow: `.github/workflows/auto-tag.yml` - Workflow: `.github/workflows/auto-tag.yml`
- Triggers on manifest.json changes - Triggers on manifest.json changes
- Automatically creates tag if it doesn't exist - Automatically creates tag if it doesn't exist
- Prevents "forgot to tag" mistakes - Prevents "forgot to tag" mistakes
3. **Local Script** (testing, preview, and updating releases) 3. **Local Script** (testing, preview, and updating releases)
- Script: `./scripts/release/generate-notes [FROM_TAG] [TO_TAG]` - Script: `./scripts/release/generate-notes [FROM_TAG] [TO_TAG]`
- Parses Conventional Commits between tags - Parses Conventional Commits between tags
- Supports multiple backends (auto-detected): - Supports multiple backends (auto-detected):
- **AI-powered**: GitHub Copilot CLI (best, context-aware) - **AI-powered**: GitHub Copilot CLI (best, context-aware)
- **Template-based**: git-cliff (fast, reliable) - **Template-based**: git-cliff (fast, reliable)
- **Manual**: grep/awk fallback (always works) - **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) - **Auto-update feature**: If a GitHub release exists for TO_TAG, automatically offers to update release notes (interactive prompt)
**Usage examples:** **Usage examples:**
```bash ```bash
# Generate and preview notes # Generate and preview notes
./scripts/release/generate-notes v0.2.0 v0.3.0 ./scripts/release/generate-notes v0.2.0 v0.3.0
# If release exists, you'll see: # If release exists, you'll see:
# → Generated release notes # → Generated release notes
# → Detection: "A GitHub release exists for v0.3.0" # → Detection: "A GitHub release exists for v0.3.0"
# → Prompt: "Do you want to update the release notes on GitHub? [y/N]" # → Prompt: "Do you want to update the release notes on GitHub? [y/N]"
# → Answer 'y' to auto-update, 'n' to skip # → Answer 'y' to auto-update, 'n' to skip
# Force specific backend # Force specific backend
RELEASE_NOTES_BACKEND=copilot ./scripts/release/generate-notes v0.2.0 v0.3.0 RELEASE_NOTES_BACKEND=copilot ./scripts/release/generate-notes v0.2.0 v0.3.0
``` ```
4. **GitHub UI Button** (manual, PR-based) 4. **GitHub UI Button** (manual, PR-based)
- Uses `.github/release.yml` configuration - Uses `.github/release.yml` configuration
- Click "Generate release notes" when creating release - Click "Generate release notes" when creating release
- Works best with PRs that have labels - Works best with PRs that have labels
- Direct commits appear in "Other Changes" category - Direct commits appear in "Other Changes" category
5. **CI/CD Automation** (automatic on tag push) 5. **CI/CD Automation** (automatic on tag push)
- Workflow: `.github/workflows/release.yml` - Workflow: `.github/workflows/release.yml`
- Triggers on version tags (v1.0.0, v2.1.3, etc.) - Triggers on version tags (v1.0.0, v2.1.3, etc.)
- Uses git-cliff backend (AI disabled in CI) - Uses git-cliff backend (AI disabled in CI)
- Filters out version bump commits automatically - Filters out version bump commits automatically
- Creates GitHub release automatically - Creates GitHub release automatically
**Recommended Release Workflow:** **Recommended Release Workflow:**
@ -1087,13 +1087,13 @@ If you want better release notes after the automated release:
**Semantic Versioning Rules:** **Semantic Versioning Rules:**
- **Pre-1.0 (0.x.y)**: - **Pre-1.0 (0.x.y)**:
- Breaking changes → bump MINOR (0.x.0) - Breaking changes → bump MINOR (0.x.0)
- New features → bump MINOR (0.x.0) - New features → bump MINOR (0.x.0)
- Bug fixes → bump PATCH (0.0.x) - Bug fixes → bump PATCH (0.0.x)
- **Post-1.0 (x.y.z)**: - **Post-1.0 (x.y.z)**:
- Breaking changes → bump MAJOR (x.0.0) - Breaking changes → bump MAJOR (x.0.0)
- New features → bump MINOR (0.x.0) - New features → bump MINOR (0.x.0)
- Bug fixes → bump PATCH (0.0.x) - Bug fixes → bump PATCH (0.0.x)
**Alternative: Manual Bump (with Auto-Tag Safety Net):** **Alternative: Manual Bump (with Auto-Tag Safety Net):**
@ -1135,25 +1135,25 @@ USE_AI=false ./scripts/release/generate-notes
**Backend Comparison:** **Backend Comparison:**
- **GitHub Copilot CLI** (`copilot`): - **GitHub Copilot CLI** (`copilot`):
- ✅ AI-powered semantic understanding - ✅ AI-powered semantic understanding
- ✅ Smart grouping of related commits into single release notes - ✅ Smart grouping of related commits into single release notes
- ✅ Interprets "Impact:" sections for user-friendly descriptions - ✅ Interprets "Impact:" sections for user-friendly descriptions
- ✅ Multiple commits can be combined with all links: ([hash1](url1), [hash2](url2)) - ✅ Multiple commits can be combined with all links: ([hash1](url1), [hash2](url2))
- ⚠️ Uses premium request quota - ⚠️ Uses premium request quota
- ⚠️ Output may vary between runs - ⚠️ Output may vary between runs
- **git-cliff** (template-based): - **git-cliff** (template-based):
- ✅ Fast and consistent - ✅ Fast and consistent
- ✅ 1:1 commit to release note line mapping - ✅ 1:1 commit to release note line mapping
- ✅ Highly configurable via `cliff.toml` - ✅ Highly configurable via `cliff.toml`
- ❌ No semantic understanding - ❌ No semantic understanding
- ❌ Cannot intelligently group related commits - ❌ Cannot intelligently group related commits
- **manual** (grep/awk): - **manual** (grep/awk):
- ✅ Always available (no dependencies) - ✅ Always available (no dependencies)
- ✅ Basic commit categorization - ✅ Basic commit categorization
- ❌ No commit grouping - ❌ No commit grouping
- ❌ Basic formatting only - ❌ Basic formatting only
**Output Format:** **Output Format:**
@ -1469,14 +1469,14 @@ Calling `ruff` or `uv run ruff` directly can cause unintended side effects:
1. Run your custom `ruff` command 1. Run your custom `ruff` command
2. **Immediately after**, clean up any installation artifacts: 2. **Immediately after**, clean up any installation artifacts:
```bash ```bash
# Use our cleanup script (uses both pip and uv pip for compatibility) # Use our cleanup script (uses both pip and uv pip for compatibility)
./scripts/clean --minimal ./scripts/clean --minimal
# Or manually: # Or manually:
pip uninstall -y tibber_prices 2>/dev/null || true pip uninstall -y tibber_prices 2>/dev/null || true
uv pip uninstall tibber_prices 2>/dev/null || true uv pip uninstall tibber_prices 2>/dev/null || true
``` ```
3. Ask user to restart HA: `./scripts/develop` 3. Ask user to restart HA: `./scripts/develop`
@ -1710,24 +1710,24 @@ We use **Pyright** for static type checking:
**Log Level Strategy:** **Log Level Strategy:**
- **INFO Level** - User-facing results and high-level progress: - **INFO Level** - User-facing results and high-level progress:
- Compact 1-line summaries (no multi-line blocks) - Compact 1-line summaries (no multi-line blocks)
- Important results only (success/failure outcomes) - Important results only (success/failure outcomes)
- No indentation (scannability) - No indentation (scannability)
- Example: `"Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0%"` - 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)"` - Example: `"Day 2025-11-11: Success after 1 relaxation phase (2 periods)"`
- **DEBUG Level** - Detailed execution trace: - **DEBUG Level** - Detailed execution trace:
- Full context headers with all relevant configuration - Full context headers with all relevant configuration
- Step-by-step progression through logic - Step-by-step progression through logic
- Hierarchical indentation to show call depth/logic structure - Hierarchical indentation to show call depth/logic structure
- Intermediate results and calculations - Intermediate results and calculations
- Example: `" Day 2025-11-11: Found 1 baseline period (need 2)"` - Example: `" Day 2025-11-11: Found 1 baseline period (need 2)"`
- Example: `" Phase 1: flex 20.25% + original filters"` - Example: `" Phase 1: flex 20.25% + original filters"`
- **WARNING Level** - Problems and unexpected states: - **WARNING Level** - Problems and unexpected states:
- Top-level important messages (no indentation) - Top-level important messages (no indentation)
- Clear indication of what went wrong - Clear indication of what went wrong
- Example: `"Day 2025-11-11: All relaxation phases exhausted, still only 1 period found"` - Example: `"Day 2025-11-11: All relaxation phases exhausted, still only 1 period found"`
**Hierarchical Indentation Pattern:** **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 - **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 <commit-hash>` to verify if code is in any `v*.*.*` tag - **Check if released**: Use `./scripts/release/check-if-released <commit-hash>` 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`: - **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 - ✓ NOT RELEASED → No migration needed, just use new code
- ✗ ALREADY RELEASED → Migration may be needed for users upgrading from that version - ✗ 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 - **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. - **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:** **Examples and use cases:**
- **Regional context**: Tibber operates primarily in European markets (Norway, Sweden, Germany, Netherlands). Examples should reflect European context: - **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 - ✅ 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) - ✅ 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) - ✅ 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 - ✅ 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-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) - ❌ 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: - **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 - 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) - 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 - Heat pumps: Primary heating source (not just cooling), complex defrost cycles, weather-dependent operation
- **Units and formats**: Use European conventions where appropriate: - **Units and formats**: Use European conventions where appropriate:
- Prices: ct/kWh or øre/kWh (as provided by Tibber API) - Prices: ct/kWh or øre/kWh (as provided by Tibber API)
- Time: 24-hour format (00:00-23:59) - Time: 24-hour format (00:00-23:59)
- Dates: ISO 8601 format (YYYY-MM-DD) - Dates: ISO 8601 format (YYYY-MM-DD)
**Language style and tone:** **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...". - **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". - **Imperative mood**: Use direct imperatives for instructions: "Configure the integration" not "You should configure the integration".
- **Language-specific notes**: - **Language-specific notes**:
- German: Use "du" (informal) and gender-neutral imperatives (e.g., "Konfiguriere" instead of "Konfigurieren Sie") - 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) - 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) - Swedish/Norwegian: Already use informal address by default (no formal "Ni"/"De" in modern usage)
- English: Already gender-neutral and appropriately informal - English: Already gender-neutral and appropriately informal
**User Documentation Quality:** **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: 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 - **Clarity over completeness**: Users want to understand concepts, not read technical specifications
- ✅ Good: "Relaxation automatically loosens filters until enough periods are found" - ✅ Good: "Relaxation automatically loosens filters until enough periods are found"
- ❌ Bad: "The relaxation algorithm implements a 4×4 matrix strategy with multiplicative flex increments" - ❌ 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 - **Visual examples**: Use timeline diagrams, code blocks with comments, before/after comparisons
- ✅ Show what a "period" looks like on a 24-hour timeline - ✅ Show what a "period" looks like on a 24-hour timeline
- ✅ Include automation examples with real entity names - ✅ Include automation examples with real entity names
- **Use-case driven**: Start with "what can I do with this?" not "how does it work internally" - **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 - ✅ Structure: Quick Start → Common Scenarios → Configuration Guide → Advanced Topics
- ❌ Avoid: Starting with mathematical formulas or algorithm descriptions - ❌ Avoid: Starting with mathematical formulas or algorithm descriptions
- **Practical troubleshooting**: Address real problems users encounter - **Practical troubleshooting**: Address real problems users encounter
- ✅ "No periods found → Try: increase flex from 15% to 20%" - ✅ "No periods found → Try: increase flex from 15% to 20%"
- ❌ Avoid: Generic "check your configuration" without specific guidance - ❌ Avoid: Generic "check your configuration" without specific guidance
- **Progressive disclosure**: Basic concepts first, advanced details later - **Progressive disclosure**: Basic concepts first, advanced details later
- ✅ Main doc covers 80% use cases in simple terms - ✅ Main doc covers 80% use cases in simple terms
- ✅ Link to advanced/technical docs for edge cases - ✅ Link to advanced/technical docs for edge cases
- ❌ Don't mix basic explanations with deep technical details - ❌ Don't mix basic explanations with deep technical details
- **When code changed significantly**: Verify documentation still matches - **When code changed significantly**: Verify documentation still matches
- If relaxation strategy changed from 3 phases to 4×4 matrix → documentation MUST reflect this - 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 metadata format changed → update all examples showing attributes
- If per-day independence was added → explain why some days relax differently - If per-day independence was added → explain why some days relax differently
**Documentation Writing Strategy:** **Documentation Writing Strategy:**
Understanding **how** good documentation emerges is as important as knowing what makes it good: Understanding **how** good documentation emerges is as important as knowing what makes it good:
- **Live Understanding vs. Code Analysis** - **Live Understanding vs. Code Analysis**
- ✅ **DO:** Write docs during/after active development - ✅ **DO:** Write docs during/after active development
- When implementing complex logic, document it while the "why" is fresh - When implementing complex logic, document it while the "why" is fresh
- Use real examples from debugging sessions (actual logs, real data) - Use real examples from debugging sessions (actual logs, real data)
- Document decisions as they're made, not after the fact - Document decisions as they're made, not after the fact
- ❌ **DON'T:** Write docs from cold code analysis - ❌ **DON'T:** Write docs from cold code analysis
- Reading code shows "what", not "why" - Reading code shows "what", not "why"
- Missing context: Which alternatives were considered? - Missing context: Which alternatives were considered?
- No user perspective: What's actually confusing? - No user perspective: What's actually confusing?
- **User Feedback Loop** - **User Feedback Loop**
- Key insight: Documentation improves when users question it - Key insight: Documentation improves when users question it
- Pattern: - Pattern:
1. User asks: "Does this still match the code?" 1. User asks: "Does this still match the code?"
2. AI realizes: "Oh, the 3-phase model is outdated" 2. AI realizes: "Oh, the 3-phase model is outdated"
3. Together we trace through real behavior 3. Together we trace through real behavior
4. Documentation gets rewritten with correct mental model 4. Documentation gets rewritten with correct mental model
- Why it works: User questions force critical thinking, real confusion points get addressed - Why it works: User questions force critical thinking, real confusion points get addressed
- **Log-Driven Documentation** - **Log-Driven Documentation**
- Observation: When logs explain logic clearly, documentation becomes easier - 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 - 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 - Pattern: If you spent hours making logs clear → use that clarity in documentation too
- **Concrete Examples > Abstract Descriptions** - **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)" - ✅ **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" - ❌ **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 - Use real data from debug sessions, show actual attribute values, demonstrate with timeline diagrams
- **Context Accumulation in Long Sessions** - **Context Accumulation in Long Sessions**
- Advantage: AI builds mental model incrementally, sees evolution of logic (not just final state), understands trade-offs - 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 - 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 - Lesson: Complex documentation benefits from focused, uninterrupted work with accumulated context
- **Document the "Why", Not Just the "What"** - **Document the "Why", Not Just the "What"**
- Every complex pattern should answer: - Every complex pattern should answer:
1. **What** does it do? (quick summary) 1. **What** does it do? (quick summary)
2. **Why** was it designed this way? (alternatives considered) 2. **Why** was it designed this way? (alternatives considered)
3. **How** does a user benefit? (practical impact) 3. **How** does a user benefit? (practical impact)
4. **When** does it fail? (known limitations) 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." - 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 ## 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: 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: 1. **Determine calculation pattern** - Choose which group your sensor belongs to:
- **Interval-based**: Uses time offset from current interval (e.g., current/next/previous) - **Interval-based**: Uses time offset from current interval (e.g., current/next/previous)
- **Rolling hour**: Aggregates 5-interval window (2 before + center + 2 after) - **Rolling hour**: Aggregates 5-interval window (2 before + center + 2 after)
- **Daily statistics**: Min/max/avg within calendar day boundaries - **Daily statistics**: Min/max/avg within calendar day boundaries
- **24h windows**: Trailing/leading from current interval - **24h windows**: Trailing/leading from current interval
- **Future forecast**: N-hour windows starting from next interval - **Future forecast**: N-hour windows starting from next interval
- **Volatility**: Statistical analysis of price variation - **Volatility**: Statistical analysis of price variation
- **Diagnostic**: System information and metadata - **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. **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`: 2. **Add entity description** to appropriate sensor group in `sensor/definitions.py`:
- `INTERVAL_PRICE_SENSORS`, `INTERVAL_LEVEL_SENSORS`, or `INTERVAL_RATING_SENSORS` - `INTERVAL_PRICE_SENSORS`, `INTERVAL_LEVEL_SENSORS`, or `INTERVAL_RATING_SENSORS`
- `ROLLING_HOUR_PRICE_SENSORS`, `ROLLING_HOUR_LEVEL_SENSORS`, or `ROLLING_HOUR_RATING_SENSORS` - `ROLLING_HOUR_PRICE_SENSORS`, `ROLLING_HOUR_LEVEL_SENSORS`, or `ROLLING_HOUR_RATING_SENSORS`
- `DAILY_STAT_SENSORS` - `DAILY_STAT_SENSORS`
- `WINDOW_24H_SENSORS` - `WINDOW_24H_SENSORS`
- `FUTURE_AVG_SENSORS` or `FUTURE_TREND_SENSORS` - `FUTURE_AVG_SENSORS` or `FUTURE_TREND_SENSORS`
- `VOLATILITY_SENSORS` - `VOLATILITY_SENSORS`
- `DIAGNOSTIC_SENSORS` - `DIAGNOSTIC_SENSORS`
3. **Add handler mapping** in `sensor/core.py``_get_value_getter()` method: 3. **Add handler mapping** in `sensor/core.py``_get_value_getter()` method:
- For interval-based: Use `_get_interval_value(interval_offset, value_type)` - For interval-based: Use `_get_interval_value(interval_offset, value_type)`
- For rolling hour: Use `_get_rolling_hour_value(hour_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 daily stats: Use `_get_daily_stat_value(day, stat_func)`
- For 24h windows: Use `_get_24h_window_value(stat_func)` - For 24h windows: Use `_get_24h_window_value(stat_func)`
- For others: Implement specific handler if needed - For others: Implement specific handler if needed
4. **Add translation keys** to `/translations/en.json` and `/custom_translations/en.json` 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`: The refactoring consolidated duplicate logic into unified methods in `sensor/core.py`:
- **`_get_interval_value(interval_offset, value_type, in_euro=False)`** - **`_get_interval_value(interval_offset, value_type, in_euro=False)`**
- Replaces: `_get_interval_price_value()`, `_get_interval_level_value()`, `_get_interval_rating_value()` - Replaces: `_get_interval_price_value()`, `_get_interval_level_value()`, `_get_interval_rating_value()`
- Handles: All interval-based sensors (current/next/previous) - Handles: All interval-based sensors (current/next/previous)
- Returns: Price (float), level (str), or rating (str) based on value_type - Returns: Price (float), level (str), or rating (str) based on value_type
- **`_get_rolling_hour_value(hour_offset, 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()` - Replaces: `_get_rolling_hour_average_value()`, `_get_rolling_hour_level_value()`, `_get_rolling_hour_rating_value()`
- Handles: All 5-interval rolling hour windows - Handles: All 5-interval rolling hour windows
- Returns: Aggregated value (average price, aggregated level/rating) - Returns: Aggregated value (average price, aggregated level/rating)
- **`_get_daily_stat_value(day, stat_func)`** - **`_get_daily_stat_value(day, stat_func)`**
- Replaces: `_get_statistics_value()` (calendar day portion) - Replaces: `_get_statistics_value()` (calendar day portion)
- Handles: Min/max/avg for calendar days (today/tomorrow) - Handles: Min/max/avg for calendar days (today/tomorrow)
- Returns: Price in subunit currency units (cents/øre) - Returns: Price in subunit currency units (cents/øre)
- **`_get_24h_window_value(stat_func)`** - **`_get_24h_window_value(stat_func)`**
- Replaces: `_get_average_value()`, `_get_minmax_value()` - Replaces: `_get_average_value()`, `_get_minmax_value()`
- Handles: Trailing/leading 24h window statistics - Handles: Trailing/leading 24h window statistics
- Returns: Price in subunit currency units (cents/øre) - 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. 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: The config flow is split into three separate flow handlers:
1. **User Flow** (`config_flow/user_flow.py`) - Initial setup and reauth 1. **User Flow** (`config_flow/user_flow.py`) - Initial setup and reauth
- `async_step_user()` - API token input - `async_step_user()` - API token input
- `async_step_select_home()` - Home selection - `async_step_select_home()` - Home selection
- `async_step_reauth()` / `async_step_reauth_confirm()` - Reauth flow - `async_step_reauth()` / `async_step_reauth_confirm()` - Reauth flow
2. **Subentry Flow** (`config_flow/subentry_flow.py`) - Add additional homes 2. **Subentry Flow** (`config_flow/subentry_flow.py`) - Add additional homes
- `async_step_user()` - Select from available homes - `async_step_user()` - Select from available homes
- `async_step_init()` - Subentry options - `async_step_init()` - Subentry options
3. **Options Flow** (`config_flow/options_flow.py`) - Reconfiguration 3. **Options Flow** (`config_flow/options_flow.py`) - Reconfiguration
- `async_step_init()` - General settings - `async_step_init()` - General settings
- `async_step_current_interval_price_rating()` - Price rating thresholds - `async_step_current_interval_price_rating()` - Price rating thresholds
- `async_step_volatility()` - Volatility settings - `async_step_volatility()` - Volatility settings
- `async_step_best_price()` - Best price period settings - `async_step_best_price()` - Best price period settings
- `async_step_peak_price()` - Peak price period settings - `async_step_peak_price()` - Peak price period settings
- `async_step_price_trend()` - Price trend thresholds - `async_step_price_trend()` - Price trend thresholds
To add a new step: To add a new step:

View file

@ -18,14 +18,14 @@ For detailed developer documentation, see [docs/development/](docs/development/)
1. **Fork the repository** on GitHub 1. **Fork the repository** on GitHub
2. **Clone your fork**: 2. **Clone your fork**:
```bash ```bash
git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git
cd hass.tibber_prices cd hass.tibber_prices
``` ```
3. **Open in DevContainer** (recommended): 3. **Open in DevContainer** (recommended):
- Open in VS Code - Open in VS Code
- Click "Reopen in Container" when prompted - Click "Reopen in Container" when prompted
- Or manually: `Ctrl+Shift+P` → "Dev Containers: Reopen in Container" - Or manually: `Ctrl+Shift+P` → "Dev Containers: Reopen in Container"
See [Development Setup](docs/development/setup.md) for detailed instructions. 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 1. **Push your branch** to your fork
2. **Create a Pull Request** on GitHub with: 2. **Create a Pull Request** on GitHub with:
- Clear title describing the change - Clear title describing the change
- Detailed description with context - Detailed description with context
- Reference related issues (`Fixes #123`) - Reference related issues (`Fixes #123`)
3. **Wait for review** and address feedback 3. **Wait for review** and address feedback
### PR Requirements ### 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: Documentation is organized in two Docusaurus sites:
- **User docs** (`docs/user/`): Installation, configuration, usage guides - **User docs** (`docs/user/`): Installation, configuration, usage guides
- Markdown files in `docs/user/docs/*.md` - Markdown files in `docs/user/docs/*.md`
- Navigation via `docs/user/sidebars.ts` - Navigation via `docs/user/sidebars.ts`
- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides
- Markdown files in `docs/developer/docs/*.md` - Markdown files in `docs/developer/docs/*.md`
- Navigation via `docs/developer/sidebars.ts` - Navigation via `docs/developer/sidebars.ts`
**When adding new documentation:** **When adding new documentation:**

View file

@ -128,32 +128,32 @@ The integration provides **100+ entities** across sensors, binary sensors, switc
```yaml ```yaml
automation: automation:
- alias: "Start Dishwasher During Best Price Period" - alias: "Start Dishwasher During Best Price Period"
trigger: trigger:
- platform: state - platform: state
entity_id: binary_sensor.tibber_best_price_period entity_id: binary_sensor.tibber_best_price_period
to: "on" to: "on"
action: action:
- action: switch.turn_on - action: switch.turn_on
target: target:
entity_id: switch.dishwasher entity_id: switch.dishwasher
``` ```
**Reduce heating when prices spike above average:** **Reduce heating when prices spike above average:**
```yaml ```yaml
automation: automation:
- alias: "Reduce Heating During High Prices" - alias: "Reduce Heating During High Prices"
trigger: trigger:
- platform: numeric_state - platform: numeric_state
entity_id: sensor.tibber_current_interval_price_rating entity_id: sensor.tibber_current_interval_price_rating
above: 20 # More than 20% above 24h average above: 20 # More than 20% above 24h average
action: action:
- action: climate.set_temperature - action: climate.set_temperature
target: target:
entity_id: climate.living_room entity_id: climate.living_room
data: data:
temperature: 19 temperature: 19
``` ```
📖 **[More automations →](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples)** — EV charging, heat pump control, price notifications, and more 📖 **[More automations →](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples)** — EV charging, heat pump control, price notifications, and more

View file

@ -1,15 +1,11 @@
{ {
"domain": "tibber_prices", "domain": "tibber_prices",
"name": "Tibber Price Information & Ratings", "name": "Tibber Price Information & Ratings",
"codeowners": [ "codeowners": ["@jpawlowski"],
"@jpawlowski"
],
"config_flow": true, "config_flow": true,
"documentation": "https://github.com/jpawlowski/hass.tibber_prices", "documentation": "https://github.com/jpawlowski/hass.tibber_prices",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues", "issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues",
"requirements": [ "requirements": ["aiofiles>=23.2.1"],
"aiofiles>=23.2.1"
],
"version": "0.31.0b1" "version": "0.31.0b1"
} }

View file

@ -1,6 +1,9 @@
[build-system] # Custom Component pyproject.toml based on Home Assistant Core
requires = ["setuptools==82.0.1"] # https://github.com/home-assistant/core/blob/dev/pyproject.toml
build-backend = "setuptools.build_meta" #
# Sections not included (HA Core specific):
# - [build-system] - Not published to PyPI (installed via HACS)
# - [tool.pylint.*] - Optional additional linting (Ruff is sufficient)
[project] [project]
name = "tibber_prices" name = "tibber_prices"
@ -13,73 +16,276 @@ packages = ["custom_components.tibber_prices"]
[tool.pyright] [tool.pyright]
include = ["custom_components/tibber_prices"] include = ["custom_components/tibber_prices"]
exclude = [ exclude = [
"**/node_modules", "**/.*",
"**/__pycache__", "**/__pycache__",
"**/.git", "**/config",
"**/.github",
"**/docs", "**/docs",
"**/node_modules",
"**/venv", "**/venv",
"**/.venv",
] ]
venvPath = "." venvPath = "."
venv = ".venv" venv = ".venv"
typeCheckingMode = "basic" typeCheckingMode = "basic"
reportUnusedImport = "none"
[tool.ruff] reportUnusedVariable = "none"
# Based on https://github.com/home-assistant/core/blob/dev/pyproject.toml reportUnusedCoroutine = "none"
target-version = "py314" reportMissingTypeStubs = "none"
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"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
python_files = ["test_*.py"] norecursedirs = [".git", "testing_config"]
python_classes = ["Test*"] log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s"
python_functions = ["test_*"] log_date_format = "%Y-%m-%d %H:%M:%S"
asyncio_debug = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function" asyncio_default_fixture_loop_scope = "function"
addopts = "-ra -q --strict-markers" addopts = "-ra -q --strict-markers"
markers = [ markers = [
"unit: Unit tests (fast, no external dependencies)", "unit: Unit tests (fast, no external dependencies)",
"integration: Integration tests (may use coordinator/time service)", "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] [project.optional-dependencies]
test = [ test = ["pytest-homeassistant-custom-component>=0.13.323"]
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"pytest-homeassistant-custom-component>=0.13.323",
]

View file

@ -84,15 +84,7 @@
"description": "The integration type.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type", "description": "The integration type.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type",
"type": "string", "type": "string",
"default": "hub", "default": "hub",
"enum": [ "enum": ["device", "entity", "hardware", "helper", "hub", "service", "system"]
"device",
"entity",
"hardware",
"helper",
"hub",
"service",
"system"
]
}, },
"config_flow": { "config_flow": {
"description": "Whether the integration is configurable from the UI.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#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": { "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", "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", "type": "string",
"enum": [ "enum": ["assumed_state", "cloud_polling", "cloud_push", "local_polling", "local_push", "calculated"]
"assumed_state",
"cloud_polling",
"cloud_push",
"local_polling",
"local_push",
"calculated"
]
}, },
"single_config_entry": { "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", "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, "additionalProperties": false,
"required": [ "required": ["domain", "name", "codeowners", "documentation", "issue_tracker", "version"],
"domain",
"name",
"codeowners",
"documentation",
"issue_tracker",
"version"
],
"dependencies": { "dependencies": {
"mqtt": { "mqtt": {
"anyOf": [ "anyOf": [