Compare commits

..

No commits in common. "91147bd79c4245c9d7c61d1c620e240fdc330fb3" and "9efa7809d02ea274727f49acff88df765d7fc83a" have entirely different histories.

21 changed files with 595 additions and 892 deletions

View file

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

View file

@ -1,7 +1,6 @@
# top-most EditorConfig file
root = true
# Default settings - AI-friendly baseline
[*]
charset = utf-8
end_of_line = lf
@ -10,71 +9,12 @@ trim_trailing_whitespace = true
indent_style = space
indent_size = 4
# Python - Home Assistant & Ruff defaults (120 chars)
[*.py]
# Python style aligns with Black
indent_size = 4
max_line_length = 120
# YAML - Home Assistant configs, GitHub workflows
[*.{yaml,yml}]
indent_size = 2
# JSON - manifest.json, translations, etc.
[*.json]
indent_size = 2
# Markdown - READMEs, docs (preserve AI formatting)
[*.md]
indent_size = 2
trim_trailing_whitespace = false
max_line_length = off
# TOML - pyproject.toml, Python packaging
[*.toml]
indent_size = 4
# Shell scripts - setup scripts, CI/CD
[*.{sh,bash}]
indent_size = 4
end_of_line = lf
# JavaScript/TypeScript - Frontend panel development
[*.{js,ts,jsx,tsx,mjs,cjs}]
indent_size = 2
# CSS/SCSS - Frontend styling
[*.{css,scss,sass}]
indent_size = 2
# HTML - Lovelace cards, frontend templates
[*.html]
indent_size = 2
# XML - Android Auto integration, etc.
[*.xml]
indent_size = 2
# Jinja2 templates - Home Assistant templates
[*.jinja,*.jinja2,*.j2]
indent_size = 2
# Makefiles require tabs
[Makefile]
indent_style = tab
# GitHub-specific files
[.github/workflows/*.{yml,yaml}]
indent_size = 2
[.github/dependabot.{yml,yaml}]
indent_size = 2
# Docker files
[Dockerfile*]
indent_size = 2
[*.dockerignore]
indent_size = 2
[docker-compose*.{yml,yaml}]
indent_size = 2

2
.github/FUNDING.yml vendored
View file

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

View file

@ -3,69 +3,69 @@ name: "Bug report"
description: "Report a bug with the custom integration"
labels: ["bug"]
body:
- type: markdown
attributes:
value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
- type: input
attributes:
label: "Home Assistant version"
description: "The version of Home Assistant you are using"
placeholder: "2025.1.0"
validations:
required: true
- type: input
attributes:
label: "Integration version"
description: "The version of this custom integration you are using"
placeholder: "1.0.0"
validations:
required: false
- type: textarea
attributes:
label: "System Health details"
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)"
validations:
required: false
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have enabled debug logging for my installation.
required: true
- label: I have filled out the issue template to the best of my ability.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- label: This issue is not a duplicate issue of any [previous issues](https://github.com/jpawlowski/hass.tibber_prices/issues?q=is%3Aissue+label%3A%22Bug%22+)..
required: true
- type: textarea
attributes:
label: "Describe the issue"
description: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: "Without steps to reproduce, it will be hard to fix. It is very important that you fill out this part. Issues without it will be closed."
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: "Debug logs"
description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue."
render: text
validations:
required: true
- type: markdown
attributes:
value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
- type: input
attributes:
label: "Home Assistant version"
description: "The version of Home Assistant you are using"
placeholder: "2025.1.0"
validations:
required: true
- type: input
attributes:
label: "Integration version"
description: "The version of this custom integration you are using"
placeholder: "1.0.0"
validations:
required: false
- type: textarea
attributes:
label: "System Health details"
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)"
validations:
required: false
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have enabled debug logging for my installation.
required: true
- label: I have filled out the issue template to the best of my ability.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- label: This issue is not a duplicate issue of any [previous issues](https://github.com/jpawlowski/hass.tibber_prices/issues?q=is%3Aissue+label%3A%22Bug%22+)..
required: true
- type: textarea
attributes:
label: "Describe the issue"
description: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: "Without steps to reproduce, it will be hard to fix. It is very important that you fill out this part. Issues without it will be closed."
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: "Debug logs"
description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue."
render: text
validations:
required: true
- type: textarea
attributes:
label: "Diagnostics dump"
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"
validations:
required: false
- type: textarea
attributes:
label: "Diagnostics dump"
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"
validations:
required: false

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

View file

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

View file

@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- "custom_components/tibber_prices/manifest.json"
- 'custom_components/tibber_prices/manifest.json'
permissions:
contents: write
@ -22,7 +22,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0 # Need full history for git describe
fetch-depth: 0 # Need full history for git describe
- name: Extract version from manifest.json
id: manifest

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,6 @@
# Custom Component pyproject.toml based on Home Assistant Core
# https://github.com/home-assistant/core/blob/dev/pyproject.toml
#
# Sections not included (HA Core specific):
# - [build-system] - Not published to PyPI (installed via HACS)
# - [tool.pylint.*] - Optional additional linting (Ruff is sufficient)
[build-system]
requires = ["setuptools==82.0.1"]
build-backend = "setuptools.build_meta"
[project]
name = "tibber_prices"
@ -16,276 +13,73 @@ packages = ["custom_components.tibber_prices"]
[tool.pyright]
include = ["custom_components/tibber_prices"]
exclude = [
"**/.*",
"**/__pycache__",
"**/config",
"**/docs",
"**/node_modules",
"**/__pycache__",
"**/.git",
"**/.github",
"**/docs",
"**/venv",
"**/.venv",
]
venvPath = "."
venv = ".venv"
typeCheckingMode = "basic"
reportUnusedImport = "none"
reportUnusedVariable = "none"
reportUnusedCoroutine = "none"
reportMissingTypeStubs = "none"
[tool.ruff]
# Based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
target-version = "py314"
line-length = 120
[tool.ruff.lint]
select = ["ALL"]
ignore = [
# "ANN101", # Missing type annotation for `self` in method
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"D203", # no-blank-line-before-class (incompatible with formatter)
"D212", # multi-line-summary-first-line (incompatible with formatter)
"COM812", # incompatible with formatter
"ISC001", # incompatible with formatter
"UP037", # quoted annotations; needed for TYPE_CHECKING forward references
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = [
"S101", # assert is fine in tests
"PLR2004", # Magic values are fine in tests
]
"scripts/*" = [
"T201", # print() is the correct output method for CLI scripts
"INP001", # scripts/ is not a Python package (no __init__.py)
]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
[tool.ruff.lint.pyupgrade]
keep-runtime-typing = true
[tool.ruff.lint.mccabe]
max-complexity = 25
[tool.ruff.lint.isort]
force-single-line = false
known-first-party = ["custom_components", "homeassistant"]
[tool.pytest.ini_options]
testpaths = ["tests"]
norecursedirs = [".git", "testing_config"]
log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"
asyncio_debug = true
asyncio_mode = "auto"
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
asyncio_default_fixture_loop_scope = "function"
addopts = "-ra -q --strict-markers"
markers = [
"unit: Unit tests (fast, no external dependencies)",
"integration: Integration tests (may use coordinator/time service)",
]
filterwarnings = [
# Treat warnings as errors to catch issues early
"error",
# Ignore specific warnings from third-party libraries as needed
# "ignore:.*custom_components.* is using deprecated.*:DeprecationWarning",
]
[tool.coverage.run]
source = ["custom_components/tibber_prices"]
omit = ["tests/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"@overload",
]
[tool.ruff]
# Based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
required-version = ">=0.15.1"
target-version = "py314"
line-length = 120
[tool.ruff.lint]
select = [
"A001", # Variable {name} is shadowing a Python builtin
"ASYNC", # flake8-async
"B002", # Python does not support the unary prefix increment
"B005", # Using .strip() with multi-character strings is misleading
"B007", # Loop control variable {name} not used within loop body
"B009", # Do not call getattr with a constant attribute value. It is not any safer than normal property access.
"B014", # Exception handler with duplicate exception
"B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it.
"B017", # pytest.raises(BaseException) should be considered evil
"B018", # Found useless attribute access. Either assign it to a variable or remove it.
"B023", # Function definition does not bind loop variable {name}
"B024", # `{name}` is an abstract base class, but it has no abstract methods or properties
"B025", # try-except* block with duplicate exception {name}
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
"B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)?
"B035", # Dictionary comprehension uses static key
"B904", # Use raise from to specify exception cause
"B905", # zip() without an explicit strict= parameter
"BLE",
"C", # complexity
"COM818", # Trailing comma on bare tuple prohibited
"D", # docstrings
"DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow()
"DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts)
"E", # pycodestyle
"F", # pyflakes/autoflake
"F541", # f-string without any placeholders
"FLY", # flynt
"FURB", # refurb
"G", # flake8-logging-format
"I", # isort
"INP", # flake8-no-pep420
"ISC", # flake8-implicit-str-concat
"ICN001", # import concentions; {name} should be imported as {asname}
"LOG", # flake8-logging
"N804", # First argument of a class method should be named cls
"N805", # First argument of a method should be named self
"N815", # Variable {name} in class scope should not be mixedCase
"PERF", # Perflint
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PTH", # flake8-pathlib
"PYI", # flake8-pyi
"RET", # flake8-return
"RSE", # flake8-raise
"RUF005", # Consider iterable unpacking instead of concatenation
"RUF006", # Store a reference to the return value of asyncio.create_task
"RUF007", # Prefer itertools.pairwise() over zip() when iterating over successive pairs
"RUF008", # Do not use mutable default values for dataclass attributes
"RUF010", # Use explicit conversion flag
"RUF013", # PEP 484 prohibits implicit Optional
"RUF016", # Slice in indexed access to type {value_type} uses type {index_type} instead of an integer
"RUF017", # Avoid quadratic list summation
"RUF018", # Avoid assignment expressions in assert statements
"RUF019", # Unnecessary key check before dictionary access
"RUF020", # {never_like} | T is equivalent to T
"RUF021", # Parenthesize a and b expressions when chaining and and or together, to make the precedence clear
"RUF022", # Sort __all__
"RUF023", # Sort __slots__
"RUF024", # Do not pass mutable objects as values to dict.fromkeys
"RUF026", # default_factory is a positional-only argument to defaultdict
"RUF030", # print() call in assert statement is likely unintentional
"RUF032", # Decimal() called with float literal argument
"RUF033", # __post_init__ method with argument defaults
"RUF034", # Useless if-else condition
"RUF059", # unused-unpacked-variable
"RUF100", # Unused `noqa` directive
"RUF101", # noqa directives that use redirected rule codes
"RUF200", # Failed to parse pyproject.toml: {message}
"S102", # Use of exec detected
"S103", # bad-file-permissions
"S108", # hardcoded-temp-file
"S306", # suspicious-mktemp-usage
"S307", # suspicious-eval-usage
"S313", # suspicious-xmlc-element-tree-usage
"S314", # suspicious-xml-element-tree-usage
"S315", # suspicious-xml-expat-reader-usage
"S316", # suspicious-xml-expat-builder-usage
"S317", # suspicious-xml-sax-usage
"S318", # suspicious-xml-mini-dom-usage
"S319", # suspicious-xml-pull-dom-usage
"S601", # paramiko-call
"S602", # subprocess-popen-with-shell-equals-true
"S604", # call-with-shell-equals-true
"S608", # hardcoded-sql-expression
"S609", # unix-command-wildcard-injection
"SIM", # flake8-simplify
"SLF", # flake8-self
"SLOT", # flake8-slots
"T100", # Trace found: {name} used
"T20", # flake8-print
"TC", # flake8-type-checking
"TID", # Tidy imports
"TRY", # tryceratops
"UP", # pyupgrade
"UP031", # Use format specifiers instead of percent format
"UP032", # Use f-string instead of `format` call
"W", # pycodestyle
]
ignore = [
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"ASYNC109", # Async function definition with a `timeout` parameter Use `asyncio.timeout` instead
"ASYNC110", # Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop
"ASYNC240", # Use an async function for entering the file system
"D202", # No blank lines allowed after function docstring
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
"D406", # Section name should end with a newline
"D407", # Section name underlining
"D417", # Missing argument descriptions in docstring - to allow documenting only non-obvious parameters
"E501", # line too long
"PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives
"PLR0911", # Too many return statements ({returns} > {max_returns})
"PLR0912", # Too many branches ({branches} > {max_branches})
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
"PLR0915", # Too many statements ({statements} > {max_statements})
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"PLW0108", # Unnecessary lambda wrapping a function call; can often be replaced by the function itself
"PLW1641", # __eq__ without __hash__
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
"PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception
"PT018", # Assertion should be broken down into multiple parts
"RUF001", # String contains ambiguous unicode character.
"RUF002", # Docstring contains ambiguous unicode character.
"RUF003", # Comment contains ambiguous unicode character.
"RUF015", # Prefer next(...) over single element slice
"SIM102", # Use a single if statement instead of nested if statements
"SIM103", # Return the condition {condition} directly
"SIM108", # Use ternary operator {contents} instead of if-else-block
"SIM115", # Use context handler for opening files
# Moving imports into type-checking blocks can mess with pytest.patch()
"TC001", # Move application import {} into a type-checking block
"TC002", # Move third-party import {} into a type-checking block
"TC003", # Move standard library import {} into a type-checking block
# Quotes for typing.cast generally not necessary, only for performance critical paths
"TC006", # Add quotes to type expression in typing.cast()
"TRY003", # Avoid specifying long messages outside the exception class
"TRY400", # Use `logging.exception` instead of `logging.error`
"UP046", # Non PEP 695 generic class
"UP047", # Non PEP 696 generic function
"UP049", # Avoid private type parameter names
# May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
"W191",
"E111",
"E114",
"E117",
"D203",
"D206",
"D212",
"D300",
"Q",
"COM812",
"COM819",
"ISC001",
# Disabled because ruff does not understand type of __all__ generated by a function
"PLE0605",
"FURB116",
]
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
# Commonly used Home Assistant imports
voluptuous = "vol"
"homeassistant.helpers.config_validation" = "cv"
"homeassistant.helpers.device_registry" = "dr"
"homeassistant.helpers.entity_registry" = "er"
"homeassistant.util.dt" = "dt_util"
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"async_timeout".msg = "use asyncio.timeout instead"
"pytz".msg = "use zoneinfo instead"
"tests".msg = "You should not import tests"
[tool.ruff.lint.isort]
force-sort-within-sections = true
known-first-party = ["custom_components", "homeassistant"]
combine-as-imports = true
split-on-trailing-comma = false
[tool.ruff.lint.per-file-ignores]
"script/*" = [
"T20", # print() allowed in scripts
"INP001", # Implicit namespace package (scripts are not a package)
]
"tests/*" = [
"S101", # assert is fine in tests
"PLR2004", # Magic values are fine in tests
"D", # Docstrings not required in tests
"PTH", # Use pathlib - temporary exemption for tests
]
[tool.ruff.lint.mccabe]
max-complexity = 25
[tool.ruff.lint.pydocstyle]
convention = "google"
property-decorators = ["propcache.api.cached_property"]
[tool.ruff.lint.pyupgrade]
keep-runtime-typing = true
[project.optional-dependencies]
test = ["pytest-homeassistant-custom-component>=0.13.323"]
test = [
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"pytest-homeassistant-custom-component>=0.13.323",
]

View file

@ -84,7 +84,15 @@
"description": "The integration type.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type",
"type": "string",
"default": "hub",
"enum": ["device", "entity", "hardware", "helper", "hub", "service", "system"]
"enum": [
"device",
"entity",
"hardware",
"helper",
"hub",
"service",
"system"
]
},
"config_flow": {
"description": "Whether the integration is configurable from the UI.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#config-flow",
@ -367,7 +375,14 @@
"iot_class": {
"description": "The IoT class of the integration, describing how the integration connects to the device or service.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#iot-class",
"type": "string",
"enum": ["assumed_state", "cloud_polling", "cloud_push", "local_polling", "local_push", "calculated"]
"enum": [
"assumed_state",
"cloud_polling",
"cloud_push",
"local_polling",
"local_push",
"calculated"
]
},
"single_config_entry": {
"description": "Whether the integration only supports a single config entry.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#single-config-entry-only",
@ -380,7 +395,14 @@
}
},
"additionalProperties": false,
"required": ["domain", "name", "codeowners", "documentation", "issue_tracker", "version"],
"required": [
"domain",
"name",
"codeowners",
"documentation",
"issue_tracker",
"version"
],
"dependencies": {
"mqtt": {
"anyOf": [