# Copilot Instructions This is a **Home Assistant custom component** for Tibber electricity price data, distributed via **HACS**. It fetches, caches, and enriches **quarter-hourly** electricity prices with statistical analysis, price levels, and ratings. ## Documentation Metadata - **Last Major Update**: 2025-11-09 - **Last Architecture Review**: 2025-11-09 (Translation system restructured - selector translations now use `selector.{translation_key}.options.{value}` pattern) - **Documentation Status**: ✅ Current (verified against codebase) _Note: When proposing significant updates to this file, update the metadata above with the new date and brief description of changes._ ## Maintaining This Documentation **CRITICAL: This file is the AI's long-term memory across sessions.** When working with the codebase, Copilot MUST actively maintain consistency between this documentation and the actual code: **Scope:** "This documentation" and "this file" refer specifically to `AGENTS.md` in the repository root. This does NOT include user-facing documentation like `README.md`, `/docs/user/`, or comments in code. Those serve different purposes and are maintained separately. **Documentation Organization:** - **This file** (`AGENTS.md`): AI/Developer long-term memory, patterns, conventions - **`docs/user/`**: End-user guides (installation, configuration, usage examples) - **`docs/development/`**: Contributor guides (setup, architecture, release management) - **`README.md`**: Project overview with links to detailed documentation **Automatic Inconsistency Detection:** - When code changes affect documented patterns, examples, file paths, function names, or architectural decisions **in this file**, IMMEDIATELY flag the inconsistency - If a documented function is renamed, moved, or deleted → suggest documentation update - If file structure changes (files moved/renamed/deleted) → suggest path updates - If implementation patterns change (e.g., new translation structure, different caching approach) → suggest pattern documentation update - If new learnings emerge during debugging or development → suggest adding to documentation **Documentation Update Process:** 1. **Detect** inconsistency or valuable new learning during work 2. **ALWAYS ask first** before modifying this file - propose what should be changed/added and explain WHY 3. **Wait for approval** from user 4. **Apply changes** only after confirmation 5. Keep proposals concise but specific (rough outline acceptable, not full text required) **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) - **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...") **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. **When to Propose Updates (with Confidence Levels):** 🔴 **HIGH Confidence** - Factual inconsistencies (flag immediately): - ✅ Documented function/class renamed, moved, or deleted - ✅ File paths changed (files moved/renamed/deleted) - ✅ Example code references non-existent code - ✅ Breaking changes to documented APIs or patterns 🟡 **MEDIUM Confidence** - Possible changes (ask for clarification): - ✅ Implementation pattern changed (might be intentional refactor) - ✅ New approach observed alongside documented old approach (unclear which is preferred) - ✅ Documented pattern still works but seems outdated 🟢 **LOW Confidence** - Suggestions for additions (propose when valuable): - ✅ New architectural pattern discovered during debugging (like the selector translation structure fix) - ✅ Important learnings that would help future sessions - ✅ User expressed wish for documentation - ✅ HA best practice learned that applies to this project **Do NOT Propose Updates For:** - ❌ Temporary debugging code or experimental changes - ❌ Minor implementation details that don't affect understanding - ❌ Private helper function internals (unless part of documented pattern) - ❌ TODO comments (unless they represent architectural decisions) - ❌ Variable names or internal state (unless part of public API) **Update Proposal Format:** Include confidence level and impact in proposals: > **[🔴 HIGH]** I noticed the translation pattern in AGENTS.md references `enrich_price_info_with_differences()` in `price_utils.py`, but this function was renamed to `enrich_prices()`. > > **Impact:** Future sessions will look for wrong function name. > > **Proposed change:** Update function name in "Price Data Enrichment" section. > > Should I update the documentation? **Batch Updates:** If you detect 3+ related minor changes (e.g., multiple constant renames during refactoring), propose them as one batch update instead of asking separately for each. This ensures the documentation stays accurate and useful as the codebase evolves, while maintaining user control over what gets documented. ## Architecture Overview **Core Data Flow:** 1. `TibberPricesApiClient` (`api.py`) queries Tibber's GraphQL API with `resolution:QUARTER_HOURLY` for user data and prices (yesterday/today/tomorrow - 192 intervals total) 2. `TibberPricesDataUpdateCoordinator` (`coordinator.py`) orchestrates updates every 15 minutes, manages persistent storage via `Store`, and schedules quarter-hour entity refreshes 3. Price enrichment functions (`price_utils.py`, `average_utils.py`) calculate trailing/leading 24h averages, price differences, and rating levels for each 15-minute interval 4. Entity platforms (`sensor.py`, `binary_sensor.py`) expose enriched data as Home Assistant entities 5. Custom services (`services.py`) provide API endpoints for integrations like ApexCharts **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**: `/scripts/json_schemas/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}`). Example: ```python # config_flow.py SelectSelector(SelectSelectorConfig( options=["LOW", "MODERATE", "HIGH"], translation_key="volatility" )) ``` ```json # translations/en.json { "selector": { "volatility": { "options": { "low": "Low", "moderate": "Moderate", "high": "High" } } } } ``` **CRITICAL:** When using `translation_key`, pass options as **plain string list**, NOT `SelectOptionDict`. **VALIDATION:** Selector option keys MUST be lowercase: `[a-z0-9-_]+` pattern (no uppercase, cannot start/end with hyphen/underscore). Hassfest will reject keys like `LOW`, `ANY`, `VERY_HIGH`. Use `low`, `any`, `very_high` instead. ```python # ✅ CORRECT with translation_key SelectSelector(SelectSelectorConfig( options=["LOW", "MODERATE", "HIGH"], # Plain strings! translation_key="volatility" )) # ❌ WRONG - label parameter overrides translations SelectSelector(SelectSelectorConfig( options=[SelectOptionDict(value="LOW", label="Low"), ...], translation_key="volatility" # translation_key is ignored when label is set! )) # ✅ SelectOptionDict ONLY for dynamic/non-translatable options SelectSelector(SelectSelectorConfig( options=[SelectOptionDict(value=home_id, label=home_name) for ...], # No translation_key - labels come from runtime data )) ``` - **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 `price_utils.py`. Enriched structure example: ```python { "startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, # Original from API "level": "NORMAL", # Original from API "trailing_avg_24h": 0.2312, # Added: 24h trailing average "difference": 9.6, # Added: % diff from trailing avg "rating_level": "NORMAL" # Added: LOW/NORMAL/HIGH based on thresholds } ``` - **Quarter-hour precision**: Entities update on 00/15/30/45-minute boundaries via `_schedule_quarter_hour_refresh()` in coordinator, not just on data fetch intervals. This ensures current price sensors update without waiting for the next API poll. - **Currency handling**: Multi-currency support with major/minor 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 **Component Structure:** ``` custom_components/tibber_prices/ ├── __init__.py # Entry setup, platform registration ├── coordinator.py # DataUpdateCoordinator with caching/scheduling ├── api.py # GraphQL client with retry/error handling ├── price_utils.py # Price enrichment, level/rating calculations ├── average_utils.py # Trailing/leading average utilities ├── services.py # Custom services (get_price, ApexCharts, etc.) ├── sensor.py # Price/stats/diagnostic sensors ├── binary_sensor.py # Peak/best hour binary sensors ├── entity.py # Base TibberPricesEntity class ├── data.py # @dataclass TibberPricesData ├── const.py # Constants, translation loaders, currency helpers ├── config_flow.py # UI configuration flow └── services.yaml # Service definitions ``` ## Development Environment Setup **Python Virtual Environment:** - Project uses `.venv` located at `/home/vscode/.venv/` (outside workspace) - Symlinked into workspace root as `.venv` → `/home/vscode/.venv/` - **Why outside workspace?** Project folder is bind-mounted from host, which doesn't support hardlinks required by `uv` **Package Manager:** - Uses `uv` (modern, fast Python package manager) - **Always use `uv` commands**, not `pip` directly: ```bash # ✅ Correct uv pip install uv run pytest # ❌ Wrong - uses system Python pip install python -m pytest ``` **Development Scripts:** - All scripts in `./scripts/` automatically use the correct `.venv` - No need to manually activate venv or specify Python path - Examples: `./scripts/lint`, `./scripts/develop`, `./scripts/lint-check` - Release management: `./scripts/prepare-release`, `./scripts/generate-release-notes` **Release Note Backends (auto-installed in DevContainer):** - **Rust toolchain**: Minimal Rust installation via DevContainer feature - **git-cliff**: Template-based release notes (fast, reliable, installed via cargo in `scripts/setup`) - Manual grep/awk parsing as fallback (always available) **When generating shell commands:** 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')" ``` 3. **For package management**, always use `uv`: ```bash uv pip list uv pip install --upgrade homeassistant ``` **Debugging Environment Issues:** - If `import homeassistant` fails: Check if `.venv` symlink exists and points to correct location - If packages missing: Run `uv sync` to install dependencies from `pyproject.toml` - If wrong Python version: Verify `.venv/bin/python --version` (should be 3.13+) ## Development Workflow **IMPORTANT: This project is designed for DevContainer development.** 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 **If user insists on local development without container**, warn that: - You'll need to adapt commands for their local setup - Some features (like `.venv` symlink) won't work as documented - Support will be limited (not the intended workflow) **Start dev environment:** ```bash ./scripts/develop # Starts HA in debug mode with config/ dir, sets PYTHONPATH ``` **Linting (auto-fix):** ```bash ./scripts/lint # Runs ruff format + ruff check --fix ``` **Linting (check-only):** ```bash ./scripts/lint-check # CI mode, no modifications ``` **Testing:** ```bash pytest tests/ # Unit tests exist (test_*.py) but no framework enforced ``` ## Testing Changes **IMPORTANT: Never start `./scripts/develop` automatically.** 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) 3. **Keep test guidance concise** - 3-5 bullet points max 4. **Focus on session changes only** - Don't suggest testing unrelated features **Example:** > ✅ Changes complete! Please test: > > 1. Rebuild DevContainer (VS Code: "Reopen and Rebuild") > 2. Open a Python file - Pylance should now resolve `homeassistant.*` imports without errors > 3. Check that autocomplete works for HA APIs > > No need to test the integration itself - these are environment-only changes. **What NOT to do:** - ❌ Don't execute `./scripts/develop` automatically - ❌ Don't suggest exhaustive testing of unrelated features - ❌ Don't check `git status` to determine what changed (trust session memory) - ❌ Don't assume user needs reminding to commit (they manage their own workflow) ## Git Workflow Guidance **Purpose:** Maintain clean, atomic commits that enable future release note generation while preserving technical accuracy. **Why This Matters:** - Commits stay **technical** (for developers, describe what changed and why) - Commits are **structured** (Conventional Commits format with "Impact:" sections) - Release notes are **user-friendly** (AI translates commits into user language later) - Clean history enables automatic release note generation from commit messages **Critical Principles:** 1. **AI suggests commits, NEVER executes them** - User maintains full control of git operations 2. **Commits are for developers** - Technical language, implementation details, code changes 3. **Release notes are for users** - AI will translate commit history into user-friendly format later 4. **Suggest conservatively** - Only at clear feature boundaries, not after every change 5. **Trust session memory** - Don't check `git status`, recall what was accomplished this session ### When to Suggest Commits **Suggest commits at clear feature boundaries:** | Scenario | Suggest? | Example | | --------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- | | Feature complete and tested | ✅ YES | "Optional: Before we start the next feature, you might want to commit the translation system fixes?" | | Bug fixed with verification | ✅ YES | "Optional: This bug fix is complete and verified. Ready to commit before moving on?" | | Multiple related files changed (logical unit) | ✅ YES | "Optional: All 5 translation files updated. This forms a logical commit." | | About to start unrelated work | ✅ YES | "Optional: Before we start refactoring the API client, commit the current sensor changes?" | | User explicitly asks what's uncommitted | ✅ YES | Provide summary of changes and suggest commit message | | Iterating on same feature | ❌ NO | Don't suggest between attempts/refinements | | Debugging in progress | ❌ NO | Wait until root cause found and fixed | | User declined previous commit suggestion | ❌ NO | Respect their workflow preference | **Suggestion Language:** - Use "Optional:" prefix to make it clear this is not required - Ask, don't assume: "Want to commit?" not "You should commit" - Accept graceful decline: If user says no or ignores, don't mention again for that boundary - Provide commit message: Include full Conventional Commit format with "Impact:" section - **Specify files to stage**: When suggesting commits, list exact files for `git add` - **Split when logical**: If session has multiple unrelated changes, suggest separate commits with specific file lists **Commit Splitting Guidelines:** Split into multiple commits when: - Different areas affected (config flow + docs + environment) - Different change types (fix + feat + docs) - Different impact scope (user-facing vs. developer-only) - Changes can work independently Combine into single commit when: - Tightly coupled changes (translations + code using them) - Single feature across files (sensor + translations + service) - Dependency chain (A requires B to function) - Small scope (2-3 related files telling one story) **Example - Single Commit:** > Optional: Ruff configuration migration complete. Ready to commit? > > **Stage these files:** > > ```bash > git add pyproject.toml AGENTS.md > ``` > > **Commit message:** > > ``` > refactor: migrate ruff config from .ruff.toml to pyproject.toml > > Consolidated ruff configuration into pyproject.toml following modern Python > conventions and integration_blueprint pattern. > > Updated all references in AGENTS.md from .ruff.toml to > pyproject.toml under [tool.ruff] section. > > Impact: Aligns with modern Python tooling standards. No user-visible changes. > ``` **Example - Multiple Commits:** > Optional: Two separate improvements ready. Suggest splitting: > > **Commit 1: Translation Fix** > > ```bash > git add custom_components/tibber_prices/config_flow.py > git add custom_components/tibber_prices/translations/*.json > ``` > > ``` > fix(config_flow): use flat selector structure for translation_key > > SelectOptionDict with label parameter was overriding translation_key, > causing config flow to fail at step 4. > > Changed to plain string lists with translation_key parameter, > following HA pattern: selector.{translation_key}.options.{value} > > Updated all 5 language files (de, en, nb, nl, sv). > > Impact: Config flow works through all 6 steps with translated options. > ``` > > **Commit 2: Documentation** > > ```bash > git add AGENTS.md > ``` > > ``` > docs(patterns): document selector translation structure > > Added correct translation pattern for SelectSelector based on > official HA documentation and debugging session. > > Documents flat selector.{translation_key}.options.{value} structure > and common pitfall of SelectOptionDict overriding translations. > > Impact: Future sessions generate correct selector translations. > ``` > > Want to commit separately or combine? ### Conventional Commits Format **Structure:** ``` (): Impact: ``` **Best Practices:** - **Subject line**: Max 50 chars (hard limit 72), imperative mood ("Add" not "Added") - **Body**: Wrap at 72 chars, explain WHAT and WHY (not HOW - code shows that) - **Blank line**: Required between subject and body - **Impact section**: Our addition for release note generation (optional but recommended) **Types:** - `feat`: New feature (appears in release notes as "New Features") - `fix`: Bug fix (appears in release notes as "Bug Fixes") - `docs`: Documentation only (appears in release notes as "Documentation") - `refactor`: Code restructure without behavior change (may or may not appear in release notes) - `chore`: Maintenance tasks (usually omitted from release notes) - `test`: Test changes only (omitted from release notes) - `style`: Formatting changes (omitted from release notes) **Scope (optional but recommended):** - `translations`: Translation system changes - `config_flow`: Configuration flow changes - `sensors`: Sensor implementation - `api`: API client changes - `coordinator`: Data coordinator changes - `docs`: Documentation files ### Technical Commit Message Examples **Example 1: Bug Fix** ``` fix(config_flow): use flat selector structure for translation_key SelectOptionDict with label parameter was overriding translation_key, causing config flow to fail at step 4 with "Unknown error occurred". Changed to use plain string lists with translation_key parameter, following official HA pattern: selector.{translation_key}.options.{value} Updated all 5 language files (de, en, nb, nl, sv) with correct structure. Impact: Config flow now works through all 6 steps with properly translated dropdown options. Users can complete setup without encountering errors. ``` **Example 2: Documentation** ``` docs(workflow): add git commit guidance for release notes Added comprehensive "Git Workflow Guidance" section to AGENTS.md documenting when AI should suggest commits, Conventional Commits format, and how to structure technical messages that enable future release note generation. Key additions: - Commit boundary detection decision table - When NOT to suggest commits (during iteration/debugging) - Conventional Commits format with types and scopes - Technical commit message examples with "Impact:" sections - Release note generation guidelines for future use Impact: AI can now help maintain clean, atomic commits structured for automatic release note generation while preserving technical accuracy. ``` **Example 3: Feature** ``` feat(environment): add VS Code Python environment configuration Added .vscode/settings.json with universal Python/Ruff settings and updated .devcontainer/devcontainer.json to use workspace .venv interpreter. Changes: - .devcontainer/devcontainer.json: Set python.defaultInterpreterPath to .venv - .devcontainer/devcontainer.json: Added python.analysis.extraPaths - .vscode/settings.json: Created with Pylance and Ruff configuration - Removed deprecated ruff.lint.args and ruff.format.args Impact: Pylance now resolves homeassistant.* imports correctly and provides full autocomplete for Home Assistant APIs. Developers get proper IDE support without manual interpreter selection. ``` **Example 4: Refactor** ``` refactor: migrate ruff config from .ruff.toml to pyproject.toml Consolidated ruff configuration into pyproject.toml following modern Python conventions and integration_blueprint pattern. Updated all references in AGENTS.md from .ruff.toml to pyproject.toml under [tool.ruff] section. Impact: Aligns with modern Python tooling standards. No user-visible changes. ``` ### "Impact:" Section Guidelines The "Impact:" section bridges technical commits and future release notes: **What to Include:** - **User-visible effects**: What changes for end users of the integration - **Developer benefits**: What improves for contributors/maintainers - **Context for translation**: Information that helps future AI translate this into user-friendly release note - **Omit "Impact:" if**: Internal refactor with zero user/dev impact (e.g., rename private variable) **Examples:** ✅ **Good Impact Sections:** - "Config flow now works through all 6 steps without errors" - "Pylance provides full autocomplete for Home Assistant APIs" - "AI maintains clean commit history for release note generation" - "Aligns with HA 2025.x translation schema requirements" - "Reduces API calls by 70% through intelligent caching" ❌ **Poor Impact Sections:** - "Code is better now" (vague, not actionable) - "Fixed the bug" (redundant with commit type) - "Updated file X" (describes action, not impact) - "This should work" (uncertain, commits should be verified) ### Release Note Generation (Future Use) **When generating release notes from commits:** 1. **Filter by type**: - Include: `feat`, `fix`, `docs` (if significant) - Maybe include: `refactor` (if user-visible) - Exclude: `chore`, `test`, `style` 2. **Group by type**: - "New Features" (feat) - "Bug Fixes" (fix) - "Documentation" (docs) - "Improvements" (refactor with user impact) 3. **Translate to user language**: - Technical: "fix(config_flow): use flat selector structure" → User: "Fixed configuration wizard failing at step 4" - Technical: "feat(environment): add VS Code configuration" → User: "Improved developer experience with better IDE support" 4. **Use "Impact:" as source**: - Extract user-visible effects from Impact sections - Preserve context (why it matters) - Rewrite in present tense, active voice 5. **Add examples if helpful**: - Show before/after for UI changes - Demonstrate new capabilities with code snippets - Link to documentation for complex features **Example Release Note (Generated from Commits):** > **Tibber Prices 2.0.1** > > **Bug Fixes** > > - Fixed configuration wizard failing at step 4 when selecting price thresholds. Dropdown options now appear correctly with proper translations. > > **Improvements** > > - Improved developer environment setup with automatic Python path detection and full Home Assistant API autocomplete in VS Code ### Philosophy **User Controls Workflow:** - User decides when to commit - User writes final commit message (AI provides suggestion) - User manages branches, PRs, and releases - AI is an assistant, not a driver **AI Suggests at Boundaries:** - Suggests when logical unit complete - Provides structured commit message - Accepts decline without repeating - Trusts session memory over `git status` **Commits Enable Release Notes:** - Technical accuracy preserved (for developers) - Structure enables automation (Conventional Commits) - Impact sections provide user context (for release notes) - Future AI translates into user-friendly format ### Release Notes Generation **Multiple Options Available:** 1. **Helper Script** (recommended, foolproof) - Script: `./scripts/prepare-release VERSION` - Bumps manifest.json version → commits → creates tag locally - You review and push when ready - Example: `./scripts/prepare-release 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 3. **Local Script** (testing, preview, and updating releases) - Script: `./scripts/generate-release-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:** ```bash # Generate and preview notes ./scripts/generate-release-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 # Force specific backend RELEASE_NOTES_BACKEND=copilot ./scripts/generate-release-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 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 **Recommended Release Workflow:** ```bash # Step 1: Get version suggestion (analyzes commits since last release) ./scripts/suggest-version # Output shows: # - Commit analysis (features, fixes, breaking changes) # - Suggested version based on Semantic Versioning # - Alternative versions (MAJOR/MINOR/PATCH) # - Preview and release commands # Step 2: Preview release notes (with AI if available) ./scripts/generate-release-notes v0.2.0 HEAD # Step 3: Prepare release (bumps manifest.json + creates tag) ./scripts/prepare-release 0.3.0 # Or without argument to show suggestion first: ./scripts/prepare-release # Step 4: Review changes git log -1 --stat git show v0.3.0 # Step 5: Push when ready git push origin main v0.3.0 # Done! CI/CD creates release automatically with git-cliff notes ``` **Alternative: Improve existing release with AI:** If you want better release notes after the automated release: ```bash # Generate AI-powered notes and update existing release ./scripts/generate-release-notes v0.2.0 v0.3.0 # Script will: # 1. Generate notes (uses AI if available locally) # 2. Detect existing GitHub release # 3. Ask: "Do you want to update the release notes on GitHub? [y/N]" # 4. Update release automatically if you confirm # This allows: # - Fast automated releases (CI uses git-cliff) # - Manual AI improvement when desired (uses Copilot quota only on request) ``` **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) - **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) **Alternative: Manual Bump (with Auto-Tag Safety Net):** ```bash # 1. Bump manifest.json manually vim custom_components/tibber_prices/manifest.json # "version": "0.3.0" git commit -am "chore(release): bump version to 0.3.0" git push # 2. Auto-Tag workflow detects manifest.json change → creates tag # 3. Release workflow creates GitHub release ``` **Using the Local Script:** ```bash # Generate from latest tag to HEAD ./scripts/generate-release-notes # Generate between specific tags ./scripts/generate-release-notes v1.0.0 v1.1.0 # Force specific backend RELEASE_NOTES_BACKEND=manual ./scripts/generate-release-notes # Disable AI (use in CI/CD) USE_AI=false ./scripts/generate-release-notes ``` **Backend Selection Logic:** 1. If `$RELEASE_NOTES_BACKEND` set → use that backend 2. Else if in CI/CD (`$CI` or `$GITHUB_ACTIONS`) → skip AI, use git-cliff or manual 3. Else if `USE_AI=false` → skip AI, use git-cliff or manual 4. Else if GitHub Copilot CLI available (`copilot` command) → use AI (best quality, smart grouping) 5. Else if git-cliff available → use template-based (fast, reliable, 1:1 commit mapping) 6. Else → use manual grep/awk parsing (always works) **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 - **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 - **manual** (grep/awk): - ✅ Always available (no dependencies) - ✅ Basic commit categorization - ❌ No commit grouping - ❌ Basic formatting only **Output Format:** All backends produce GitHub-flavored Markdown with consistent structure: ```markdown ## 🎉 New Features - **scope**: Description ([commit_hash](link)) User-visible impact from "Impact:" section - **scope**: Combined description ([hash1](link1), [hash2](link2)) # AI backend only Multiple related commits grouped together ``` ## 🐛 Bug Fixes - **scope**: Description ([commit_hash](link)) User-visible impact ## 📚 Documentation ... ```` **Installing Optional Backends:** ```bash # git-cliff (fast, reliable, used in CI/CD) # Auto-installed in DevContainer via scripts/setup # See: https://git-cliff.org/docs/installation cargo install git-cliff # or download binary from releases ```` **When to Use Which:** - **GitHub Button**: When working with PRs, quick manual releases - **Local Script**: Before committing to test release notes, manual review needed - **CI/CD**: Automatic releases on tag push (production workflow) **Format Requirements:** - **HACS**: No specific format required, uses GitHub releases as-is - **Home Assistant**: No specific format required for custom integrations - **Markdown**: Standard GitHub-flavored Markdown supported - **HTML**: Can include `` tags for special notices (HA update entities only) **Validate JSON files:** ```bash # After editing translation files, validate syntax (ruff doesn't check JSON) python -m json.tool custom_components/tibber_prices/translations/de.json > /dev/null # Or validate all translation files at once: for f in custom_components/tibber_prices/translations/*.json; do python -m json.tool "$f" > /dev/null && echo "✓ $f" || echo "✗ INVALID: $f" done ``` **Why:** `ruff` only formats/lints Python code. JSON syntax errors (trailing commas, missing quotes) will cause HA to fail at runtime with cryptic error messages. Always validate JSON after manual edits. ## Linting Best Practices **Always use the provided scripts:** ```bash ./scripts/lint # Auto-fix mode ./scripts/lint-check # Check-only (CI mode) ``` **Why not call `ruff` directly?** Calling `ruff` or `uv run ruff` directly can cause unintended side effects: - May install the integration as a Python package (creates `__pycache__`, `.egg-info`, etc.) - HA will then load the **installed** version instead of the **development** version from `custom_components/` - Causes confusing behavior where code changes don't take effect **Exception:** If you need to run `ruff` with custom flags not supported by our scripts: 1. Run your custom `ruff` command 2. **Immediately after**, clean up any installation artifacts: ```bash # Remove any accidentally installed package uv pip uninstall tibber_prices 2>/dev/null || true # Clean up cache and build artifacts find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true ``` 3. Ask user to restart HA: `./scripts/develop` **When in doubt:** Stick to `./scripts/lint` - it's tested and safe. **Key commands:** - Dev container includes `hass` CLI for manual HA operations - Use `uv run --active` prefix for running Python tools in the venv - `pyproject.toml` (under `[tool.ruff]`) enforces max line length 120, complexity ≤25, Python 3.13 target ## Critical Project-Specific Patterns **1. Translation Loading (Async-First)** Always load translations at integration setup or before first use: ```python # In __init__.py async_setup_entry: await async_load_translations(hass, "en") await async_load_standard_translations(hass, "en") ``` Access cached translations synchronously later via `get_translation(path, language)`. **2. Price Data Enrichment** Never use raw API price data directly. Always enrich first: ```python from .price_utils import enrich_price_info_with_differences enriched = enrich_price_info_with_differences( price_info_data, # Raw API response thresholds, # User-configured rating thresholds ) ``` This adds `trailing_avg_24h`, `difference`, `rating_level` to each interval. **3. Time Handling** Always prefer Home Assistant utilities over standard library equivalents. Use `dt_util` from `homeassistant.util` instead of Python's `datetime` module. **Critical:** Always use `dt_util.as_local()` when comparing API timestamps to local time: ```python from homeassistant.util import dt as dt_util # ✅ Use dt_util for timezone-aware operations price_time = dt_util.parse_datetime(price_data["startsAt"]) price_time = dt_util.as_local(price_time) # IMPORTANT: Convert to HA's local timezone now = dt_util.now() # Current time in HA's timezone # ❌ Avoid standard library datetime for timezone operations # from datetime import datetime # now = datetime.now() # Don't use this ``` When you need Python's standard datetime types (e.g., for type annotations), import only specific types: ```python from datetime import date, datetime, timedelta # For type hints from homeassistant.util import dt as dt_util # For operations def _needs_tomorrow_data(self, tomorrow_date: date) -> bool: """Use date type hint but dt_util for operations.""" price_time = dt_util.parse_datetime(starts_at) price_date = dt_util.as_local(price_time).date() # Convert to local before extracting date ``` **4. Coordinator Data Structure** Access coordinator data like: ```python coordinator.data = { "user_data": {...}, # Cached user info from viewer query "priceInfo": { "yesterday": [...], # List of enriched price dicts "today": [...], "tomorrow": [...], "currency": "EUR", }, } ``` **5. Service Response Pattern** Services use `SupportsResponse.ONLY` and must return dicts: ```python @callback def async_setup_services(hass: HomeAssistant) -> None: hass.services.async_register( DOMAIN, "get_price", _get_price, schema=PRICE_SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) ``` ## Common Pitfalls (HA-Specific) **1. Entity State Class Compatibility:** ```python # ❌ Wrong - MONETARY with MEASUREMENT state class class PriceSensor(SensorEntity): _attr_device_class = SensorDeviceClass.MONETARY _attr_state_class = SensorStateClass.MEASUREMENT # ← WRONG! # ✅ Correct - MONETARY with TOTAL or None class PriceSensor(SensorEntity): _attr_device_class = SensorDeviceClass.MONETARY _attr_state_class = SensorStateClass.TOTAL # Or None for snapshots ``` Rule: Check [HA sensor docs](https://developers.home-assistant.io/docs/core/entity/sensor) for valid `device_class` + `state_class` combinations. Common mistakes: MONETARY requires TOTAL, TIMESTAMP requires None. **2. Config Flow Input Validation:** ```python # ❌ Missing validation - creates broken entries async def async_step_user(self, user_input=None): if user_input is not None: return self.async_create_entry(title="Name", data=user_input) # ✅ Always validate before creating entry async def async_step_user(self, user_input=None): if user_input is not None: errors = {} try: await validate_api_connection(self.hass, user_input) except InvalidAuth: errors["base"] = "invalid_auth" except CannotConnect: errors["base"] = "cannot_connect" else: return self.async_create_entry(title="Name", data=user_input) return self.async_show_form(step_id="user", errors=errors, ...) ``` Rule: ALWAYS test API connection/validate data before `async_create_entry()`. Use specific error keys for proper translation. **3. Don't Override async_update() with DataUpdateCoordinator:** ```python # ❌ Unnecessary - coordinator handles this class MySensor(CoordinatorEntity): async def async_update(self): await self.coordinator.async_request_refresh() # ✅ Only implement properties class MySensor(CoordinatorEntity): @property def native_value(self): return self.coordinator.data["value"] ``` Rule: When using `DataUpdateCoordinator`, entities get updates automatically. Don't implement `async_update()`. **4. Service Response Declaration:** ```python # ❌ Returns data without declaring response support hass.services.async_register(DOMAIN, "get_data", handler) # ✅ Explicit response support declaration hass.services.async_register( DOMAIN, "get_data", handler, supports_response=SupportsResponse.ONLY, # ONLY, OPTIONAL, or NONE ) ``` Rule: Services returning data MUST declare `supports_response`. Use `ONLY` for data-only services, `OPTIONAL` for dual-purpose, `NONE` for action-only. ## Code Quality Rules **Ruff config (`pyproject.toml` under `[tool.ruff]`):** We use **Ruff** (which replaces Black, Flake8, isort, and more) as our sole linter and formatter: - Max line length: **120** chars (not 88 from Ruff's default) - Max complexity: **25** (McCabe) - Target: Python 3.13 - No unused imports/variables (`F401`, `F841`) - No mutable default args (`B008`) - Use `_LOGGER` not `print()` (`T201`) **Import order (enforced by isort):** 1. Python stdlib (only specific types needed, e.g., `from datetime import date, datetime, timedelta`) 2. Third-party (`homeassistant.*`, `aiohttp`, etc.) 3. Local (`.api`, `.const`, etc.) **Import best practices:** - Prefer Home Assistant utilities over stdlib equivalents: `from homeassistant.util import dt as dt_util` instead of `import datetime` - Import only specific stdlib types when needed for type hints: `from datetime import date, datetime, timedelta` - Use `dt_util` for all datetime operations (parsing, timezone conversion, current time) - Avoid aliasing stdlib modules with same names as HA utilities (e.g., `import datetime as dt` conflicts with `dt_util`) **Error handling best practices:** - Keep try blocks minimal - only wrap code that can throw exceptions - Process data **after** the try/except block, not inside - Catch specific exceptions, avoid bare `except Exception:` (allowed only in config flows and background tasks) - Use `ConfigEntryNotReady` for temporary failures (device offline) - Use `ConfigEntryAuthFailed` for auth issues - Use `ServiceValidationError` for user input errors in services **Logging guidelines:** - Use lazy logging: `_LOGGER.debug("Message with %s", variable)` - No periods at end of log messages - No integration name in messages (added automatically) - Debug level for non-user-facing messages **Function organization:** Public entry points → direct helpers (call order) → pure utilities. Prefix private helpers with `_`. **Legacy/Backwards compatibility:** - **Do NOT add legacy migration code** unless the change was already released in a version tag - **Check if released**: Use `./scripts/check-if-released ` to verify if code is in any `v*.*.*` tag - **Example**: If introducing breaking config change in commit `abc123`, run `./scripts/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 - **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. **Translation sync:** When updating `/translations/en.json`, update ALL language files (`de.json`, etc.) with same keys (placeholder values OK). **Documentation language:** - **CRITICAL**: All user-facing documentation (`README.md`, `/docs/user/`, `/docs/development/`) MUST be written in **English** - **Code comments**: Always use English for code comments and docstrings - **UI translations**: Multi-language support exists in `/translations/` and `/custom_translations/` (de, en, nb, nl, sv) for UI strings only - **Why English-only docs**: Ensures maintainability, accessibility to global community, and consistency with Home Assistant ecosystem - **Entity names in documentation**: Use **translated display names** from `/translations/en.json` (what users see), not internal entity IDs. Example: "Best Price Period" not "sensor.tibber_home_best_price_period" (add entity ID as comment if needed for clarity). **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) - **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 - **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) **Language style and tone:** - **Informal address**: Always use informal "you" forms (German: "du" not "Sie", Dutch: "je/jouw" not "u/uw"). This applies to all translations. - **Gender-neutral language**: Use gender-neutral formulations where possible, but keep them natural - avoid forced or artificial constructions. - **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 ## Ruff Code Style Guidelines These rules ensure generated code passes `./scripts/lint` on first try. Ruff enforces these automatically. **String Formatting:** ```python # ✅ Use f-strings for simple formatting message = f"Found {count} items" url = f"{base_url}/api/{endpoint}" # ✅ Use lazy logging (no f-strings in logger calls) _LOGGER.debug("Processing %s items", count) # ❌ Avoid .format() and % formatting message = "Found {} items".format(count) # Ruff will suggest f-string ``` **String Quotes:** ```python # ✅ Use double quotes (Ruff's default) name = "tibber_prices" message = "Hello world" # ✅ Use single quotes to avoid escaping html = '
content
' # ❌ Inconsistent quote usage name = 'tibber_prices' # Ruff will change to double quotes ``` **Trailing Commas:** ```python # ✅ Always use trailing commas in multi-line structures SENSOR_TYPES = [ "current_price", "min_price", "max_price", # ← Trailing comma ] # ✅ Also for function arguments def calculate_average( prices: list[dict], start_time: datetime, end_time: datetime, # ← Trailing comma ) -> float: pass # ❌ Missing trailing comma SENSOR_TYPES = [ "current_price", "min_price", "max_price" # Ruff will add trailing comma ] ``` **Docstrings:** ```python # ✅ Use triple double-quotes, single-line for simple cases def get_price() -> float: """Return current electricity price.""" return 0.25 # ✅ Multi-line docstrings: summary line, blank, details def calculate_average(prices: list[dict]) -> float: """Calculate average price from interval list. Args: prices: List of price dictionaries with 'total' key. Returns: Average price as float. """ return sum(p["total"] for p in prices) / len(prices) # ❌ Single quotes or missing docstrings on public functions def get_price() -> float: '''Return price''' # Ruff will change to double quotes ``` **Line Breaking:** ```python # ✅ Break long lines at logical points result = some_function( argument1=value1, argument2=value2, argument3=value3, ) # ✅ Break long conditions if ( price > threshold and time_of_day == "peak" and day_of_week in ["Monday", "Friday"] ): do_something() # ✅ Chain methods with line breaks df = ( data_frame .filter(lambda x: x > 0) .sort_values() .reset_index() ) ``` **Type Annotations:** ```python # ✅ Annotate function signatures (public functions) def get_current_price(coordinator: DataUpdateCoordinator) -> float: """Get current price from coordinator.""" return coordinator.data["priceInfo"]["today"][0]["total"] # ✅ Use modern type syntax (Python 3.13) def process_prices(prices: list[dict[str, Any]]) -> dict[str, float]: pass # ❌ Avoid old-style typing (List, Dict from typing module) from typing import List, Dict def process_prices(prices: List[Dict[str, Any]]) -> Dict[str, float]: # Use list, dict instead pass # ✅ Optional parameters def fetch_data(home_id: str, max_retries: int = 3) -> dict | None: pass ``` **Import Grouping:** ```python # ✅ Correct order with blank lines between groups from datetime import date, datetime, timedelta # Stdlib from homeassistant.core import HomeAssistant # Third-party from homeassistant.util import dt as dt_util from .const import DOMAIN # Local from .coordinator import TibberPricesDataUpdateCoordinator # ❌ Mixed order or missing blank lines from .const import DOMAIN from datetime import datetime from homeassistant.core import HomeAssistant # Ruff will reorder ``` **List/Dict Comprehensions:** ```python # ✅ Use comprehensions for simple transformations prices = [interval["total"] for interval in data] price_map = {interval["startsAt"]: interval["total"] for interval in data} # ✅ Break long comprehensions prices = [ interval["total"] for interval in data if interval["total"] is not None ] # ❌ Don't use comprehensions for complex logic result = [ # Use regular loop instead calculate_something_complex(x, y, z) for x in items for y in x.nested if some_complex_condition(y) for z in y.more_nested ] ``` **Common Ruff Auto-fixes:** - Unused imports → removed automatically - Unused variables → prefixed with `_` if intentional: `_unused = value` - Mutable default args → use `None` with `if x is None: x = []` - `== True` / `== False` → simplified to `if x:` / `if not x:` - Long lines → Ruff suggests breaks but may need manual adjustment ## Attribute Naming Conventions Entity attributes exposed to users must be **self-explanatory and descriptive**. Follow these rules to ensure clarity in automations and dashboards: ### General Principles 1. **Be Explicit About Context**: Attribute names should indicate what the value represents AND how/where it was calculated 2. **Avoid Ambiguity**: Generic terms like "status", "value", "data" need qualifiers 3. **Show Relationships**: When comparing/calculating, name must show what is compared to what 4. **Consistency First**: Follow established patterns in the codebase ### Attribute Ordering Attributes should follow a **logical priority order** to make the most important information easily accessible in automations and UI: **Standard Order Pattern:** ```python attributes = { # 1. Time information (when does this apply?) "timestamp": ..., # ALWAYS FIRST: Reference time for state/attributes validity "start": ..., "end": ..., "duration_minutes": ..., # 2. Core decision attributes (what should I do?) "level": ..., # Price level (VERY_CHEAP, CHEAP, NORMAL, etc.) "rating_level": ..., # Price rating (LOW, NORMAL, HIGH) # 3. Price statistics (how much does it cost?) "price_avg": ..., "price_min": ..., "price_max": ..., # 4. Price differences (optional - how does it compare?) "price_diff_from_daily_min": ..., "price_diff_from_daily_min_%": ..., # 5. Detail information (additional context) "hour": ..., "minute": ..., "time": ..., "period_position": ..., "interval_count": ..., # 6. Meta information (technical details) "periods": [...], # Nested structures last "intervals": [...], # 7. Extended descriptions (always last) "description": "...", # Short description from custom_translations (always shown) "long_description": "...", # Detailed explanation from custom_translations (shown when CONF_EXTENDED_DESCRIPTIONS enabled) "usage_tips": "...", # Usage examples from custom_translations (shown when CONF_EXTENDED_DESCRIPTIONS enabled) } ``` **Critical: The `timestamp` Attribute** The `timestamp` attribute **MUST always be first** in every sensor's attributes. It serves as the reference time indicating: - **For which interval** the state and attributes are valid - **Current interval sensors**: Contains `startsAt` of the current 15-minute interval - **Future/forecast sensors**: Contains `startsAt` of the future interval being calculated - **Statistical sensors (min/max)**: Contains `startsAt` of the specific interval when the extreme value occurs - **Statistical sensors (avg)**: Contains start of the day (00:00) since average applies to entire day This allows users to verify data freshness and understand temporal context without parsing other attributes. **Rationale:** - **Time first**: Users need to know when/for which interval the data applies before interpreting values - **Decisions next**: Core attributes for automation logic (is it cheap/expensive?) - **Prices after**: Actual values to display or use in calculations - **Differences optionally**: Contextual comparisons if relevant - **Details follow**: Supplementary information for deeper analysis - **Meta last**: Complex nested data and technical information - **Descriptions always last**: Human-readable help text from `custom_translations/` (must always be defined; `description` always shown, `long_description` and `usage_tips` shown only when user enables `CONF_EXTENDED_DESCRIPTIONS`) **In Practice:** ```python # ✅ Good: Follows priority order { "timestamp": "2025-11-08T14:00:00+01:00", # ALWAYS first "start": "2025-11-08T14:00:00+01:00", "end": "2025-11-08T15:00:00+01:00", "rating_level": "LOW", "price_avg": 18.5, "interval_count": 4, "intervals": [...] } # ❌ Bad: Random order makes it hard to scan { "intervals": [...], "interval_count": 4, "rating_level": "LOW", "start": "2025-11-08T14:00:00+01:00", "price_avg": 18.5, "end": "2025-11-08T15:00:00+01:00" } ``` ### Naming Patterns **Time-based Attributes:** - Use `next_*` for future calculations starting from the next interval (not "future\_\*") - Use `trailing_*` for backward-looking calculations - Use `leading_*` for forward-looking calculations - Always include the time span: `next_3h_avg`, `trailing_24h_max` - For multi-part periods, be specific: `second_half_6h_avg` (not "later_half") **Counting Attributes:** - Use singular `_count` for counting items: `interval_count`, `period_count` - Exception: `intervals_available` is a status indicator (how many are available), not a count of items being processed - Prefer singular form: `interval_count` over `intervals_count` (the word "count" already implies plurality) **Difference/Comparison Attributes:** - Use `_diff` suffix (not "difference") - Always specify what is being compared: `price_diff_from_daily_min`, `second_half_3h_diff_from_current` - For percentages, use `_diff_%` suffix with underscore: `price_diff_from_max_%` **Duration Attributes:** - Be specific about scope: `remaining_minutes_in_period` (not "after_interval") - Pattern: `{remaining/elapsed}_{unit}_in_{scope}` **Status/Boolean Attributes:** - Use descriptive suffixes: `data_available` (not just "available") - Qualify generic terms: `data_status` (not just "status") - Pattern: `{what}_{status_type}` like `tomorrow_data_status` **Grouped/Nested Data:** - Describe the grouping: `intervals_by_hour` (not just "hours") - Pattern: `{items}_{grouping_method}` **Price-Related Attributes:** - Period averages: `period_price_avg` (average across the period) - Reference comparisons: `period_price_diff_from_daily_min` (period avg vs daily min) - Interval-specific: `interval_price_diff_from_daily_max` (current interval vs daily max) ### Examples **❌ Bad (Ambiguous):** ```python attributes = { "future_avg_3h": 0.25, # Future when? From when? "later_half_diff_%": 5.2, # Later than what? Diff from what? "remaining_minutes": 45, # Remaining in what? "status": "partial", # Status of what? "hours": [{...}], # What about hours? "intervals_count": 12, # Should be singular: interval_count } ``` **✅ Good (Clear):** ```python attributes = { "next_3h_avg": 0.25, # Average of next 3 hours from next interval "second_half_3h_diff_from_current_%": 5.2, # Second half of 3h window vs current price "remaining_minutes_in_period": 45, # Minutes remaining in the current period "data_status": "partial", # Status of data availability "intervals_by_hour": [{...}], # Intervals grouped by hour "interval_count": 12, # Number of intervals (singular) } ``` ### Before Adding New Attributes Ask yourself: 1. **Would a user understand this without reading documentation?** 2. **Is it clear what time period/scope this refers to?** 3. **If it's a calculation, is it obvious what's being compared/calculated?** 4. **Does it follow existing patterns in the codebase?** If the answer to any is "no", make the name more explicit. ## Common Tasks **Add a new sensor:** 1. Define entity description in `sensor.py` (add to `SENSOR_TYPES`) 2. Add translation keys to `/translations/en.json` and `/custom_translations/en.json` 3. Sync all language files 4. Implement `@property` methods in `TibberPricesSensor` class **Modify price calculations:** Edit `price_utils.py` or `average_utils.py`. These are stateless pure functions operating on price lists. **Add a new service:** 1. Define schema in `services.py` (top-level constants) 2. Add service definition to `services.yaml` 3. Implement handler function in `services.py` 4. Register in `async_setup_services()` **Change update intervals:** Edit `UPDATE_INTERVAL` in `coordinator.py` (default: 15 min) or `QUARTER_HOUR_BOUNDARIES` for entity refresh timing. **Debug GraphQL queries:** Check `api.py` → `QueryType` enum and `_build_query()` method. Queries are dynamically constructed based on operation type. ## Debugging Unknown Home Assistant Patterns When encountering unfamiliar HA patterns (especially UI/config flow/translation related): **1. Check Official HA Documentation First:** - **Config Flow**: https://developers.home-assistant.io/docs/config_entries_config_flow_handler - **Translations**: https://developers.home-assistant.io/docs/internationalization/core - **Selectors**: https://developers.home-assistant.io/docs/blueprint/selectors - **Data Entry Flow**: https://developers.home-assistant.io/docs/data_entry_flow_index **2. Search HA Core Codebase:** - Repository: https://github.com/home-assistant/core - Look for similar patterns in core integrations (use GitHub search) - Check `homeassistant/helpers/` for utility patterns - Example: Search for `translation_key` usage to see real-world examples **3. Test Incrementally:** - Make small changes, test each one - Don't assume complex solutions work without verification - Ask user to test with `./scripts/develop` when needed **Real Example from This Project:** During translation implementation, we tried several incorrect structures: - ❌ `selector.select.options.{field_name}` (didn't work) - ❌ `selector.select.{translation_key}` (didn't work) - ❌ `options.step.{step_id}.data.{field}.options` (overly complex) Only after consulting the official HA docs did we discover the correct pattern: - ✅ `selector.{translation_key}.options.{value}` (simple, flat structure) **Lesson:** When stuck, consult official docs first - don't guess at complex nested structures. ## Anti-Patterns to Avoid **Never do these:** ```python # ❌ Blocking operations in event loop data = requests.get(url) # Use aiohttp with async_get_clientsession(hass) time.sleep(5) # Use await asyncio.sleep(5) # ❌ Processing data inside try block try: data = await api.get_data() processed = data["value"] * 100 # Move outside try self._attr_native_value = processed except ApiError: pass # ❌ Hardcoded strings (not translatable) self._attr_name = "Temperature Sensor" # Use translation_key instead # ❌ Accessing hass.data directly in tests coord = hass.data[DOMAIN][entry.entry_id] # Use proper fixtures # ❌ User-configurable polling intervals vol.Optional("scan_interval"): cv.positive_int # Not allowed, integration determines this # ❌ Using standard library datetime for timezone operations from datetime import datetime now = datetime.now() # Use dt_util.now() instead ``` **Do these instead:** ```python # ✅ Async operations data = await session.get(url) await asyncio.sleep(5) # ✅ Process after exception handling try: data = await api.get_data() except ApiError: return processed = data["value"] * 100 # Safe processing after try/except # ✅ Translatable entities _attr_has_entity_name = True _attr_translation_key = "temperature_sensor" # ✅ Proper test setup with fixtures @pytest.fixture async def init_integration(hass, mock_config_entry): mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) return mock_config_entry # ✅ Use Home Assistant datetime utilities from homeassistant.util import dt as dt_util now = dt_util.now() # Timezone-aware current time ```